Merge commit '0496cd907bc7bb96e9aa1c6d385bedb616bf3233' into work/00001M10FJVA2-orchestrator-queue-notice

This commit is contained in:
2026-08-27 12:46:27 +09:00
8 changed files with 187 additions and 65 deletions
+52 -20
View File
@@ -576,6 +576,7 @@ pub(crate) enum IntakeRegistryUpdate {
pub(crate) struct ReadyTicketPlanningReturnRequest {
workspace_root: PathBuf,
ticket_id: String,
ticket_key: String,
user_instruction: String,
followup: ReadyTicketPlanningReturnFollowup,
}
@@ -2031,11 +2032,18 @@ impl DashboardApp {
return None;
};
let ticket_id = ticket.id.clone();
let ticket_key = match required_ticket_handoff_key(ticket.resource_key.as_deref()) {
Ok(ticket_key) => ticket_key.to_string(),
Err(error) => {
self.notice = Some(error);
return None;
}
};
let mut context =
TicketRoleLaunchContext::new(current_workspace_root(), TicketRole::Intake);
context.ticket = Some(TicketRef::id(ticket_id.clone()));
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 ShowTicket body/thread/artifacts before making routing or requirements decisions."
"Continue Intake for existing Ticket {ticket_key}. 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) {
Ok(store) => store,
@@ -2048,7 +2056,7 @@ impl DashboardApp {
Ok(Some(claim)) => {
let status = local_claim_status_for_pod(&claim.worker_name, &self.list);
self.notice = Some(existing_ticket_claim_notice(
&ticket_id,
&ticket_key,
&claim.worker_name,
status,
));
@@ -2076,7 +2084,7 @@ impl DashboardApp {
self.sending = true;
self.notice = Some(format!(
"Launching Ticket Intake for {} as {}",
ticket_id, planned.worker_name
ticket_key, planned.worker_name
));
Some(IntakeLaunchRequest {
context,
@@ -2147,10 +2155,17 @@ impl DashboardApp {
return None;
};
let ticket_id = ticket.id.clone();
let ticket_key = match required_ticket_handoff_key(ticket.resource_key.as_deref()) {
Ok(ticket_key) => ticket_key.to_string(),
Err(error) => {
self.notice = Some(error);
return None;
}
};
if ticket.workflow_state != TicketWorkflowState::Ready {
self.notice = Some(format!(
"Ticket {} is {}; expected ready before returning to planning.",
ticket_id,
ticket_key,
ticket.workflow_state.as_str()
));
return None;
@@ -2202,7 +2217,7 @@ impl DashboardApp {
TicketRoleLaunchContext::new(workspace_root.clone(), TicketRole::Intake);
context.ticket = Some(TicketRef::id(ticket_id.clone()));
context.user_instruction = Some(build_ready_ticket_refinement_launch_instruction(
&ticket_id,
&ticket_key,
&user_instruction,
));
let peer_registration = self.prepare_intake_peer_registration(&mut context);
@@ -2226,11 +2241,12 @@ impl DashboardApp {
self.sending = true;
self.notice = Some(format!(
"Returning ready Ticket {} to planning for refinement…",
ticket_id
ticket_key
));
Some(ReadyTicketPlanningReturnRequest {
workspace_root,
ticket_id,
ticket_key,
user_instruction,
followup,
})
@@ -3882,21 +3898,35 @@ fn bounded_refinement_instruction(input: &str) -> String {
.to_string()
}
fn build_ready_ticket_refinement_thread_body(ticket_id: &str, instruction: &str) -> String {
fn required_ticket_handoff_key(resource_key: Option<&str>) -> Result<&str, String> {
let resource_key = resource_key.ok_or_else(|| {
"Ticket handoff is unavailable because the canonical T-* resource key is missing. Refresh the panel and retry."
.to_string()
})?;
let sequence = resource_key.strip_prefix("T-").filter(|sequence| {
!sequence.is_empty() && sequence.bytes().all(|byte| byte.is_ascii_digit())
});
sequence.map(|_| resource_key).ok_or_else(|| {
"Ticket handoff is unavailable because the canonical T-* resource key is invalid. Refresh the panel and retry."
.to_string()
})
}
fn build_ready_ticket_refinement_thread_body(ticket_key: &str, instruction: &str) -> String {
format!(
"Panel returned ready Ticket {ticket_id} to planning for requirements sync. This is not Queue routing and must not start implementation.\n\n## User refinement instruction\n\n{instruction}\n"
"Panel returned ready Ticket {ticket_key} to planning for requirements sync. This is not Queue routing and must not start implementation.\n\n## User refinement instruction\n\n{instruction}\n"
)
}
fn build_ready_ticket_refinement_launch_instruction(ticket_id: &str, instruction: &str) -> String {
fn build_ready_ticket_refinement_launch_instruction(ticket_key: &str, instruction: &str) -> String {
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 ShowTicket 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_key}. 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}"
)
}
fn build_ready_ticket_refinement_notify(ticket_id: &str, instruction: &str) -> String {
fn build_ready_ticket_refinement_notify(ticket_key: &str, instruction: &str) -> String {
format!(
"Ticket {ticket_id} was returned from ready to planning from the Panel for requirements sync. Continue Intake/refinement only; do not Queue or route implementation. Read the Ticket thread for the recorded state change and user instruction.\n\nUser refinement instruction:\n\n{instruction}"
"Ticket {ticket_key} was returned from ready to planning from the Panel for requirements sync. Continue Intake/refinement only; do not Queue or route implementation. Read the Ticket thread for the recorded state change and user instruction.\n\nUser refinement instruction:\n\n{instruction}"
)
}
@@ -3925,10 +3955,12 @@ async fn dispatch_ready_ticket_planning_return(
let ticket = backend
.show(id.clone())
.map_err(|error| TicketActionError::Ticket(error.to_string()))?;
let ticket_key =
required_ticket_handoff_key(Some(&request.ticket_key)).map_err(TicketActionError::Stale)?;
if ticket.meta.workflow_state != TicketWorkflowState::Ready {
return Err(TicketActionError::Stale(format!(
"Ticket {} is {}; expected ready before returning it to planning. Refresh the panel and retry if appropriate.",
ticket.meta.id,
ticket_key,
ticket.meta.workflow_state.as_str()
)));
}
@@ -3937,7 +3969,7 @@ async fn dispatch_ready_ticket_planning_return(
TicketWorkflowState::Planning.as_str(),
"panel_return_to_planning",
MarkdownText::from(build_ready_ticket_refinement_thread_body(
&ticket.meta.id,
ticket_key,
&request.user_instruction,
)),
);
@@ -3951,7 +3983,7 @@ async fn dispatch_ready_ticket_planning_return(
ReadyTicketPlanningReturnOutcome {
notice: format!(
"Ticket {} returned to planning for refinement; launching Ticket Intake…",
ticket.meta.id
ticket_key
),
followup: ReadyTicketPlanningReturnAfterMutation::LaunchIntake(request),
}
@@ -3961,19 +3993,19 @@ async fn dispatch_ready_ticket_planning_return(
socket_path,
} => {
let message =
build_ready_ticket_refinement_notify(&ticket.meta.id, &request.user_instruction);
build_ready_ticket_refinement_notify(ticket_key, &request.user_instruction);
match send_notify_only(&socket_path, message, true).await {
Ok(()) => ReadyTicketPlanningReturnOutcome {
notice: format!(
"Ticket {} returned to planning for refinement; notified live Intake Worker {}.",
ticket.meta.id, worker_name
ticket_key, worker_name
),
followup: ReadyTicketPlanningReturnAfterMutation::None,
},
Err(error) => ReadyTicketPlanningReturnOutcome {
notice: bounded_panel_diagnostic(format!(
"Ticket {} returned to planning and instruction was recorded, but notifying Intake Worker {} failed: {}",
ticket.meta.id, worker_name, error
ticket_key, worker_name, error
)),
followup: ReadyTicketPlanningReturnAfterMutation::None,
},
@@ -3984,7 +4016,7 @@ async fn dispatch_ready_ticket_planning_return(
ReadyTicketPlanningReturnOutcome {
notice: format!(
"Ticket {} returned to planning for refinement; opening/restoring claimed Intake Worker {}…",
ticket.meta.id, worker_name
ticket_key, worker_name
),
followup: ReadyTicketPlanningReturnAfterMutation::OpenClaim(request),
}
@@ -3993,7 +4025,7 @@ async fn dispatch_ready_ticket_planning_return(
ReadyTicketPlanningReturnOutcome {
notice: bounded_panel_diagnostic(format!(
"Ticket {} returned to planning and instruction was recorded, but Intake launch was not attempted because existing Intake claim {} is stale; inspect or clear the local claim before launching another Intake Worker.",
ticket.meta.id, worker_name
ticket_key, worker_name
)),
followup: ReadyTicketPlanningReturnAfterMutation::None,
}
+24
View File
@@ -390,6 +390,7 @@ fn planning_return_request(
ReadyTicketPlanningReturnRequest {
workspace_root: temp.path().to_path_buf(),
ticket_id,
ticket_key: "T-482".to_string(),
user_instruction: instruction.to_string(),
followup: ReadyTicketPlanningReturnFollowup::BlockedByStaleClaim {
worker_name: "stale-intake".to_string(),
@@ -494,6 +495,7 @@ fn ready_ticket_intake_enter_prepares_planning_return_not_queue_or_generic_launc
};
assert_eq!(request.ticket_id, "20260608-000123-ready");
assert_eq!(request.ticket_key, "T-1");
assert_eq!(request.user_instruction, "clarify expected behavior");
assert!(matches!(
request.followup,
@@ -515,6 +517,7 @@ async fn planning_return_with_launch_followup_changes_state_before_launch_follow
let request = ReadyTicketPlanningReturnRequest {
workspace_root: temp.path().to_path_buf(),
ticket_id: ticket_id.clone(),
ticket_key: "T-482".to_string(),
user_instruction: "launch intake after state change".to_string(),
followup: ReadyTicketPlanningReturnFollowup::LaunchIntake(IntakeLaunchRequest {
context: TicketRoleLaunchContext::new(temp.path().to_path_buf(), TicketRole::Intake),
@@ -3504,6 +3507,27 @@ fn ticket_action_error_records_f2_diagnostic_details() {
assert!(!app.panel_diagnostic_open);
}
#[test]
fn ready_ticket_refinement_projection_uses_only_canonical_resource_key() {
const INTERNAL_ID: &str = "00001KZVNXFNK";
let thread = build_ready_ticket_refinement_thread_body("T-482", "Clarify rollback.");
let launch = build_ready_ticket_refinement_launch_instruction("T-482", "Clarify rollback.");
let notify = build_ready_ticket_refinement_notify("T-482", "Clarify rollback.");
for projection in [&thread, &launch, &notify] {
assert!(projection.contains("T-482"));
assert!(!projection.contains(INTERNAL_ID));
}
}
#[test]
fn ticket_handoff_fails_closed_without_canonical_resource_key() {
assert_eq!(required_ticket_handoff_key(Some("T-482")), Ok("T-482"));
for invalid in [None, Some(""), Some("00001KZVNXFNK"), Some("T-key")] {
assert!(required_ticket_handoff_key(invalid).is_err());
}
}
fn plain_line(line: &Line<'_>) -> String {
line.spans
.iter()