From 6f8571f77fc0be6fd81d4fd8756333ecc34becef Mon Sep 17 00:00:00 2001 From: Hare Date: Sat, 13 Jun 2026 13:08:12 +0900 Subject: [PATCH] fix: render ticket event notice from prompt resource --- crates/pod/src/prompt/catalog.rs | 5 ++ crates/pod/src/ticket_event_notify.rs | 81 +++++++++++++++---- resources/prompts/internal.toml | 2 + .../pod/ticket_event_companion_notice.md | 7 ++ 4 files changed, 81 insertions(+), 14 deletions(-) create mode 100644 resources/prompts/pod/ticket_event_companion_notice.md diff --git a/crates/pod/src/prompt/catalog.rs b/crates/pod/src/prompt/catalog.rs index 266760b8..1989c296 100644 --- a/crates/pod/src/prompt/catalog.rs +++ b/crates/pod/src/prompt/catalog.rs @@ -95,6 +95,8 @@ pub enum PodPrompt { /// Trailing Pod orchestration guidance, appended when registered tools /// include Pod-management capabilities. PodOrchestrationGuidanceSection, + /// Weak Companion Notify payload for explicit Orchestrator Ticket events. + TicketEventCompanionNotice, /// LLM-facing description for the SpawnPod tool, including discovered /// profile selectors. SpawnPodToolDescription, @@ -115,6 +117,7 @@ impl PodPrompt { Self::ResidentKnowledgeSection => "resident_knowledge_section", Self::ResidentWorkflowsSection => "resident_workflows_section", Self::PodOrchestrationGuidanceSection => "pod_orchestration_guidance_section", + Self::TicketEventCompanionNotice => "ticket_event_companion_notice", Self::SpawnPodToolDescription => "spawn_pod_tool_description", } } @@ -135,6 +138,7 @@ impl PodPrompt { PodPrompt::ResidentKnowledgeSection, PodPrompt::ResidentWorkflowsSection, PodPrompt::PodOrchestrationGuidanceSection, + PodPrompt::TicketEventCompanionNotice, PodPrompt::SpawnPodToolDescription, ]; @@ -151,6 +155,7 @@ impl PodPrompt { "resident_knowledge_section", "resident_workflows_section", "pod_orchestration_guidance_section", + "ticket_event_companion_notice", "spawn_pod_tool_description", ]; } diff --git a/crates/pod/src/ticket_event_notify.rs b/crates/pod/src/ticket_event_notify.rs index a5db8d31..aa9d1e96 100644 --- a/crates/pod/src/ticket_event_notify.rs +++ b/crates/pod/src/ticket_event_notify.rs @@ -1,12 +1,15 @@ use std::sync::Arc; use async_trait::async_trait; +use minijinja::Value as TemplateValue; use serde_json::Value; +use std::collections::BTreeMap; use ticket::{LocalTicketBackend, TicketBackend, TicketIdOrSlug}; use tracing::debug; use crate::discovery::PodDiscovery; use crate::hook::{Hook, HookPostToolAction, PostToolCall, ToolResultSummary}; +use crate::prompt::catalog::{PodPrompt, PromptCatalog}; use pod_store::PodMetadataStore; const MAX_TITLE_CHARS: usize = 96; @@ -89,28 +92,56 @@ fn build_ticket_event_notice( .ok()?; let event_kind = sanitize_one_line(&event_kind, MAX_EVENT_KIND_CHARS); + let ticket_id = ticket.meta.id.as_str(); let title = sanitize_one_line(&ticket.meta.title, MAX_TITLE_CHARS); let state = ticket.meta.workflow_state.as_str(); let output_summary = sanitize_one_line(&output.summary, MAX_SUMMARY_CHARS); - let ref_path = event_ref_path(&ticket.meta.id, summary.tool_name.as_str()); - let raw_message = format!( - "Ticket event notice (weak; auto_run=false)\n\ - ticket: {ticket_id}\n\ - title: {title}\n\ - state: {state}\n\ - event: {event_kind}\n\ - summary: {output_summary}\n\ - ref: {ref_path}", - ticket_id = ticket.meta.id.as_str(), - ); + let ref_path = event_ref_path(ticket_id, summary.tool_name.as_str()); + let message = render_ticket_event_notice_message(TicketEventNoticeValues { + ticket_id, + title: &title, + state, + event_kind: &event_kind, + summary: &output_summary, + ref_path: &ref_path, + })?; Some(TicketEventNotice { - ticket_id: ticket.meta.id.as_str().to_string(), + ticket_id: ticket_id.to_string(), event_kind, - message: bound_chars(&raw_message, MAX_MESSAGE_CHARS), + message: bound_chars(&message, MAX_MESSAGE_CHARS), }) } +struct TicketEventNoticeValues<'a> { + ticket_id: &'a str, + title: &'a str, + state: &'a str, + event_kind: &'a str, + summary: &'a str, + ref_path: &'a str, +} + +fn render_ticket_event_notice_message(values: TicketEventNoticeValues<'_>) -> Option { + PromptCatalog::builtins_only() + .ok()? + .render(PodPrompt::TicketEventCompanionNotice, values.to_template()) + .ok() +} + +impl TicketEventNoticeValues<'_> { + fn to_template(&self) -> TemplateValue { + let mut values: BTreeMap<&'static str, TemplateValue> = BTreeMap::new(); + values.insert("ticket_id", TemplateValue::from(self.ticket_id)); + values.insert("title", TemplateValue::from(self.title)); + values.insert("state", TemplateValue::from(self.state)); + values.insert("event_kind", TemplateValue::from(self.event_kind)); + values.insert("summary", TemplateValue::from(self.summary)); + values.insert("ref_path", TemplateValue::from(self.ref_path)); + TemplateValue::from(values) + } +} + fn explicit_ticket_event_kind(tool_name: &str, content: &Value) -> Option { match tool_name { "TicketComment" => content @@ -230,6 +261,29 @@ mod tests { assert!(notice.message.contains("event: state/queued->inprogress")); assert!(notice.message.contains("ref: .yoi/tickets/")); assert!(notice.message.chars().count() <= MAX_MESSAGE_CHARS + 1); + + let expected = PromptCatalog::builtins_only() + .expect("load prompt catalog") + .render( + PodPrompt::TicketEventCompanionNotice, + TicketEventNoticeValues { + ticket_id: ¬ice.ticket_id, + title: &sanitize_one_line( + "A very long title that should be bounded but still identify the ticket precisely enough for Companion", + MAX_TITLE_CHARS, + ), + state: "planning", + event_kind: "state/queued->inprogress", + summary: &sanitize_one_line( + "Changed ticket state from queued to inprogress with a deliberately long summary that should be bounded before entering the weak notification payload and should not contain large logs", + MAX_SUMMARY_CHARS, + ), + ref_path: &format!(".yoi/tickets/{}/item.md", ticket_id), + } + .to_template(), + ) + .expect("render prompt resource"); + assert_eq!(notice.message, bound_chars(&expected, MAX_MESSAGE_CHARS)); } #[test] @@ -392,7 +446,6 @@ mod tests { .await; assert_eq!(action, HookPostToolAction::Continue); let message = rx.recv().await.unwrap(); - assert!(message.contains("Ticket event notice")); assert!(message.contains("event: state/queued->inprogress")); assert!(message.contains("title: Companion event hook")); companion.await.unwrap(); diff --git a/resources/prompts/internal.toml b/resources/prompts/internal.toml index 466d9587..05837bfc 100644 --- a/resources/prompts/internal.toml +++ b/resources/prompts/internal.toml @@ -68,6 +68,8 @@ The following workflows are advertised resident. When a user request matches one pod_orchestration_guidance_section = "{% include \"$yoi/common/pod-orchestration\" %}" +ticket_event_companion_notice = "{% include \"$yoi/pod/ticket_event_companion_notice\" %}" + spawn_pod_tool_description = """\ Spawn a new Pod process to work on a delegated task. The spawner's write scope is reduced by the scope passed here; the spawned Pod receives its own socket and starts running `task` immediately. The spawned Pod outlives the spawner's current turn and can be contacted again through its socket path. diff --git a/resources/prompts/pod/ticket_event_companion_notice.md b/resources/prompts/pod/ticket_event_companion_notice.md new file mode 100644 index 00000000..1551bdcb --- /dev/null +++ b/resources/prompts/pod/ticket_event_companion_notice.md @@ -0,0 +1,7 @@ +Ticket event notice (weak; auto_run=false) +ticket: {{ ticket_id }} +title: {{ title }} +state: {{ state }} +event: {{ event_kind }} +summary: {{ summary }} +ref: {{ ref_path }}