ticket: move local storage to .yoi/tickets
This commit is contained in:
@@ -16,7 +16,7 @@ Owns:
|
||||
|
||||
Does not own:
|
||||
|
||||
- authoritative project records (`work-items/`, git history)
|
||||
- authoritative project records (`.yoi/tickets/`, git history)
|
||||
- normal Pod turn orchestration (`llm-worker`)
|
||||
- product CLI command shape (`yoi`)
|
||||
- curated workflow definitions (`workflow`)
|
||||
|
||||
@@ -7,7 +7,10 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use ticket::{
|
||||
LocalTicketBackend, config::TicketConfig, tool::TICKET_TOOL_NAMES, tool::ticket_tools,
|
||||
LocalTicketBackend,
|
||||
config::{DEFAULT_TICKET_BACKEND_RELATIVE_PATH, TicketConfig},
|
||||
tool::TICKET_TOOL_NAMES,
|
||||
tool::ticket_tools,
|
||||
};
|
||||
|
||||
use crate::feature::{
|
||||
@@ -40,7 +43,7 @@ impl TicketFeature {
|
||||
match TicketConfig::load_workspace(workspace) {
|
||||
Ok(config) => Self::new(config.backend_root().to_path_buf()),
|
||||
Err(error) => Self {
|
||||
backend_root: workspace.join("work-items"),
|
||||
backend_root: workspace.join(DEFAULT_TICKET_BACKEND_RELATIVE_PATH),
|
||||
config_error: Some(error.to_string()),
|
||||
},
|
||||
}
|
||||
@@ -155,7 +158,7 @@ mod tests {
|
||||
use crate::hook::HookRegistryBuilder;
|
||||
use tempfile::TempDir;
|
||||
|
||||
fn make_work_items(root: &Path) {
|
||||
fn make_ticket_root(root: &Path) {
|
||||
std::fs::create_dir_all(root.join("open")).unwrap();
|
||||
std::fs::create_dir_all(root.join("pending")).unwrap();
|
||||
std::fs::create_dir_all(root.join("closed")).unwrap();
|
||||
@@ -191,9 +194,9 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn installs_ticket_tools_when_work_items_root_is_usable() {
|
||||
fn installs_ticket_tools_when_default_root_is_usable() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
make_work_items(&temp.path().join("work-items"));
|
||||
make_ticket_root(&temp.path().join(DEFAULT_TICKET_BACKEND_RELATIVE_PATH));
|
||||
let mut pending_tools = Vec::new();
|
||||
let mut hooks = HookRegistryBuilder::default();
|
||||
let report = FeatureRegistryBuilder::new()
|
||||
@@ -221,7 +224,7 @@ root = "tickets"
|
||||
profile = "project:coder"
|
||||
"#,
|
||||
);
|
||||
make_work_items(&temp.path().join("tickets"));
|
||||
make_ticket_root(&temp.path().join("tickets"));
|
||||
|
||||
let feature = ticket_tools_feature(temp.path());
|
||||
assert_eq!(feature.backend_root(), temp.path().join("tickets"));
|
||||
@@ -239,7 +242,7 @@ profile = "project:coder"
|
||||
#[test]
|
||||
fn malformed_ticket_config_fails_closed() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
make_work_items(&temp.path().join("work-items"));
|
||||
make_ticket_root(&temp.path().join(DEFAULT_TICKET_BACKEND_RELATIVE_PATH));
|
||||
write_ticket_config(
|
||||
temp.path(),
|
||||
r#"
|
||||
@@ -264,7 +267,7 @@ profile = "inherit"
|
||||
#[test]
|
||||
fn unsupported_ticket_backend_provider_fails_closed() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
make_work_items(&temp.path().join("work-items"));
|
||||
make_ticket_root(&temp.path().join(DEFAULT_TICKET_BACKEND_RELATIVE_PATH));
|
||||
write_ticket_config(
|
||||
temp.path(),
|
||||
r#"
|
||||
@@ -310,7 +313,7 @@ provider = "github"
|
||||
#[test]
|
||||
fn does_not_register_ticket_tools_when_root_lacks_status_dirs() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
std::fs::create_dir_all(temp.path().join("work-items")).unwrap();
|
||||
std::fs::create_dir_all(temp.path().join(DEFAULT_TICKET_BACKEND_RELATIVE_PATH)).unwrap();
|
||||
let mut pending_tools = Vec::new();
|
||||
let mut hooks = HookRegistryBuilder::default();
|
||||
let report = FeatureRegistryBuilder::new()
|
||||
|
||||
@@ -14,6 +14,8 @@ use serde::{Deserialize, Serialize};
|
||||
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";
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum TicketConfigError {
|
||||
@@ -107,7 +109,7 @@ impl TicketBackendConfig {
|
||||
pub fn default_for_workspace(workspace_root: &Path) -> Self {
|
||||
Self {
|
||||
provider: TicketBackendProvider::BuiltinYoiLocal,
|
||||
root: workspace_root.join("work-items"),
|
||||
root: workspace_root.join(DEFAULT_TICKET_BACKEND_RELATIVE_PATH),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -452,7 +454,9 @@ impl RawBackendConfig {
|
||||
);
|
||||
}
|
||||
};
|
||||
let root = self.root.unwrap_or_else(|| PathBuf::from("work-items"));
|
||||
let root = self
|
||||
.root
|
||||
.unwrap_or_else(|| PathBuf::from(DEFAULT_TICKET_BACKEND_RELATIVE_PATH));
|
||||
Ok(TicketBackendConfig {
|
||||
provider,
|
||||
root: join_if_relative(workspace_root, &root),
|
||||
@@ -510,7 +514,10 @@ mod tests {
|
||||
config.backend.provider,
|
||||
TicketBackendProvider::BuiltinYoiLocal
|
||||
);
|
||||
assert_eq!(config.backend.root, temp.path().join("work-items"));
|
||||
assert_eq!(
|
||||
config.backend.root,
|
||||
temp.path().join(DEFAULT_TICKET_BACKEND_RELATIVE_PATH)
|
||||
);
|
||||
for role in TicketRole::ALL {
|
||||
let role_config = config.role(role);
|
||||
assert_eq!(role_config.profile.as_str(), "inherit");
|
||||
@@ -527,7 +534,7 @@ mod tests {
|
||||
r#"
|
||||
[backend]
|
||||
provider = "builtin:yoi_local"
|
||||
root = "custom-work-items"
|
||||
root = "custom-tickets"
|
||||
|
||||
[roles.intake]
|
||||
profile = "project:intake"
|
||||
@@ -561,7 +568,7 @@ workflow = "ticket-orchestrator-routing"
|
||||
config.backend.provider,
|
||||
TicketBackendProvider::BuiltinYoiLocal
|
||||
);
|
||||
assert_eq!(config.backend.root, temp.path().join("custom-work-items"));
|
||||
assert_eq!(config.backend.root, temp.path().join("custom-tickets"));
|
||||
assert_eq!(
|
||||
config.profile_for(TicketRole::Intake).as_str(),
|
||||
"project:intake"
|
||||
@@ -638,7 +645,7 @@ system_instruction = "$workspace/not-supported"
|
||||
r#"
|
||||
[backend]
|
||||
kind = "local"
|
||||
root = "legacy-work-items"
|
||||
root = "legacy-tickets"
|
||||
"#,
|
||||
);
|
||||
|
||||
@@ -647,7 +654,7 @@ root = "legacy-work-items"
|
||||
config.backend.provider,
|
||||
TicketBackendProvider::BuiltinYoiLocal
|
||||
);
|
||||
assert_eq!(config.backend_root(), temp.path().join("legacy-work-items"));
|
||||
assert_eq!(config.backend_root(), temp.path().join("legacy-tickets"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -716,12 +723,12 @@ kind = "local"
|
||||
temp.path(),
|
||||
r#"
|
||||
[backend]
|
||||
root = "nested/work-items"
|
||||
root = "nested/tickets"
|
||||
"#,
|
||||
);
|
||||
|
||||
let config = TicketConfig::load_workspace(temp.path()).unwrap();
|
||||
assert_eq!(config.backend_root(), temp.path().join("nested/work-items"));
|
||||
assert_eq!(config.backend_root(), temp.path().join("nested/tickets"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
+11
-11
@@ -1,7 +1,7 @@
|
||||
//! Ticket domain types and the local `work-items/` file backend.
|
||||
//! Ticket domain types and the local `.yoi/tickets/` file backend.
|
||||
//!
|
||||
//! The public domain name is **Ticket**. `LocalTicketBackend` preserves the
|
||||
//! repository's current `work-items/{open,pending,closed}/<id>/` layout and the
|
||||
//! repository's current `.yoi/tickets/{open,pending,closed}/<id>/` layout and the
|
||||
//! event format used by `tickets.sh` while exposing typed Rust operations.
|
||||
|
||||
use std::collections::{BTreeMap, BTreeSet, HashMap};
|
||||
@@ -1525,7 +1525,7 @@ mod tests {
|
||||
use tempfile::TempDir;
|
||||
|
||||
fn backend(dir: &TempDir) -> LocalTicketBackend {
|
||||
LocalTicketBackend::new(dir.path().join("work-items"))
|
||||
LocalTicketBackend::new(dir.path().join("tickets"))
|
||||
}
|
||||
|
||||
fn script_path() -> PathBuf {
|
||||
@@ -1592,12 +1592,12 @@ action_required: none
|
||||
let mut input = NewTicket::new("Example Ticket");
|
||||
input.labels = vec!["ticket".into(), "backend".into()];
|
||||
let ticket = backend.create(input).unwrap();
|
||||
let dir = tmp.path().join("work-items/open").join(&ticket.id);
|
||||
let dir = tmp.path().join("tickets/open").join(&ticket.id);
|
||||
assert!(dir.join("item.md").exists());
|
||||
assert!(dir.join("thread.md").exists());
|
||||
assert!(dir.join("artifacts/.gitkeep").exists());
|
||||
assert_eq!(ticket.slug, "example-ticket");
|
||||
assert_script_ok(&tmp.path().join("work-items"), &["doctor"]);
|
||||
assert_script_ok(&tmp.path().join("tickets"), &["doctor"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1622,7 +1622,7 @@ action_required: none
|
||||
.unwrap();
|
||||
let pending_item = tmp
|
||||
.path()
|
||||
.join("work-items/pending")
|
||||
.join("tickets/pending")
|
||||
.join(&ticket.id)
|
||||
.join("item.md");
|
||||
assert!(pending_item.exists());
|
||||
@@ -1632,19 +1632,19 @@ action_required: none
|
||||
MarkdownText::new("Done.\n"),
|
||||
)
|
||||
.unwrap();
|
||||
let closed_dir = tmp.path().join("work-items/closed").join(&ticket.id);
|
||||
let closed_dir = tmp.path().join("tickets/closed").join(&ticket.id);
|
||||
assert!(closed_dir.join("resolution.md").exists());
|
||||
let thread = fs::read_to_string(closed_dir.join("thread.md")).unwrap();
|
||||
assert!(thread.contains("<!-- event: review"));
|
||||
assert!(thread.contains("status: approve"));
|
||||
assert!(thread.contains("<!-- event: close"));
|
||||
assert_script_ok(&tmp.path().join("work-items"), &["doctor"]);
|
||||
assert_script_ok(&tmp.path().join("tickets"), &["doctor"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reads_ticket_created_by_tickets_sh_and_script_mutates_rust_ticket() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let work_items = tmp.path().join("work-items");
|
||||
let work_items = tmp.path().join("tickets");
|
||||
let id = assert_script_ok(
|
||||
&work_items,
|
||||
&[
|
||||
@@ -1676,7 +1676,7 @@ action_required: none
|
||||
#[test]
|
||||
fn doctor_reports_core_consistency_errors() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let root = tmp.path().join("work-items");
|
||||
let root = tmp.path().join("tickets");
|
||||
fs::create_dir_all(root.join("open/bad/artifacts")).unwrap();
|
||||
fs::write(
|
||||
root.join("open/bad/item.md"),
|
||||
@@ -1733,7 +1733,7 @@ action_required: none
|
||||
#[test]
|
||||
fn rejects_unsafe_components_for_status_moves() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let root = tmp.path().join("work-items");
|
||||
let root = tmp.path().join("tickets");
|
||||
fs::create_dir_all(root.join("open/bad/artifacts")).unwrap();
|
||||
fs::write(
|
||||
root.join("open/bad/item.md"),
|
||||
|
||||
@@ -41,7 +41,7 @@ pub const TICKET_TOOL_NAMES: [&str; 8] = [
|
||||
];
|
||||
|
||||
const CREATE_DESCRIPTION: &str = "Create a Ticket through the configured typed Ticket backend. \
|
||||
Inputs mirror the work-items item.md fields; `title` is required, `body` is Markdown, and the \
|
||||
Inputs mirror the Ticket `item.md` fields; `title` is required, `body` is Markdown, and the \
|
||||
backend assigns the id and writes tickets.sh-compatible files under the configured backend root.";
|
||||
const LIST_DESCRIPTION: &str = "List Tickets from the configured typed Ticket backend. Filter by \
|
||||
status (`open`, `pending`, `closed`, or `all`) and optionally kind/priority/label. Output is a \
|
||||
@@ -778,7 +778,7 @@ mod tests {
|
||||
use tempfile::TempDir;
|
||||
|
||||
fn backend(temp: &TempDir) -> LocalTicketBackend {
|
||||
LocalTicketBackend::new(temp.path().join("work-items"))
|
||||
LocalTicketBackend::new(temp.path().join("tickets"))
|
||||
}
|
||||
|
||||
fn tool(definition: ToolDefinition) -> Arc<dyn Tool> {
|
||||
|
||||
@@ -15,7 +15,7 @@ Owns:
|
||||
Does not own:
|
||||
|
||||
- generated memory records (`memory`)
|
||||
- work item file lifecycle (`tickets.sh`, `work-items/`)
|
||||
- Ticket file lifecycle (`tickets.sh`, `.yoi/tickets/`)
|
||||
- Pod orchestration decisions (`pod`, workflows executed by agents)
|
||||
- product CLI command shape (`yoi`)
|
||||
|
||||
|
||||
@@ -745,7 +745,7 @@ fn default_author() -> String {
|
||||
}
|
||||
|
||||
fn help_text() -> &'static str {
|
||||
"yoi ticket\n\nUsage:\n yoi ticket create --title <title> [--slug <slug>] [--kind <kind>] [--priority P2] [--label a,b]\n yoi ticket list [--status open|pending|closed|all]\n yoi ticket show <id-or-slug>\n yoi ticket comment <id-or-slug> [--role comment|plan|decision|implementation_report] (--file <path>|--message <text>)\n yoi ticket review <id-or-slug> (--approve|--request-changes) (--file <path>|--message <text>)\n yoi ticket status <id-or-slug> <open|pending|closed>\n yoi ticket close <id-or-slug> (--resolution <text>|--file <path>)\n yoi ticket doctor\n\nOptions:\n -h, --help Print help\n\nBackend:\n Uses the workspace Ticket config at .yoi/ticket.config.toml when present.\n Supported provider: builtin:yoi_local.\n Without config, the transitional local backend root is <cwd>/work-items.\n"
|
||||
"yoi ticket\n\nUsage:\n yoi ticket create --title <title> [--slug <slug>] [--kind <kind>] [--priority P2] [--label a,b]\n yoi ticket list [--status open|pending|closed|all]\n yoi ticket show <id-or-slug>\n yoi ticket comment <id-or-slug> [--role comment|plan|decision|implementation_report] (--file <path>|--message <text>)\n yoi ticket review <id-or-slug> (--approve|--request-changes) (--file <path>|--message <text>)\n yoi ticket status <id-or-slug> <open|pending|closed>\n yoi ticket close <id-or-slug> (--resolution <text>|--file <path>)\n yoi ticket doctor\n\nOptions:\n -h, --help Print help\n\nBackend:\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"
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -786,6 +786,8 @@ mod tests {
|
||||
assert_eq!(created.status, TicketCliStatus::Success);
|
||||
assert!(created.stdout.contains("created\t"));
|
||||
assert!(created.stdout.contains("\tcli-created\topen"));
|
||||
assert!(temp.path().join(".yoi/tickets/open").exists());
|
||||
assert!(!temp.path().join("work-items").exists());
|
||||
|
||||
let listed = run(&temp, &["list", "--status", "open"]);
|
||||
assert!(listed.stdout.contains("status\tid\tslug"));
|
||||
@@ -842,7 +844,7 @@ mod tests {
|
||||
assert_eq!(doctor.status, TicketCliStatus::Success);
|
||||
assert_eq!(doctor.stdout, "doctor: ok\n");
|
||||
|
||||
let backend = LocalTicketBackend::new(temp.path().join("work-items"));
|
||||
let backend = LocalTicketBackend::new(temp.path().join(".yoi/tickets"));
|
||||
let ticket = backend
|
||||
.show(TicketIdOrSlug::Query("cli-created".to_string()))
|
||||
.unwrap();
|
||||
@@ -867,7 +869,7 @@ mod tests {
|
||||
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-work-items\"\n",
|
||||
"[backend]\nprovider = \"builtin:yoi_local\"\nroot = \"custom-tickets\"\n",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
@@ -884,7 +886,7 @@ mod tests {
|
||||
|
||||
assert!(
|
||||
temp.path()
|
||||
.join("custom-work-items/open")
|
||||
.join("custom-tickets/open")
|
||||
.read_dir()
|
||||
.unwrap()
|
||||
.any(|entry| entry
|
||||
|
||||
Reference in New Issue
Block a user