feat: cut over Ticket and Objective read tools

This commit is contained in:
2026-08-17 05:18:38 +09:00
parent 14aa1aabea
commit 4964583868
7 changed files with 354 additions and 157 deletions
+60 -61
View File
@@ -36,8 +36,8 @@ const MAX_DIAGNOSTIC_LIMIT: usize = 500;
pub const TICKET_BASE_TOOL_NAMES: [&str; 14] = [ pub const TICKET_BASE_TOOL_NAMES: [&str; 14] = [
"TicketCreate", "TicketCreate",
"TicketEditItem", "TicketEditItem",
"TicketList", "QueryTicket",
"TicketShow", "ShowTicket",
"TicketComment", "TicketComment",
"TicketPlan", "TicketPlan",
"TicketDecision", "TicketDecision",
@@ -51,8 +51,8 @@ pub const TICKET_BASE_TOOL_NAMES: [&str; 14] = [
]; ];
pub const TICKET_BASE_READ_ONLY_TOOL_NAMES: [&str; 4] = [ pub const TICKET_BASE_READ_ONLY_TOOL_NAMES: [&str; 4] = [
"TicketList", "QueryTicket",
"TicketShow", "ShowTicket",
"TicketDependencyCheck", "TicketDependencyCheck",
"TicketDoctor", "TicketDoctor",
]; ];
@@ -71,8 +71,8 @@ pub const TICKET_ORCHESTRATION_READ_ONLY_TOOL_NAMES: [&str; 2] =
pub const TICKET_TOOL_NAMES: [&str; 19] = [ pub const TICKET_TOOL_NAMES: [&str; 19] = [
"TicketCreate", "TicketCreate",
"TicketEditItem", "TicketEditItem",
"TicketList", "QueryTicket",
"TicketShow", "ShowTicket",
"TicketComment", "TicketComment",
"TicketPlan", "TicketPlan",
"TicketDecision", "TicketDecision",
@@ -91,8 +91,8 @@ pub const TICKET_TOOL_NAMES: [&str; 19] = [
]; ];
pub const TICKET_READ_ONLY_TOOL_NAMES: [&str; 6] = [ pub const TICKET_READ_ONLY_TOOL_NAMES: [&str; 6] = [
"TicketList", "QueryTicket",
"TicketShow", "ShowTicket",
"TicketDependencyCheck", "TicketDependencyCheck",
"TicketDoctor", "TicketDoctor",
"TicketRelationQuery", "TicketRelationQuery",
@@ -121,13 +121,12 @@ backend assigns the id and writes the local Ticket file layout under the configu
const EDIT_ITEM_DESCRIPTION: &str = "Edit a Ticket item through the configured typed Ticket backend. \ const EDIT_ITEM_DESCRIPTION: &str = "Edit a Ticket item through the configured typed Ticket backend. \
This updates the current item title/body and appends an audited item_edit thread event. Intended for \ This updates the current item title/body and appends an audited item_edit thread event. Intended for \
User/Companion authoring surfaces, not Orchestrator implementation control."; User/Companion authoring surfaces, not Orchestrator implementation control.";
const LIST_DESCRIPTION: &str = "List Tickets from the configured typed Ticket backend as a \ const LIST_DESCRIPTION: &str = "Query Tickets from the configured typed Ticket backend as a bounded \
lightweight bounded overview for selection only. Filter by query (`active`, `all`, a single workflow \ overview. The local backend supports workflow-state selection; Workspace-backed Workers replace this \
state, or an explicit workflow-state list). Output is short summaries only; use TicketShow before \ definition with the richer authoritative text/event/evidence/relation/Objective/time/attention query.";
routing, closing, planning, or implementation decisions."; const SHOW_DESCRIPTION: &str = "Show one Ticket by id or exact query through the configured typed \
const SHOW_DESCRIPTION: &str = "Show one Ticket by id or exact query through the configured \ Ticket backend. Output includes bounded Markdown body, recent thread events, resolution, and artifact \
typed Ticket backend. Output includes bounded Markdown body, recent thread events, resolution, and \ metadata; Workspace-backed Workers replace this definition with the richer authoritative evidence projection.";
artifact metadata.";
const COMMENT_DESCRIPTION: &str = "Append a typed Ticket comment event. `body` is Markdown."; const COMMENT_DESCRIPTION: &str = "Append a typed Ticket comment event. `body` is Markdown.";
const PLAN_DESCRIPTION: &str = "Append a typed Ticket plan event. `body` is Markdown."; const PLAN_DESCRIPTION: &str = "Append a typed Ticket plan event. `body` is Markdown.";
const DECISION_DESCRIPTION: &str = "Append a typed Ticket decision event. `body` is Markdown."; const DECISION_DESCRIPTION: &str = "Append a typed Ticket decision event. `body` is Markdown.";
@@ -169,8 +168,8 @@ fn base_tool_description(name: &str) -> &'static str {
match name { match name {
"TicketCreate" => CREATE_DESCRIPTION, "TicketCreate" => CREATE_DESCRIPTION,
"TicketEditItem" => EDIT_ITEM_DESCRIPTION, "TicketEditItem" => EDIT_ITEM_DESCRIPTION,
"TicketList" => LIST_DESCRIPTION, "QueryTicket" => LIST_DESCRIPTION,
"TicketShow" => SHOW_DESCRIPTION, "ShowTicket" => SHOW_DESCRIPTION,
"TicketComment" => COMMENT_DESCRIPTION, "TicketComment" => COMMENT_DESCRIPTION,
"TicketPlan" => PLAN_DESCRIPTION, "TicketPlan" => PLAN_DESCRIPTION,
"TicketDecision" => DECISION_DESCRIPTION, "TicketDecision" => DECISION_DESCRIPTION,
@@ -464,7 +463,7 @@ impl TicketWorkflowStateParam {
#[derive(Debug, Clone, Copy, Deserialize, schemars::JsonSchema)] #[derive(Debug, Clone, Copy, Deserialize, schemars::JsonSchema)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
enum TicketListStateParam { enum QueryTicketStateParam {
Active, Active,
Planning, Planning,
Ready, Ready,
@@ -475,7 +474,7 @@ enum TicketListStateParam {
All, All,
} }
impl TicketListStateParam { impl QueryTicketStateParam {
fn as_list_state(self) -> Option<TicketListState> { fn as_list_state(self) -> Option<TicketListState> {
match self { match self {
Self::Planning => Some(TicketListState::Planning), Self::Planning => Some(TicketListState::Planning),
@@ -490,10 +489,10 @@ impl TicketListStateParam {
} }
#[derive(Debug, Deserialize, schemars::JsonSchema)] #[derive(Debug, Deserialize, schemars::JsonSchema)]
struct TicketListParams { struct QueryTicketParams {
/// State filter. Defaults to active Tickets (all non-closed states). Use `all` to include closed Tickets. /// State filter. Defaults to active Tickets (all non-closed states). Use `all` to include closed Tickets.
#[serde(default)] #[serde(default)]
state: Option<TicketListStateParam>, state: Option<QueryTicketStateParam>,
/// Explicit workflow-state filter list. Cannot be combined with `state`. /// Explicit workflow-state filter list. Cannot be combined with `state`.
#[serde(default)] #[serde(default)]
states: Option<Vec<TicketWorkflowStateParam>>, states: Option<Vec<TicketWorkflowStateParam>>,
@@ -502,28 +501,28 @@ struct TicketListParams {
limit: Option<usize>, limit: Option<usize>,
} }
impl TicketListParams { impl QueryTicketParams {
fn into_query(self) -> Result<(crate::TicketListQuery, String, Option<usize>), TicketError> { fn into_query(self) -> Result<(crate::TicketListQuery, String, Option<usize>), TicketError> {
let query = if let Some(states) = self.states { let query = if let Some(states) = self.states {
if self.state.is_some() { if self.state.is_some() {
return Err(TicketError::Conflict( return Err(TicketError::Conflict(
"TicketList accepts either `state` or `states`, not both".to_string(), "QueryTicket accepts either `state` or `states`, not both".to_string(),
)); ));
} }
if states.is_empty() { if states.is_empty() {
return Err(TicketError::Conflict( return Err(TicketError::Conflict(
"TicketList `states` must include at least one workflow state".to_string(), "QueryTicket `states` must include at least one workflow state".to_string(),
)); ));
} }
crate::TicketListQuery::states(states.into_iter().map(|state| state.into_list_state())) crate::TicketListQuery::states(states.into_iter().map(|state| state.into_list_state()))
} else { } else {
match self.state.unwrap_or(TicketListStateParam::Active) { match self.state.unwrap_or(QueryTicketStateParam::Active) {
TicketListStateParam::Active => crate::TicketListQuery::active(), QueryTicketStateParam::Active => crate::TicketListQuery::active(),
TicketListStateParam::All => crate::TicketListQuery::all(), QueryTicketStateParam::All => crate::TicketListQuery::all(),
state => crate::TicketListQuery::state( state => crate::TicketListQuery::state(
state state
.as_list_state() .as_list_state()
.expect("workflow state list param maps to TicketListState"), .expect("workflow state list param maps to QueryTicketState"),
), ),
} }
}; };
@@ -533,7 +532,7 @@ impl TicketListParams {
} }
#[derive(Debug, Deserialize, schemars::JsonSchema)] #[derive(Debug, Deserialize, schemars::JsonSchema)]
struct TicketShowParams { struct ShowTicketParams {
/// Ticket id. Exactly one of `id` or `query` must be provided. /// Ticket id. Exactly one of `id` or `query` must be provided.
#[serde(default)] #[serde(default)]
id: Option<String>, id: Option<String>,
@@ -768,17 +767,17 @@ struct TicketRefOutput {
} }
#[derive(Debug, Serialize)] #[derive(Debug, Serialize)]
struct TicketListOutput { struct QueryTicketOutput {
state_filter: String, state_filter: String,
count: usize, count: usize,
returned: usize, returned: usize,
truncated: bool, truncated: bool,
limit: usize, limit: usize,
tickets: Vec<TicketListTicketOutput>, tickets: Vec<QueryTicketTicketOutput>,
} }
#[derive(Debug, Serialize)] #[derive(Debug, Serialize)]
struct TicketListTicketOutput { struct QueryTicketTicketOutput {
id: String, id: String,
title: String, title: String,
state: String, state: String,
@@ -808,12 +807,12 @@ struct TicketEditItemTool {
} }
#[derive(Clone)] #[derive(Clone)]
struct TicketListTool { struct QueryTicketTool {
backend: TicketToolBackend, backend: TicketToolBackend,
} }
#[derive(Clone)] #[derive(Clone)]
struct TicketShowTool { struct ShowTicketTool {
backend: TicketToolBackend, backend: TicketToolBackend,
} }
@@ -976,28 +975,28 @@ impl Tool for TicketEditItemTool {
} }
#[async_trait] #[async_trait]
impl Tool for TicketListTool { impl Tool for QueryTicketTool {
async fn execute( async fn execute(
&self, &self,
input_json: &str, input_json: &str,
_ctx: llm_engine::tool::ToolExecutionContext, _ctx: llm_engine::tool::ToolExecutionContext,
) -> Result<ToolOutput, ToolError> { ) -> Result<ToolOutput, ToolError> {
let params: TicketListParams = parse_input("TicketList", input_json)?; let params: QueryTicketParams = parse_input("QueryTicket", input_json)?;
let (filter, state_filter, params_limit) = params let (filter, state_filter, params_limit) = params
.into_query() .into_query()
.map_err(|error| backend_error("TicketList", error))?; .map_err(|error| backend_error("QueryTicket", error))?;
let limit = bounded(params_limit, DEFAULT_LIST_LIMIT, MAX_LIST_LIMIT); let limit = bounded(params_limit, DEFAULT_LIST_LIMIT, MAX_LIST_LIMIT);
let tickets = self let tickets = self
.backend .backend
.list(filter) .list(filter)
.map_err(|error| backend_error("TicketList", error))?; .map_err(|error| backend_error("QueryTicket", error))?;
let count = tickets.len(); let count = tickets.len();
let returned_tickets: Vec<_> = tickets let returned_tickets: Vec<_> = tickets
.into_iter() .into_iter()
.take(limit) .take(limit)
.map(ticket_summary_json) .map(ticket_summary_json)
.collect(); .collect();
let output = TicketListOutput { let output = QueryTicketOutput {
state_filter: state_filter.to_string(), state_filter: state_filter.to_string(),
count, count,
returned: returned_tickets.len(), returned: returned_tickets.len(),
@@ -1017,13 +1016,13 @@ impl Tool for TicketListTool {
} }
#[async_trait] #[async_trait]
impl Tool for TicketShowTool { impl Tool for ShowTicketTool {
async fn execute( async fn execute(
&self, &self,
input_json: &str, input_json: &str,
_ctx: llm_engine::tool::ToolExecutionContext, _ctx: llm_engine::tool::ToolExecutionContext,
) -> Result<ToolOutput, ToolError> { ) -> Result<ToolOutput, ToolError> {
let params: TicketShowParams = parse_input("TicketShow", input_json)?; let params: ShowTicketParams = parse_input("ShowTicket", input_json)?;
let query = id_or_query(params.id, params.query)?; let query = id_or_query(params.id, params.query)?;
let event_limit = bounded(params.event_limit, DEFAULT_EVENT_LIMIT, MAX_EVENT_LIMIT); let event_limit = bounded(params.event_limit, DEFAULT_EVENT_LIMIT, MAX_EVENT_LIMIT);
let artifact_limit = bounded( let artifact_limit = bounded(
@@ -1039,7 +1038,7 @@ impl Tool for TicketShowTool {
let ticket = self let ticket = self
.backend .backend
.show(query) .show(query)
.map_err(|error| backend_error("TicketShow", error))?; .map_err(|error| backend_error("ShowTicket", error))?;
let summary = format!( let summary = format!(
"Ticket {} state {}", "Ticket {} state {}",
ticket.meta.id, ticket.meta.id,
@@ -1484,9 +1483,9 @@ fn id_or_query(id: Option<String>, query: Option<String>) -> Result<TicketIdOrSl
} }
} }
fn ticket_summary_json(ticket: TicketSummary) -> TicketListTicketOutput { fn ticket_summary_json(ticket: TicketSummary) -> QueryTicketTicketOutput {
let hints = ticket_list_hints(&ticket); let hints = ticket_list_hints(&ticket);
TicketListTicketOutput { QueryTicketTicketOutput {
id: ticket.id, id: ticket.id,
title: truncate_inline(ticket.title.as_str(), LIST_TITLE_MAX_CHARS), title: truncate_inline(ticket.title.as_str(), LIST_TITLE_MAX_CHARS),
state: ticket.workflow_state.as_str().to_string(), state: ticket.workflow_state.as_str().to_string(),
@@ -1722,8 +1721,8 @@ fn input_schema(name: &str) -> Value {
match name { match name {
"TicketCreate" => serde_json::to_value(schemars::schema_for!(TicketCreateParams)), "TicketCreate" => serde_json::to_value(schemars::schema_for!(TicketCreateParams)),
"TicketEditItem" => serde_json::to_value(schemars::schema_for!(TicketEditItemParams)), "TicketEditItem" => serde_json::to_value(schemars::schema_for!(TicketEditItemParams)),
"TicketList" => serde_json::to_value(schemars::schema_for!(TicketListParams)), "QueryTicket" => serde_json::to_value(schemars::schema_for!(QueryTicketParams)),
"TicketShow" => serde_json::to_value(schemars::schema_for!(TicketShowParams)), "ShowTicket" => serde_json::to_value(schemars::schema_for!(ShowTicketParams)),
"TicketComment" | "TicketPlan" | "TicketDecision" | "TicketImplementationReport" => { "TicketComment" | "TicketPlan" | "TicketDecision" | "TicketImplementationReport" => {
serde_json::to_value(schemars::schema_for!(TicketThreadEventParams)) serde_json::to_value(schemars::schema_for!(TicketThreadEventParams))
} }
@@ -1769,8 +1768,8 @@ macro_rules! impl_from_backend {
impl_from_backend!(TicketCreateTool); impl_from_backend!(TicketCreateTool);
impl_from_backend!(TicketEditItemTool); impl_from_backend!(TicketEditItemTool);
impl_from_backend!(TicketListTool); impl_from_backend!(QueryTicketTool);
impl_from_backend!(TicketShowTool); impl_from_backend!(ShowTicketTool);
impl_from_backend!(TicketCommentTool); impl_from_backend!(TicketCommentTool);
impl_from_backend!(TicketPlanTool); impl_from_backend!(TicketPlanTool);
impl_from_backend!(TicketDecisionTool); impl_from_backend!(TicketDecisionTool);
@@ -1793,8 +1792,8 @@ pub fn ticket_tools(backend: impl Into<TicketToolBackend>) -> Vec<ToolDefinition
vec![ vec![
tool_definition::<TicketCreateTool>("TicketCreate", backend.clone()), tool_definition::<TicketCreateTool>("TicketCreate", backend.clone()),
tool_definition::<TicketEditItemTool>("TicketEditItem", backend.clone()), tool_definition::<TicketEditItemTool>("TicketEditItem", backend.clone()),
tool_definition::<TicketListTool>("TicketList", backend.clone()), tool_definition::<QueryTicketTool>("QueryTicket", backend.clone()),
tool_definition::<TicketShowTool>("TicketShow", backend.clone()), tool_definition::<ShowTicketTool>("ShowTicket", backend.clone()),
tool_definition::<TicketCommentTool>("TicketComment", backend.clone()), tool_definition::<TicketCommentTool>("TicketComment", backend.clone()),
tool_definition::<TicketPlanTool>("TicketPlan", backend.clone()), tool_definition::<TicketPlanTool>("TicketPlan", backend.clone()),
tool_definition::<TicketDecisionTool>("TicketDecision", backend.clone()), tool_definition::<TicketDecisionTool>("TicketDecision", backend.clone()),
@@ -1861,8 +1860,8 @@ mod tests {
assert_eq!( assert_eq!(
TICKET_READ_ONLY_TOOL_NAMES, TICKET_READ_ONLY_TOOL_NAMES,
[ [
"TicketList", "QueryTicket",
"TicketShow", "ShowTicket",
"TicketDependencyCheck", "TicketDependencyCheck",
"TicketDoctor", "TicketDoctor",
"TicketRelationQuery", "TicketRelationQuery",
@@ -1941,8 +1940,8 @@ mod tests {
let temp = TempDir::new().unwrap(); let temp = TempDir::new().unwrap();
let backend = backend(&temp); let backend = backend(&temp);
let create = tool_by_name(backend.clone(), "TicketCreate"); let create = tool_by_name(backend.clone(), "TicketCreate");
let list = tool_by_name(backend.clone(), "TicketList"); let list = tool_by_name(backend.clone(), "QueryTicket");
let show = tool_by_name(backend.clone(), "TicketShow"); let show = tool_by_name(backend.clone(), "ShowTicket");
let doctor = tool_by_name(backend.clone(), "TicketDoctor"); let doctor = tool_by_name(backend.clone(), "TicketDoctor");
let created = create let created = create
@@ -2004,7 +2003,7 @@ mod tests {
async fn ticket_list_tool_truncates_long_titles_and_hints() { async fn ticket_list_tool_truncates_long_titles_and_hints() {
let temp = TempDir::new().unwrap(); let temp = TempDir::new().unwrap();
let backend = backend(&temp); let backend = backend(&temp);
let list = tool_by_name(backend.clone(), "TicketList"); let list = tool_by_name(backend.clone(), "QueryTicket");
let mut ticket = NewTicket::new(format!( let mut ticket = NewTicket::new(format!(
"Long Title {}", "Long Title {}",
"x".repeat(LIST_TITLE_MAX_CHARS + 40) "x".repeat(LIST_TITLE_MAX_CHARS + 40)
@@ -2032,7 +2031,7 @@ mod tests {
async fn ticket_list_tool_default_and_max_limits_are_bounded() { async fn ticket_list_tool_default_and_max_limits_are_bounded() {
let temp = TempDir::new().unwrap(); let temp = TempDir::new().unwrap();
let backend = backend(&temp); let backend = backend(&temp);
let list = tool_by_name(backend.clone(), "TicketList"); let list = tool_by_name(backend.clone(), "QueryTicket");
for index in 0..(MAX_LIST_LIMIT + 5) { for index in 0..(MAX_LIST_LIMIT + 5) {
backend backend
.create(NewTicket::new(format!("Ticket {index:03}"))) .create(NewTicket::new(format!("Ticket {index:03}")))
@@ -2083,7 +2082,7 @@ mod tests {
async fn ticket_list_tool_caps_all_and_closed_default_listing() { async fn ticket_list_tool_caps_all_and_closed_default_listing() {
let temp = TempDir::new().unwrap(); let temp = TempDir::new().unwrap();
let backend = backend(&temp); let backend = backend(&temp);
let list = tool_by_name(backend.clone(), "TicketList"); let list = tool_by_name(backend.clone(), "QueryTicket");
for index in 0..(DEFAULT_LIST_LIMIT + 3) { for index in 0..(DEFAULT_LIST_LIMIT + 3) {
let mut ticket = NewTicket::new(format!("Closed Ticket {index:03}")); let mut ticket = NewTicket::new(format!("Closed Ticket {index:03}"));
ticket.workflow_state = Some(TicketWorkflowState::Closed); ticket.workflow_state = Some(TicketWorkflowState::Closed);
@@ -2141,7 +2140,7 @@ mod tests {
async fn ticket_list_tool_accepts_multi_state_list_and_rejects_mixed_filters() { async fn ticket_list_tool_accepts_multi_state_list_and_rejects_mixed_filters() {
let temp = TempDir::new().unwrap(); let temp = TempDir::new().unwrap();
let backend = backend(&temp); let backend = backend(&temp);
let list = tool_by_name(backend.clone(), "TicketList"); let list = tool_by_name(backend.clone(), "QueryTicket");
let planning = backend.create(NewTicket::new("Planning Ticket")).unwrap(); let planning = backend.create(NewTicket::new("Planning Ticket")).unwrap();
let mut ready_input = NewTicket::new("Ready Ticket"); let mut ready_input = NewTicket::new("Ready Ticket");
ready_input.workflow_state = Some(TicketWorkflowState::Ready); ready_input.workflow_state = Some(TicketWorkflowState::Ready);
@@ -2188,7 +2187,7 @@ mod tests {
async fn ticket_list_tool_omits_body_thread_artifact_and_resolution_content() { async fn ticket_list_tool_omits_body_thread_artifact_and_resolution_content() {
let temp = TempDir::new().unwrap(); let temp = TempDir::new().unwrap();
let backend = backend(&temp); let backend = backend(&temp);
let list = tool_by_name(backend.clone(), "TicketList"); let list = tool_by_name(backend.clone(), "QueryTicket");
let close = tool_by_name(backend.clone(), "TicketClose"); let close = tool_by_name(backend.clone(), "TicketClose");
let body_secret = "ITEM_BODY_SECRET_DO_NOT_LIST"; let body_secret = "ITEM_BODY_SECRET_DO_NOT_LIST";
let thread_secret = "THREAD_SECRET_DO_NOT_LIST"; let thread_secret = "THREAD_SECRET_DO_NOT_LIST";
@@ -2325,7 +2324,7 @@ mod tests {
let record = tool_by_name(backend.clone(), "TicketRelationRecord"); let record = tool_by_name(backend.clone(), "TicketRelationRecord");
let remove = tool_by_name(backend.clone(), "TicketRelationRemove"); let remove = tool_by_name(backend.clone(), "TicketRelationRemove");
let query = tool_by_name(backend.clone(), "TicketRelationQuery"); let query = tool_by_name(backend.clone(), "TicketRelationQuery");
let show = tool_by_name(backend.clone(), "TicketShow"); let show = tool_by_name(backend.clone(), "ShowTicket");
let recorded = record let recorded = record
.execute( .execute(
@@ -2785,7 +2784,7 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn ticket_show_requires_exactly_one_identifier() { async fn ticket_show_requires_exactly_one_identifier() {
let temp = TempDir::new().unwrap(); let temp = TempDir::new().unwrap();
let show = tool_by_name(backend(&temp), "TicketShow"); let show = tool_by_name(backend(&temp), "ShowTicket");
let error = show let error = show
.execute( .execute(
&json!({ "id": "a", "query": "b" }).to_string(), &json!({ "id": "a", "query": "b" }).to_string(),
+2 -2
View File
@@ -2045,7 +2045,7 @@ impl DashboardApp {
TicketRoleLaunchContext::new(current_workspace_root(), TicketRole::Intake); TicketRoleLaunchContext::new(current_workspace_root(), TicketRole::Intake);
context.ticket = Some(TicketRef::id(ticket_id.clone())); context.ticket = Some(TicketRef::id(ticket_id.clone()));
context.user_instruction = Some(format!( context.user_instruction = Some(format!(
"Continue Intake for existing Ticket {ticket_id}. Do not create a duplicate Ticket unless the user explicitly requests one. Read TicketShow body/thread/artifacts before making routing or requirements decisions." "Continue Intake for existing Ticket {ticket_id}. Do not create a duplicate Ticket unless the user explicitly requests one. Read ShowTicket body/thread/artifacts before making routing or requirements decisions."
)); ));
let store = match PanelRegistryStore::default_for_workspace(&context.workspace_root) { let store = match PanelRegistryStore::default_for_workspace(&context.workspace_root) {
Ok(store) => store, Ok(store) => store,
@@ -3925,7 +3925,7 @@ fn build_ready_ticket_refinement_thread_body(ticket_id: &str, instruction: &str)
fn build_ready_ticket_refinement_launch_instruction(ticket_id: &str, instruction: &str) -> String { fn build_ready_ticket_refinement_launch_instruction(ticket_id: &str, instruction: &str) -> String {
format!( format!(
"Continue Ticket Intake / requirements sync for existing Ticket {ticket_id}. The Panel has returned the Ticket from ready to planning; do not queue the Ticket, do not route implementation, and do not create a duplicate unless the user explicitly asks for one. Read TicketShow body/thread/artifacts before making requirements or readiness decisions.\n\nUser refinement instruction:\n\n{instruction}" "Continue Ticket Intake / requirements sync for existing Ticket {ticket_id}. The Panel has returned the Ticket from ready to planning; do not queue the Ticket, do not route implementation, and do not create a duplicate unless the user explicitly asks for one. Read ShowTicket body/thread/artifacts before making requirements or readiness decisions.\n\nUser refinement instruction:\n\n{instruction}"
) )
} }
+76 -76
View File
@@ -26,36 +26,45 @@ impl WorkspaceHttpObjectiveBackend {
Self { client } Self { client }
} }
async fn list(&self, input: ObjectiveListInput) -> Result<ToolOutput, ToolError> { async fn list(&self, input: QueryObjectiveInput) -> Result<ToolOutput, ToolError> {
let mut url = format!( let url = format!(
"/api/w/{}/objectives", "/api/w/{}/objectives/query",
self.client.workspace_id().unwrap_or_default() self.client.workspace_id().unwrap_or_default()
); );
if let Some(limit) = input.limit { let response = send_json::<QueryObjectiveInput, serde_json::Value>(
url.push_str(&format!("?limit={}", limit.min(1000))); self.client.as_ref(),
} reqwest::Method::POST,
let response = get_json::<ObjectiveListResponse>(self.client.as_ref(), &url) &url,
&input,
)
.await .await
.map_err(backend_error)?; .map_err(backend_error)?;
let count = response.items.len();
Ok(ToolOutput { Ok(ToolOutput {
summary: format!("Listed {count} objective(s)"), summary: "Queried Objectives".to_string(),
content: Some(serde_json::to_string_pretty(&response).map_err(decode_error)?), content: Some(serde_json::to_string_pretty(&response).map_err(decode_error)?),
attachments: Vec::new(), attachments: Vec::new(),
}) })
} }
async fn show(&self, input: ObjectiveShowInput) -> Result<ToolOutput, ToolError> { async fn show(&self, input: ShowObjectiveInput) -> Result<ToolOutput, ToolError> {
let id = validate_id(&input.id, "ObjectiveShow")?; let id = validate_id(&input.id, "ShowObjective")?;
let url = self.objective_url(id); let url = format!("{}/show", self.objective_url(id));
let response = get_json::<ObjectiveDetail>(self.client.as_ref(), &url) let response = send_json::<ObjectiveShowRequest, serde_json::Value>(
self.client.as_ref(),
reqwest::Method::POST,
&url,
&ObjectiveShowRequest {
event_limit: input.event_limit,
event_cursor: input.event_cursor,
},
)
.await .await
.map_err(backend_error)?; .map_err(backend_error)?;
Ok(objective_output( Ok(ToolOutput {
format!("Read objective {}", response.id), summary: format!("Read objective {id}"),
response, content: Some(serde_json::to_string_pretty(&response).map_err(decode_error)?),
)?) attachments: Vec::new(),
})
} }
async fn create(&self, input: ObjectiveCreateInput) -> Result<ToolOutput, ToolError> { async fn create(&self, input: ObjectiveCreateInput) -> Result<ToolOutput, ToolError> {
@@ -195,13 +204,6 @@ fn backend_error(error: WorkspaceObjectiveBackendError) -> ToolError {
ToolError::ExecutionFailed(error.to_string()) ToolError::ExecutionFailed(error.to_string())
} }
async fn get_json<T: for<'de> Deserialize<'de>>(
client: &dyn WorkspaceClient,
path: &str,
) -> Result<T, WorkspaceObjectiveBackendError> {
decode_response(client.execute(WorkspaceRequest::get(path))?)
}
async fn send_json<B: Serialize, T: for<'de> Deserialize<'de>>( async fn send_json<B: Serialize, T: for<'de> Deserialize<'de>>(
client: &dyn WorkspaceClient, client: &dyn WorkspaceClient,
method: reqwest::Method, method: reqwest::Method,
@@ -270,14 +272,14 @@ pub fn workspace_http_objective_tools(client: Arc<dyn WorkspaceClient>) -> Vec<T
let backend = WorkspaceHttpObjectiveBackend::new(client); let backend = WorkspaceHttpObjectiveBackend::new(client);
vec![ vec![
objective_tool( objective_tool(
"ObjectiveList", "QueryObjective",
LIST_DESCRIPTION, LIST_DESCRIPTION,
list_schema(), list_schema(),
backend.clone(), backend.clone(),
ObjectiveOperation::List, ObjectiveOperation::List,
), ),
objective_tool( objective_tool(
"ObjectiveShow", "ShowObjective",
SHOW_DESCRIPTION, SHOW_DESCRIPTION,
show_schema(), show_schema(),
backend.clone(), backend.clone(),
@@ -367,11 +369,11 @@ impl Tool for WorkspaceHttpObjectiveTool {
) -> Result<ToolOutput, ToolError> { ) -> Result<ToolOutput, ToolError> {
match self.operation { match self.operation {
ObjectiveOperation::List => { ObjectiveOperation::List => {
let input = parse_input::<ObjectiveListInput>(input_json)?; let input = parse_input::<QueryObjectiveInput>(input_json)?;
self.backend.list(input).await self.backend.list(input).await
} }
ObjectiveOperation::Show => { ObjectiveOperation::Show => {
let input = parse_input::<ObjectiveShowInput>(input_json)?; let input = parse_input::<ShowObjectiveInput>(input_json)?;
self.backend.show(input).await self.backend.show(input).await
} }
ObjectiveOperation::Create => { ObjectiveOperation::Create => {
@@ -402,10 +404,8 @@ fn parse_input<T: for<'de> Deserialize<'de>>(input: &str) -> Result<T, ToolError
serde_json::from_str(input).map_err(|error| ToolError::InvalidArgument(error.to_string())) serde_json::from_str(input).map_err(|error| ToolError::InvalidArgument(error.to_string()))
} }
const LIST_DESCRIPTION: &str = const LIST_DESCRIPTION: &str = "Query authoritative Objectives with bounded typed filters, stable snippets, linked-Ticket context, and cursor metadata.";
"List Objective records through Backend Workspace API authority as bounded summaries."; const SHOW_DESCRIPTION: &str = "Show one authoritative Objective with its revision, full linked-Ticket context, bounded body, and paged event metadata.";
const SHOW_DESCRIPTION: &str =
"Show one Objective record by canonical id through Backend Workspace API authority.";
const CREATE_DESCRIPTION: &str = const CREATE_DESCRIPTION: &str =
"Create an Objective record through Backend Workspace API authority."; "Create an Objective record through Backend Workspace API authority.";
const EDIT_DESCRIPTION: &str = const EDIT_DESCRIPTION: &str =
@@ -422,13 +422,29 @@ fn list_schema() -> serde_json::Value {
"type":"object", "type":"object",
"additionalProperties": false, "additionalProperties": false,
"properties":{ "properties":{
"limit":{"type":["integer","null"],"minimum":0,"maximum":1000} "text":{"type":["string","null"]},
"states":{"type":"array","items":{"type":"string"},"default":[]},
"linked_ticket_id":{"type":["string","null"]},
"updated_after":{"type":["string","null"]},
"updated_before":{"type":["string","null"]},
"sort":{"type":["string","null"],"enum":["updated_desc","title",null]},
"limit":{"type":["integer","null"],"minimum":1,"maximum":100},
"cursor":{"type":["string","null"]}
} }
}) })
} }
fn show_schema() -> serde_json::Value { fn show_schema() -> serde_json::Value {
id_schema(&["id"]) json!({
"type":"object",
"additionalProperties": false,
"required":["id"],
"properties":{
"id":{"type":"string"},
"event_limit":{"type":["integer","null"],"minimum":1,"maximum":50},
"event_cursor":{"type":["string","null"]}
}
})
} }
fn create_schema() -> serde_json::Value { fn create_schema() -> serde_json::Value {
@@ -480,17 +496,6 @@ fn unlink_ticket_schema() -> serde_json::Value {
id_ticket_schema(&["id", "ticket_id"]) id_ticket_schema(&["id", "ticket_id"])
} }
fn id_schema(required: &[&str]) -> serde_json::Value {
json!({
"type":"object",
"additionalProperties": false,
"required": required,
"properties":{
"id":{"type":"string"}
}
})
}
fn id_ticket_schema(required: &[&str]) -> serde_json::Value { fn id_ticket_schema(required: &[&str]) -> serde_json::Value {
json!({ json!({
"type":"object", "type":"object",
@@ -503,14 +508,30 @@ fn id_ticket_schema(required: &[&str]) -> serde_json::Value {
}) })
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
struct ObjectiveListInput { struct QueryObjectiveInput {
text: Option<String>,
#[serde(default)]
states: Vec<String>,
linked_ticket_id: Option<String>,
updated_after: Option<String>,
updated_before: Option<String>,
sort: Option<String>,
limit: Option<usize>, limit: Option<usize>,
cursor: Option<String>,
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
struct ObjectiveShowInput { struct ShowObjectiveInput {
id: String, id: String,
event_limit: Option<usize>,
event_cursor: Option<String>,
}
#[derive(Debug, Serialize)]
struct ObjectiveShowRequest {
event_limit: Option<usize>,
event_cursor: Option<String>,
} }
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
@@ -574,30 +595,6 @@ fn default_state() -> String {
"active".to_string() "active".to_string()
} }
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
struct ObjectiveListResponse {
items: Vec<ObjectiveSummary>,
invalid_records: Vec<InvalidProjectRecord>,
record_authority: String,
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
struct InvalidProjectRecord {
label: String,
reason: String,
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
struct ObjectiveSummary {
id: String,
title: String,
state: String,
updated_at: Option<String>,
summary: String,
linked_tickets: Vec<String>,
record_source: String,
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
struct ObjectiveDetail { struct ObjectiveDetail {
id: String, id: String,
@@ -637,10 +634,10 @@ mod tests {
"ObjectiveCreate", "ObjectiveCreate",
"ObjectiveEdit", "ObjectiveEdit",
"ObjectiveLinkTicket", "ObjectiveLinkTicket",
"ObjectiveList",
"ObjectiveSetState", "ObjectiveSetState",
"ObjectiveShow",
"ObjectiveUnlinkTicket", "ObjectiveUnlinkTicket",
"QueryObjective",
"ShowObjective",
] ]
); );
} }
@@ -648,9 +645,12 @@ mod tests {
#[test] #[test]
fn objective_tool_schemas_are_bounded_and_mutation_scoped() { fn objective_tool_schemas_are_bounded_and_mutation_scoped() {
let list = list_schema(); let list = list_schema();
assert_eq!(list["properties"]["limit"]["maximum"], 1000); assert_eq!(list["properties"]["limit"]["maximum"], 100);
assert!(list["properties"]["cursor"].is_object());
assert!(list["properties"]["linked_ticket_id"].is_object());
let show = show_schema(); let show = show_schema();
assert_eq!(show["required"][0], "id"); assert_eq!(show["required"][0], "id");
assert_eq!(show["properties"]["event_limit"]["maximum"], 50);
let create = create_schema(); let create = create_schema();
assert_eq!(create["required"][0], "title"); assert_eq!(create["required"][0], "title");
let edit = edit_schema(); let edit = edit_schema();
+207 -9
View File
@@ -9,6 +9,10 @@ use std::{
sync::Arc, sync::Arc,
}; };
use async_trait::async_trait;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_json::{Value, json};
use ticket::{ use ticket::{
LocalTicketBackend, MarkdownText, NewOrchestrationPlanRecord, NewTicket, NewTicketEvent, LocalTicketBackend, MarkdownText, NewOrchestrationPlanRecord, NewTicket, NewTicketEvent,
NewTicketRelation, OrchestrationPlanKind, OrchestrationPlanRecord, Result as TicketResult, NewTicketRelation, OrchestrationPlanKind, OrchestrationPlanRecord, Result as TicketResult,
@@ -25,8 +29,168 @@ use crate::feature::{
FeatureDescriptor, FeatureDiagnostic, FeatureInstallContext, FeatureInstallError, FeatureDescriptor, FeatureDiagnostic, FeatureInstallContext, FeatureInstallError,
FeatureInstructionContribution, FeatureInstructionDeclaration, FeatureInstructionId, FeatureInstructionContribution, FeatureInstructionDeclaration, FeatureInstructionId,
FeatureModule, ServiceDeclaration, ServiceId, ToolContribution, ToolDeclaration, FeatureModule, ServiceDeclaration, ServiceId, ToolContribution, ToolDeclaration,
ToolDefinition,
}; };
use crate::worker::{WorkspaceClient, WorkspaceRequest, WorkspaceRequestMethod}; use crate::worker::{WorkspaceClient, WorkspaceRequest, WorkspaceRequestMethod};
use llm_engine::tool::{Tool, ToolError, ToolExecutionContext, ToolMeta, ToolOutput};
#[derive(Clone, Copy)]
enum WorkspaceTicketReadKind {
Query,
Show,
}
impl WorkspaceTicketReadKind {
fn name(self) -> &'static str {
match self {
Self::Query => "QueryTicket",
Self::Show => "ShowTicket",
}
}
fn description(self) -> &'static str {
match self {
Self::Query => {
"Query authoritative Workspace Tickets with bounded typed filters, stable snippets, evidence summaries, and cursor metadata."
}
Self::Show => {
"Show one authoritative Workspace Ticket with its item revision, paged thread, links, implementation reports, and current Merge Request review evidence."
}
}
}
fn schema(self) -> Value {
match self {
Self::Query => serde_json::to_value(schemars::schema_for!(WorkspaceQueryTicketInput))
.expect("QueryTicket schema serializes"),
Self::Show => serde_json::to_value(schemars::schema_for!(WorkspaceShowTicketInput))
.expect("ShowTicket schema serializes"),
}
}
}
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
struct WorkspaceQueryTicketInput {
/// Full-text match over Ticket title, item body, and bounded thread excerpts.
text: Option<String>,
/// Exact workflow states. Empty means every state.
#[serde(default)]
states: Vec<String>,
/// Exact typed event kinds that must occur in the bounded thread window.
#[serde(default)]
event_kinds: Vec<String>,
/// Required evidence kinds: implementation_report, implementation_report_after_rescope,
/// merge_request, commit, or approved_review.
#[serde(default)]
evidence: Vec<String>,
/// Current authoritative Merge Request review status.
review_status: Option<String>,
/// Attention filters: blocked, ready, awaiting_review, unresolved_changes,
/// stale_after_rescope, or missing_evidence.
#[serde(default)]
attention: Vec<String>,
related_ticket_id: Option<String>,
relation_kind: Option<String>,
linked_objective_id: Option<String>,
updated_after: Option<String>,
updated_before: Option<String>,
/// updated_desc (default), priority, or title.
sort: Option<String>,
/// Page size; bounded by the Backend to 1..=100.
limit: Option<usize>,
/// Opaque cursor returned by a prior QueryTicket page.
cursor: Option<String>,
}
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
struct WorkspaceShowTicketInput {
id: String,
/// Most-recent thread entries to return, bounded by the Backend to 1..=50.
event_limit: Option<usize>,
/// Opaque event cursor returned by a prior ShowTicket page.
event_cursor: Option<String>,
}
#[derive(Clone)]
struct WorkspaceTicketReadTool {
client: Arc<dyn WorkspaceClient>,
kind: WorkspaceTicketReadKind,
}
#[async_trait]
impl Tool for WorkspaceTicketReadTool {
async fn execute(
&self,
input: &str,
_context: ToolExecutionContext,
) -> Result<ToolOutput, ToolError> {
let workspace_id = self.client.workspace_id().ok_or_else(|| {
ToolError::InvalidArgument("Workspace Ticket reads require workspace identity".into())
})?;
let (path, body) = match self.kind {
WorkspaceTicketReadKind::Query => {
let input: WorkspaceQueryTicketInput = serde_json::from_str(&input)
.map_err(|error| ToolError::InvalidArgument(error.to_string()))?;
(
format!("/api/w/{workspace_id}/tickets/query"),
serde_json::to_value(input)
.map_err(|error| ToolError::ExecutionFailed(error.to_string()))?,
)
}
WorkspaceTicketReadKind::Show => {
let input: WorkspaceShowTicketInput = serde_json::from_str(&input)
.map_err(|error| ToolError::InvalidArgument(error.to_string()))?;
if input.id.trim().is_empty() {
return Err(ToolError::InvalidArgument(
"ShowTicket.id must not be empty".into(),
));
}
let path = format!("/api/w/{workspace_id}/tickets/{}/show", input.id.trim());
let body = json!({
"event_limit": input.event_limit,
"event_cursor": input.event_cursor,
});
(path, body)
}
};
let response = self
.client
.execute(WorkspaceRequest::json(
WorkspaceRequestMethod::Post,
path,
serde_json::to_string(&body)
.map_err(|error| ToolError::Internal(error.to_string()))?,
))
.map_err(|error| ToolError::ExecutionFailed(error.to_string()))?;
if !response.is_success() {
return Err(ToolError::ExecutionFailed(format!(
"Workspace Ticket API returned HTTP {}: {}",
response.status, response.body
)));
}
Ok(ToolOutput {
summary: self.kind.name().to_string(),
content: Some(response.body),
attachments: Vec::new(),
})
}
}
fn workspace_ticket_read_definition(
client: Arc<dyn WorkspaceClient>,
kind: WorkspaceTicketReadKind,
) -> ToolDefinition {
Arc::new(move || {
let meta = ToolMeta::new(kind.name())
.description(kind.description())
.input_schema(kind.schema());
let tool: Arc<dyn Tool> = Arc::new(WorkspaceTicketReadTool {
client: client.clone(),
kind,
});
(meta, tool)
})
}
const FEATURE_ID: &str = "ticket"; const FEATURE_ID: &str = "ticket";
const FEATURE_NAME: &str = "Ticket tools"; const FEATURE_NAME: &str = "Ticket tools";
@@ -143,8 +307,8 @@ impl TicketFeatureAccess {
} }
const READ_ONLY_TOOL_NAMES: &[&str] = &[ const READ_ONLY_TOOL_NAMES: &[&str] = &[
"TicketList", "QueryTicket",
"TicketShow", "ShowTicket",
"TicketDependencyCheck", "TicketDependencyCheck",
"TicketDoctor", "TicketDoctor",
"TicketRelationQuery", "TicketRelationQuery",
@@ -168,8 +332,8 @@ const INTAKE_TOOL_NAMES: &[&str] = &["TicketIntakeReady"];
const WORKSPACE_AUTHORING_TOOL_NAMES: &[&str] = &[ const WORKSPACE_AUTHORING_TOOL_NAMES: &[&str] = &[
"TicketCreate", "TicketCreate",
"TicketEditItem", "TicketEditItem",
"TicketList", "QueryTicket",
"TicketShow", "ShowTicket",
"TicketComment", "TicketComment",
"TicketQueue", "TicketQueue",
"TicketClose", "TicketClose",
@@ -183,8 +347,8 @@ const WORKSPACE_AUTHORING_TOOL_NAMES: &[&str] = &[
#[cfg(test)] #[cfg(test)]
const WORKFLOW_TOOL_NAMES: &[&str] = &[ const WORKFLOW_TOOL_NAMES: &[&str] = &[
"TicketList", "QueryTicket",
"TicketShow", "ShowTicket",
"TicketComment", "TicketComment",
"TicketWorkflowState", "TicketWorkflowState",
"TicketClose", "TicketClose",
@@ -413,6 +577,10 @@ impl FeatureModule for TicketFeature {
ticket_workflow_instruction(), ticket_workflow_instruction(),
))?; ))?;
let allowed_tool_names = self.enabled_tool_names(); let allowed_tool_names = self.enabled_tool_names();
let workspace_client = match &self.backend {
TicketFeatureBackend::WorkspaceClient(client) => Some(client.clone()),
TicketFeatureBackend::Local { .. } => None,
};
let mut tools = context.tools(); let mut tools = context.tools();
for definition in ticket_tools(backend) { for definition in ticket_tools(backend) {
let (meta, _) = definition(); let (meta, _) = definition();
@@ -423,6 +591,15 @@ impl FeatureModule for TicketFeature {
{ {
continue; continue;
} }
let definition = match (name.as_str(), workspace_client.as_ref()) {
("QueryTicket", Some(client)) => {
workspace_ticket_read_definition(client.clone(), WorkspaceTicketReadKind::Query)
}
("ShowTicket", Some(client)) => {
workspace_ticket_read_definition(client.clone(), WorkspaceTicketReadKind::Show)
}
_ => definition,
};
tools.register(ToolContribution::new(name, definition))?; tools.register(ToolContribution::new(name, definition))?;
} }
if let TicketFeatureBackend::WorkspaceClient(client) = &self.backend { if let TicketFeatureBackend::WorkspaceClient(client) = &self.backend {
@@ -1044,6 +1221,27 @@ mod tests {
.expect("tool exists") .expect("tool exists")
} }
#[test]
fn workspace_ticket_reads_expose_bounded_query_and_show_contracts_without_legacy_aliases() {
let client: Arc<dyn WorkspaceClient> = Arc::new(
crate::worker::TestWorkspaceHttpClient::new("workspace", "http://backend"),
);
let (query, _) =
workspace_ticket_read_definition(client.clone(), WorkspaceTicketReadKind::Query)();
assert_eq!(query.name, "QueryTicket");
assert!(query.input_schema["properties"]["evidence"].is_object());
assert!(query.input_schema["properties"]["attention"].is_object());
assert!(query.input_schema["properties"]["cursor"].is_object());
let (show, _) = workspace_ticket_read_definition(client, WorkspaceTicketReadKind::Show)();
assert_eq!(show.name, "ShowTicket");
assert!(show.input_schema["properties"]["event_limit"].is_object());
let tool_names = TicketFeatureAccess::workspace_authoring().tool_names();
assert!(tool_names.contains(&"QueryTicket"));
assert!(tool_names.contains(&"ShowTicket"));
assert!(!tool_names.contains(&"TicketList"));
assert!(!tool_names.contains(&"TicketShow"));
}
#[test] #[test]
fn descriptor_declares_ticket_tools() { fn descriptor_declares_ticket_tools() {
let temp = TempDir::new().unwrap(); let temp = TempDir::new().unwrap();
@@ -1200,8 +1398,8 @@ language = "Japanese"
let descriptor_description = descriptor let descriptor_description = descriptor
.tools .tools
.iter() .iter()
.find(|tool| tool.name == "TicketShow") .find(|tool| tool.name == "ShowTicket")
.expect("TicketShow declared") .expect("ShowTicket declared")
.description .description
.clone(); .clone();
assert!(descriptor_description.contains("Ticket record language: Japanese")); assert!(descriptor_description.contains("Ticket record language: Japanese"));
@@ -1214,7 +1412,7 @@ language = "Japanese"
assert_eq!(pending_tools.len(), READ_ONLY_TOOL_NAMES.len()); assert_eq!(pending_tools.len(), READ_ONLY_TOOL_NAMES.len());
assert_eq!(report.reports[0].installed_tools, READ_ONLY_TOOL_NAMES); assert_eq!(report.reports[0].installed_tools, READ_ONLY_TOOL_NAMES);
let description = pending_tool_description(&pending_tools, "TicketShow"); let description = pending_tool_description(&pending_tools, "ShowTicket");
assert!(description.contains("Ticket record language: Japanese")); assert!(description.contains("Ticket record language: Japanese"));
assert!(description.contains("distinct from worker.language")); assert!(description.contains("distinct from worker.language"));
assert!(description.contains("Preserve protocol literals")); assert!(description.contains("Preserve protocol literals"));
+1 -1
View File
@@ -132,7 +132,7 @@ mod tests {
let request = ShutdownAfterIdleRequest::default(); let request = ShutdownAfterIdleRequest::default();
let hook = TicketIntakeReadyShutdownHook::new(request.clone(), true); let hook = TicketIntakeReadyShutdownHook::new(request.clone(), true);
hook.observe_tool_result(&tool_result("TicketShow", false)); hook.observe_tool_result(&tool_result("ShowTicket", false));
assert!(!request.is_requested()); assert!(!request.is_requested());
} }
+2 -2
View File
@@ -34,8 +34,8 @@ Maintainers can inspect the local `.yoi/tickets/` files directly when debugging
Workers with the Ticket built-in feature can use typed Ticket tools: Workers with the Ticket built-in feature can use typed Ticket tools:
- `TicketCreate` - `TicketCreate`
- `TicketList`lightweight bounded overview for selecting ids; it returns short summaries only and must not be used as body/thread/artifact authority. - `QueryTicket`bounded authoritative Ticket discovery with typed state/text/event/evidence/relation/Objective/time/attention filters, stable snippets, and cursor metadata.
- `TicketShow` — detailed authority for a single Ticket, including body/thread/artifact metadata/resolution context subject to its own bounds. - `ShowTicket` — detailed authority for one Ticket, including item revision, bounded thread/event references, relations, linked Objectives, implementation reports, and current Merge Request/review evidence.
- `TicketComment` - `TicketComment`
- `MergeRequestShow`, `MergeRequestOpen`, `MergeRequestAddRevision`, `MergeRequestComplete` - `MergeRequestShow`, `MergeRequestOpen`, `MergeRequestAddRevision`, `MergeRequestComplete`
- `MergeRequestReviewSubmit` — available only inside the attested direct-child Reviewer attempt; attempt/revision capability material is not model input. - `MergeRequestReviewSubmit` — available only inside the attested direct-child Reviewer attempt; attempt/revision capability material is not model input.
+2 -2
View File
@@ -1,9 +1,9 @@
## Ticket workflow ## Ticket workflow
Use the available typed Ticket tools as the authority for Ticket reads and mutations. Do not invoke a Ticket CLI or edit backend storage directly as an alternative implementation of those tools. Use the available typed Ticket tools as the authority for Ticket reads and mutations. Use `QueryTicket` for bounded discovery and filtering, then `ShowTicket` for the authoritative item revision, thread/evidence, relations, linked Objectives, and current Merge Request context before routing, review, or closure decisions. Do not invoke a Ticket CLI or edit backend storage directly as an alternative implementation of those tools.
Read the relevant Ticket before making implementation, routing, review, state, or closure decisions. Do not infer the current contract from an id, title, notification, or remembered summary alone. Check related or potentially duplicate Tickets when creating or materially rescoping work. Read the relevant Ticket before making implementation, routing, review, state, or closure decisions. Do not infer the current contract from an id, title, notification, or remembered summary alone. Check related or potentially duplicate Tickets when creating or materially rescoping work.
Keep durable Ticket records centered on user intent, confirmed background, requirements, acceptance criteria, binding decisions, and implementation/review evidence. Separate confirmed facts from user claims, hypotheses, and open questions. Avoid prematurely turning implementation tactics into requirements. Keep durable Ticket records centered on user intent, confirmed background, requirements, acceptance criteria, binding decisions, and implementation/review evidence. Use `QueryObjective` for bounded Objective discovery and `ShowObjective` for authoritative revision and linked-Ticket context when coordinating broader work. Separate confirmed facts from user claims, hypotheses, and open questions. Avoid prematurely turning implementation tactics into requirements.
Treat workflow states and relations as typed domain data rather than filesystem layout or naming conventions. Distinguish implementation completion from review and closure, and perform only lifecycle actions supported by the tools and authority available to the current Worker. Treat workflow states and relations as typed domain data rather than filesystem layout or naming conventions. Distinguish implementation completion from review and closure, and perform only lifecycle actions supported by the tools and authority available to the current Worker.