Merge commit '0496cd907bc7bb96e9aa1c6d385bedb616bf3233' into work/00001M10FJVA2-orchestrator-queue-notice
This commit is contained in:
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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, ¬ify] {
|
||||
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()
|
||||
|
||||
Reference in New Issue
Block a user