From 3afdd894d83190476c63132b115d5e7ea059c6f1 Mon Sep 17 00:00:00 2001 From: Hare Date: Tue, 9 Jun 2026 21:05:48 +0900 Subject: [PATCH] ticket: remove action attention fields --- .yoi/workflow/ticket-intake-workflow.md | 4 +- .yoi/workflow/ticket-orchestrator-routing.md | 8 +- crates/ticket/src/lib.rs | 61 ++++-------- crates/ticket/src/tool.rs | 34 ++----- crates/tui/src/multi_pod.rs | 67 +------------ crates/tui/src/workspace_panel.rs | 99 ++------------------ crates/yoi/src/ticket_cli.rs | 12 --- docs/development/work-items.md | 2 +- 8 files changed, 45 insertions(+), 242 deletions(-) diff --git a/.yoi/workflow/ticket-intake-workflow.md b/.yoi/workflow/ticket-intake-workflow.md index d729b3e4..108d8b75 100644 --- a/.yoi/workflow/ticket-intake-workflow.md +++ b/.yoi/workflow/ticket-intake-workflow.md @@ -41,7 +41,7 @@ Intake は以下を行う。 - 作成または refinement する Ticket が、実装・レビュー・検証・完了判断を単独で行える concrete work item であるか確認する。 - 広い依頼を分割する場合は、進捗コンテナとしての umbrella Ticket ではなく、concrete Ticket / Objective context / split decision record に責務を分ける。 - Objective-to-Ticket links を提案する場合は canonical opaque Ticket ID だけを使い、dependency / blocking / ordering relation として扱わない。 -- Ticket の title / body/request snapshot / acceptance criteria / priority / readiness / action_required / attention_required を、現在の要件として意味がある範囲で提案する。 +- Ticket の title / body/request snapshot / acceptance criteria / priority / readiness / risk flags を、現在の要件として意味がある範囲で提案する。 - canonical ID は Ticket 作成/storage が opaque な path-derived value として割り当てるため、Intake はユーザー向け metadata として提案しない。 - background / requirements / acceptance criteria / escalation conditions を整理する。 - binding decisions / invariants と implementation latitude を分けて書く。 @@ -229,7 +229,7 @@ canonical ID は作成時に storage が opaque/path-derived value として割 新規 Ticket の場合: - `TicketCreate` を使う。 -- title / priority / body と、必要な readiness / action_required / attention_required を指定する。canonical ID は storage が割り当てる。 +- title / priority / body と、必要な readiness / risk flags を指定する。canonical ID は storage が割り当てる。 - body に readiness / open questions / risk flags と、binding decisions / invariants、implementation latitude、escalation conditions を Markdown で明記する。 既存 Ticket refinement の場合: diff --git a/.yoi/workflow/ticket-orchestrator-routing.md b/.yoi/workflow/ticket-orchestrator-routing.md index ce364521..e48e2bf4 100644 --- a/.yoi/workflow/ticket-orchestrator-routing.md +++ b/.yoi/workflow/ticket-orchestrator-routing.md @@ -214,7 +214,7 @@ Action: - `TicketComment` に review target と確認観点を記録する。 - blocker 未解決のまま merge-ready としない。 -### `blocked_action_required` +### `blocked_by_dependency_or_missing_authority` 人間判断または外部イベント待ち。 @@ -229,7 +229,7 @@ Action: - 必要な判断・外部 action を短く書く。 - `TicketComment` に blocked reason と next question を記録する。 -- 必要に応じて attention / action-required frontmatter や orchestration plan の blocker/waiting-capacity 記録で、待ち理由を current state とは別に表す。lifecycle 外の storage bucket へ移す route は使わない。 +- 必要に応じて typed relation metadata や orchestration plan の blocker/waiting-capacity 記録で、待ち理由を current state とは別に表す。lifecycle 外の storage bucket へ移す route は使わない。 ### `close_ready` @@ -393,7 +393,7 @@ IntentPacket が短く書けない場合、`implementation_ready` ではなく ` - `spike_needed` → read-only investigation plan / Pod(許可後) - `implementation_ready` → `multi-agent-workflow` - `review_needed` → reviewer Pod / review workflow -- `blocked_action_required` → human / parent Orchestrator +- concrete blocker / missing decision → human / parent Orchestrator - `close_ready` → close workflow / maintainer decision ## 完了条件 @@ -410,7 +410,7 @@ IntentPacket が短く書けない場合、`implementation_ready` ではなく ` - unattended scheduler。 - LeaseStore / queue persistence。 -- action-required dashboard UI。 +- queue/dashboard UI。 - automatic Pod spawning policy。 - TicketUpdate tool の導入。 - external tracker integration。 diff --git a/crates/ticket/src/lib.rs b/crates/ticket/src/lib.rs index 09ed35dc..7334d7a7 100644 --- a/crates/ticket/src/lib.rs +++ b/crates/ticket/src/lib.rs @@ -481,9 +481,7 @@ pub struct NewTicket { pub assignee: Option, pub readiness: Option, pub risk_flags: Vec, - pub action_required: Option, pub workflow_state: Option, - pub attention_required: Option, pub queued_by: Option, pub queued_at: Option, } @@ -501,9 +499,7 @@ impl NewTicket { assignee: None, readiness: None, risk_flags: Vec::new(), - action_required: None, workflow_state: None, - attention_required: None, queued_by: None, queued_at: None, } @@ -744,10 +740,8 @@ pub struct TicketMeta { pub assignee: Option, pub readiness: Option, pub risk_flags: Vec, - pub action_required: Option, pub workflow_state: TicketWorkflowState, pub workflow_state_explicit: bool, - pub attention_required: Option, pub queued_by: Option, pub queued_at: Option, pub raw: BTreeMap, @@ -763,10 +757,8 @@ pub struct TicketSummary { pub priority: String, pub labels: Vec, pub readiness: Option, - pub action_required: Option, pub workflow_state: TicketWorkflowState, pub workflow_state_explicit: bool, - pub attention_required: Option, pub queued_by: Option, pub queued_at: Option, pub updated_at: Option, @@ -1290,10 +1282,8 @@ impl TicketBackend for LocalTicketBackend { priority: meta.priority, labels: meta.labels, readiness: meta.readiness, - action_required: meta.action_required, workflow_state: meta.workflow_state, workflow_state_explicit: meta.workflow_state_explicit, - attention_required: meta.attention_required, queued_by: meta.queued_by, queued_at: meta.queued_at, updated_at: meta.updated_at, @@ -1377,18 +1367,6 @@ impl TicketBackend for LocalTicketBackend { if !input.risk_flags.is_empty() { fields.push(("risk_flags".to_string(), labels_yaml(&input.risk_flags))); } - if let Some(action_required) = input.action_required { - fields.push(( - "action_required".to_string(), - format_yaml_string_scalar(action_required.as_str()), - )); - } - if let Some(attention_required) = input.attention_required { - fields.push(( - "attention_required".to_string(), - format_yaml_string_scalar(attention_required.as_str()), - )); - } if let Some(queued_by) = input.queued_by { fields.push(( "queued_by".to_string(), @@ -1874,7 +1852,16 @@ impl TicketBackend for LocalTicketBackend { ); } } - for obsolete in ["id", "slug", "status", "workflow_state", "kind", "labels"] { + for obsolete in [ + "id", + "slug", + "status", + "workflow_state", + "kind", + "labels", + "action_required", + "attention_required", + ] { if parsed.frontmatter.get(obsolete).is_some() { report.push_error( format!( @@ -1994,12 +1981,10 @@ struct TicketItemFrontmatter { assignee: Option, readiness: Option, risk_flags: Vec, - action_required: Option, workflow_state: Option, workflow_state_explicit: bool, state: Option, state_explicit: bool, - attention_required: Option, queued_by: Option, queued_at: Option, raw: BTreeMap, @@ -2103,12 +2088,10 @@ fn parse_ticket_frontmatter(content: &str) -> std::result::Result TicketMeta { assignee: frontmatter.assignee, readiness: frontmatter.readiness, risk_flags: frontmatter.risk_flags, - action_required: frontmatter.action_required, workflow_state, workflow_state_explicit: frontmatter.state_explicit, - attention_required: frontmatter.attention_required, queued_by: frontmatter.queued_by, queued_at: frontmatter.queued_at, raw: frontmatter.raw, @@ -3548,8 +3529,6 @@ updated_at: 2026-06-05T00:00:00Z assignee: null readiness: implementation-ready risk_flags: [low, local] -action_required: none -attention_required: none queued_by: workspace-panel queued_at: 2026-06-05T00:01:00Z --- @@ -3563,10 +3542,8 @@ queued_at: 2026-06-05T00:01:00Z assert!(meta.labels.is_empty()); assert_eq!(meta.readiness.as_deref(), Some("implementation-ready")); assert_eq!(meta.risk_flags, vec!["low", "local"]); - assert_eq!(meta.action_required.as_deref(), Some("none")); assert_eq!(meta.workflow_state, TicketWorkflowState::Ready); assert!(meta.workflow_state_explicit); - assert_eq!(meta.attention_required.as_deref(), Some("none")); assert_eq!(meta.queued_by.as_deref(), Some("workspace-panel")); assert_eq!(meta.queued_at.as_deref(), Some("2026-06-05T00:01:00Z")); } @@ -3576,8 +3553,6 @@ queued_at: 2026-06-05T00:01:00Z let frontmatter = parse_ticket_frontmatter( r#"risk_flags: [low, local] assignee: ~ -attention_required: null -action_required: "null" readiness: "~" state: planning "#, @@ -3587,8 +3562,6 @@ state: planning assert!(meta.labels.is_empty()); assert_eq!(meta.risk_flags, vec!["low", "local"]); assert_eq!(meta.assignee, None); - assert_eq!(meta.attention_required, None); - assert_eq!(meta.action_required.as_deref(), Some("null")); assert_eq!(meta.readiness.as_deref(), Some("~")); assert_eq!(meta.workflow_state, TicketWorkflowState::Planning); assert!(meta.workflow_state_explicit); @@ -3641,6 +3614,8 @@ state: planning "workflow_state:", "kind:", "labels:", + "action_required:", + "attention_required:", ] { assert!( !item.contains(obsolete), @@ -3680,8 +3655,6 @@ state: planning let mut input = NewTicket::new("123"); input.risk_flags = vec!["1".into(), "42".into()]; input.assignee = Some("42".into()); - input.attention_required = Some("0".into()); - input.action_required = Some("true".into()); let ticket = backend.create(input).unwrap(); let record = backend.show(TicketIdOrSlug::Id(ticket.id.clone())).unwrap(); @@ -3689,8 +3662,6 @@ state: planning assert!(record.meta.labels.is_empty()); assert_eq!(record.meta.risk_flags, vec!["1", "42"]); assert_eq!(record.meta.assignee.as_deref(), Some("42")); - assert_eq!(record.meta.attention_required.as_deref(), Some("0")); - assert_eq!(record.meta.action_required.as_deref(), Some("true")); let item = fs::read_to_string(tmp.path().join("tickets").join(&ticket.id).join("item.md")) .unwrap(); @@ -3698,8 +3669,8 @@ state: planning assert!(!item.contains("labels:"), "{item}"); assert!(item.contains("risk_flags: ['1', '42']"), "{item}"); assert!(item.contains("assignee: '42'"), "{item}"); - assert!(item.contains("attention_required: '0'"), "{item}"); - assert!(item.contains("action_required: 'true'"), "{item}"); + assert!(!item.contains("attention_required:"), "{item}"); + assert!(!item.contains("action_required:"), "{item}"); let report = backend.doctor().unwrap(); assert!(report.is_ok(), "{:?}", report.diagnostics); @@ -4118,7 +4089,7 @@ state: planning fs::create_dir_all(root.join("20260609-000000-001/artifacts")).unwrap(); fs::write( root.join("20260609-000000-001/item.md"), - "---\nid: old\nslug: old\ntitle: Bad\nstatus: pending\nworkflow_state: ready\nkind: task\nlabels: []\ncreated_at: x\nupdated_at: x\n---\n", + "---\nid: old\nslug: old\ntitle: Bad\nstatus: pending\nworkflow_state: ready\nkind: task\nlabels: []\naction_required: human\nattention_required: true\ncreated_at: x\nupdated_at: x\n---\n", ) .unwrap(); fs::write( @@ -4141,6 +4112,8 @@ state: planning assert!(messages.contains("obsolete current frontmatter field 'workflow_state'")); assert!(messages.contains("obsolete current frontmatter field 'kind'")); assert!(messages.contains("obsolete current frontmatter field 'labels'")); + assert!(messages.contains("obsolete current frontmatter field 'action_required'")); + assert!(messages.contains("obsolete current frontmatter field 'attention_required'")); assert!(messages.contains("review event missing valid status")); } diff --git a/crates/ticket/src/tool.rs b/crates/ticket/src/tool.rs index e05c638b..a3d80035 100644 --- a/crates/ticket/src/tool.rs +++ b/crates/ticket/src/tool.rs @@ -125,15 +125,9 @@ struct TicketCreateParams { /// Optional risk flag frontmatter values. #[serde(default)] risk_flags: Vec, - /// Optional action-required frontmatter value. - #[serde(default)] - action_required: Option, /// Optional state frontmatter value. Defaults to `planning`. #[serde(default)] state: Option, - /// Optional attention_required overlay frontmatter value. - #[serde(default)] - attention_required: Option, /// Optional queued_by frontmatter value. #[serde(default)] queued_by: Option, @@ -590,9 +584,7 @@ impl Tool for TicketCreateTool { input.assignee = params.assignee; input.readiness = params.readiness; input.risk_flags = params.risk_flags; - input.action_required = params.action_required; input.workflow_state = params.state.map(TicketWorkflowStateParam::into_state); - input.attention_required = params.attention_required; input.queued_by = params.queued_by; input.queued_at = params.queued_at; @@ -1051,18 +1043,6 @@ fn ticket_summary_json(ticket: TicketSummary) -> TicketListTicketOutput { fn ticket_list_hints(ticket: &TicketSummary) -> Vec { let mut hints = Vec::new(); - if let Some(attention) = ticket.attention_required.as_deref() { - hints.push(format!( - "attention:{}", - truncate_inline(attention, LIST_HINT_MAX_CHARS) - )); - } - if let Some(action) = ticket.action_required.as_deref() { - hints.push(format!( - "action:{}", - truncate_inline(action, LIST_HINT_MAX_CHARS) - )); - } if let Some(readiness) = ticket.readiness.as_deref() { hints.push(format!( "readiness:{}", @@ -1183,8 +1163,6 @@ fn ticket_json( "assignee": ticket.meta.assignee, "readiness": ticket.meta.readiness, "risk_flags": ticket.meta.risk_flags, - "action_required": ticket.meta.action_required, - "attention_required": ticket.meta.attention_required, "queued_by": ticket.meta.queued_by, "queued_at": ticket.meta.queued_at, }, @@ -1486,6 +1464,8 @@ mod tests { let created_text = created_json.to_string(); assert!(!created_text.contains("legacy_ticket")); assert!(!created_text.contains("needs_preflight")); + assert!(!created_text.contains("action_required")); + assert!(!created_text.contains("attention_required")); let listed = list .execute( @@ -1512,6 +1492,8 @@ mod tests { assert!(shown_content.contains("Created by tool")); assert!(!shown_content.contains("legacy_ticket")); assert!(!shown_content.contains("needs_preflight")); + assert!(!shown_content.contains("action_required")); + assert!(!shown_content.contains("attention_required")); let report = doctor .execute(&json!({}).to_string(), Default::default()) @@ -1529,8 +1511,8 @@ mod tests { "Long Title {}", "x".repeat(LIST_TITLE_MAX_CHARS + 40) )); - ticket.attention_required = Some(format!( - "Needs attention {}", + ticket.readiness = Some(format!( + "Ready after review {}", "a".repeat(LIST_HINT_MAX_CHARS + 40) )); backend.create(ticket).unwrap(); @@ -1544,7 +1526,7 @@ mod tests { assert!(title.chars().count() <= LIST_TITLE_MAX_CHARS); assert!(title.ends_with("...")); let hint = listed_json["tickets"][0]["hints"][0].as_str().unwrap(); - assert!(hint.chars().count() <= "attention:".chars().count() + LIST_HINT_MAX_CHARS); + assert!(hint.chars().count() <= "readiness:".chars().count() + LIST_HINT_MAX_CHARS); assert!(hint.ends_with("...")); } @@ -2214,6 +2196,8 @@ mod tests { .to_string(); assert!(!create_schema.contains("legacy_ticket")); assert!(!create_schema.contains("needs_preflight")); + assert!(!create_schema.contains("action_required")); + assert!(!create_schema.contains("attention_required")); let plan_record_schema = tools .iter() .map(|definition| definition().0) diff --git a/crates/tui/src/multi_pod.rs b/crates/tui/src/multi_pod.rs index 121c9703..db857b56 100644 --- a/crates/tui/src/multi_pod.rs +++ b/crates/tui/src/multi_pod.rs @@ -2109,18 +2109,6 @@ fn panel_close_blocker(ticket: &ticket::Ticket) -> Option { ticket.meta.workflow_state.as_str() )); } - if let Some(reason) = non_empty_ticket_field(ticket.meta.attention_required.as_deref()) { - return Some(format!( - "Close blocked for Ticket {ticket_id}: attention_required is set ({}); no close was recorded.", - bounded_panel_diagnostic(reason) - )); - } - if let Some(reason) = non_empty_ticket_field(ticket.meta.action_required.as_deref()) { - return Some(format!( - "Close blocked for Ticket {ticket_id}: action_required is set ({}); no close was recorded.", - bounded_panel_diagnostic(reason) - )); - } if ticket.resolution.is_some() { return Some(format!( "Close blocked for Ticket {ticket_id}: resolution.md already exists; no close was recorded." @@ -2129,10 +2117,6 @@ fn panel_close_blocker(ticket: &ticket::Ticket) -> Option { None } -fn non_empty_ticket_field(value: Option<&str>) -> Option<&str> { - value.map(str::trim).filter(|value| !value.is_empty()) -} - fn panel_close_resolution( ticket: &ticket::Ticket, record_language: Option<&str>, @@ -3295,48 +3279,6 @@ mod tests { })); } - #[tokio::test] - async fn ticket_close_action_blocks_action_required_without_mutation() { - let (temp, ticket_id, backend) = ticket_workspace( - "panel-close-action-required", - TicketWorkflowState::Done, - |input| { - input.action_required = Some("human decision needed".to_string()); - }, - ); - - let error = - dispatch_ticket_action(request_for(&temp, ticket_id.clone(), NextUserAction::Close)) - .await - .unwrap_err(); - - assert!(error.to_string().contains("action_required is set")); - assert!(error.to_string().contains("no close was recorded")); - let ticket = backend.show(TicketIdOrSlug::Id(ticket_id)).unwrap(); - assert_eq!(ticket.meta.workflow_state, TicketWorkflowState::Done); - assert!(ticket.resolution.is_none()); - } - - #[tokio::test] - async fn ticket_close_action_blocks_attention_required_without_mutation() { - let (temp, ticket_id, backend) = ticket_workspace( - "panel-close-attention-required", - TicketWorkflowState::Done, - |input| { - input.attention_required = Some("needs reply".to_string()); - }, - ); - - let error = - dispatch_ticket_action(request_for(&temp, ticket_id.clone(), NextUserAction::Close)) - .await - .unwrap_err(); - - assert!(error.to_string().contains("attention_required is set")); - let ticket = backend.show(TicketIdOrSlug::Id(ticket_id)).unwrap(); - assert!(ticket.resolution.is_none()); - } - #[tokio::test] async fn ticket_close_action_blocks_existing_resolution_without_moving_ticket() { let (temp, ticket_id, backend) = done_ticket_workspace("panel-close-resolution"); @@ -3489,8 +3431,8 @@ mod tests { ) .unwrap(); let backend = LocalTicketBackend::new(temp.path().join(".yoi/tickets")); - let mut ticket = NewTicket::new("Needs Human Reply"); - ticket.action_required = Some("answer intake question".to_string()); + let mut ticket = NewTicket::new("Ready Ticket"); + ticket.workflow_state = Some(TicketWorkflowState::Ready); backend.create(ticket).unwrap(); let list = PodList::from_sources( PodVisibilitySource::ResumePicker, @@ -3502,7 +3444,7 @@ mod tests { let panel = build_workspace_panel(temp.path(), &list); let mut app = app_with_panel(list, panel); - assert_eq!(app.selected_panel_row().unwrap().title, "Needs Human Reply"); + assert_eq!(app.selected_panel_row().unwrap().title, "Ready Ticket"); assert_eq!(app.selected_open_eligibility(), OpenEligibility::Disabled); let lines = list_lines(&app, 100, 6) .into_iter() @@ -3510,7 +3452,7 @@ mod tests { .collect::>(); let ticket_line = lines .iter() - .position(|line| line.contains("Needs Human Reply")) + .position(|line| line.contains("Ready Ticket")) .unwrap(); let pod_line = lines.iter().position(|line| line.contains("idle")).unwrap(); assert!(ticket_line < pod_line); @@ -4885,7 +4827,6 @@ mod tests { workflow_state: TicketWorkflowState::parse(state) .unwrap_or(TicketWorkflowState::Planning), workflow_state_explicit: true, - attention_required: None, next_action: Some(next_action), updated_at: None, latest_event_kind: Some("implementation_report".to_string()), diff --git a/crates/tui/src/workspace_panel.rs b/crates/tui/src/workspace_panel.rs index df2daf2d..80fc3172 100644 --- a/crates/tui/src/workspace_panel.rs +++ b/crates/tui/src/workspace_panel.rs @@ -233,7 +233,6 @@ pub(crate) struct TicketPanelEntry { pub(crate) priority: String, pub(crate) workflow_state: TicketWorkflowState, pub(crate) workflow_state_explicit: bool, - pub(crate) attention_required: Option, pub(crate) next_action: Option, pub(crate) updated_at: Option, pub(crate) latest_event_kind: Option, @@ -620,10 +619,8 @@ fn ticket_summary_from_meta(meta: &TicketMeta) -> TicketSummary { priority: meta.priority.clone(), labels: meta.labels.clone(), readiness: meta.readiness.clone(), - action_required: meta.action_required.clone(), workflow_state: meta.workflow_state, workflow_state_explicit: meta.workflow_state_explicit, - attention_required: meta.attention_required.clone(), queued_by: meta.queued_by.clone(), queued_at: meta.queued_at.clone(), updated_at: meta.updated_at.clone(), @@ -669,7 +666,6 @@ fn ticket_row( priority: summary.priority.clone(), workflow_state: summary.workflow_state, workflow_state_explicit: summary.workflow_state_explicit, - attention_required: summary.attention_required.clone(), next_action: derived.action, updated_at: summary.updated_at.clone(), latest_event_kind: latest_event.map(|event| event.kind.as_str().to_string()), @@ -735,26 +731,6 @@ fn derive_ticket_state( }; } - if let Some(reason) = summary - .attention_required - .as_deref() - .map(str::trim) - .filter(|value| !value.is_empty()) - { - return DerivedTicketState { - kind: PanelRowKind::Blocked, - priority: ActionPriority::UserReply, - action: Some(NextUserAction::Edit), - disabled_reason: Some( - "attention_required is set; resolve it before queueing or routing.".to_string(), - ), - key_hint: Some( - "Resolve attention_required in the Ticket frontmatter/thread".to_string(), - ), - blocked_reason: Some(reason.to_string()), - }; - } - match summary.workflow_state { TicketWorkflowState::Ready => DerivedTicketState { kind: PanelRowKind::Ticket, @@ -870,9 +846,6 @@ pub(crate) fn local_claim_status_for_pod(pod_name: &str, pods: &PodList) -> Tick fn ticket_subtitle(entry: &TicketPanelEntry) -> Option { let mut parts = vec![format!("{} · {}", entry.id, entry.workflow_state.as_str())]; - if let Some(reason) = entry.attention_required.as_deref() { - parts.push(format!("attention: {reason}")); - } if let Some(claim) = entry.local_claim.as_ref() { parts.push(format!( "claim: {} ({})", @@ -1044,9 +1017,7 @@ mod tests { fn workspace_panel_without_ticket_config_is_pod_only() { let temp = TempDir::new().unwrap(); let backend = LocalTicketBackend::new(temp.path().join(".yoi/tickets")); - create_ticket(&backend, "Hidden Without Config", |input| { - input.action_required = Some("answer me".to_string()); - }); + create_ticket(&backend, "Hidden Without Config", |_| {}); let model = build_workspace_panel(temp.path(), &live_pods(&["idle"])); @@ -1068,37 +1039,22 @@ mod tests { create_ticket(&backend, "Ready Ticket", |input| { input.workflow_state = Some(TicketWorkflowState::Ready); }); - create_ticket(&backend, "Needs User", |input| { - input.workflow_state = Some(TicketWorkflowState::Ready); - input.attention_required = Some("answer clarification".to_string()); - }); + create_ticket(&backend, "Planning Ticket", |_| {}); let model = build_workspace_panel(temp.path(), &empty_pods()); assert_eq!( model.composer.available_targets, vec![ComposerTarget::Companion, ComposerTarget::TicketIntake] ); - let rows = model + let row = model .rows .iter() - .map(|row| { - ( - row.title.as_str(), - row.status.as_str(), - row.priority, - row.next_action, - ) - }) - .collect::>(); + .find(|row| row.title == "Ready Ticket") + .unwrap(); - assert_eq!(rows[0].0, "Needs User"); - assert_eq!(rows[0].1, "ready"); - assert_eq!(rows[0].2, ActionPriority::UserReply); - assert_eq!(rows[0].3, Some(NextUserAction::Edit)); - assert_eq!(rows[1].0, "Ready Ticket"); - assert_eq!(rows[1].1, "ready"); - assert_eq!(rows[1].2, ActionPriority::ReadyForQueue); - assert_eq!(rows[1].3, Some(NextUserAction::Queue)); + assert_eq!(row.status, "ready"); + assert_eq!(row.priority, ActionPriority::ReadyForQueue); + assert_eq!(row.next_action, Some(NextUserAction::Queue)); } #[test] @@ -1198,45 +1154,6 @@ mod tests { ); } - #[test] - fn workspace_panel_treats_yaml_null_attention_required_as_unblocked_planning() { - let temp = TempDir::new().unwrap(); - write_ticket_config(temp.path()); - let backend = LocalTicketBackend::new(temp.path().join(".yoi/tickets")); - let ticket_ref = backend - .create({ - let mut input = NewTicket::new("Null Attention Planning"); - input.workflow_state = Some(TicketWorkflowState::Planning); - input - }) - .unwrap(); - let item_path = temp - .path() - .join(".yoi/tickets") - .join(&ticket_ref.id) - .join("item.md"); - let item = fs::read_to_string(&item_path).unwrap(); - fs::write( - &item_path, - item.replace( - "state: planning\ncreated_at:", - "state: planning\nattention_required: null\ncreated_at:", - ), - ) - .unwrap(); - - let model = build_workspace_panel(temp.path(), &empty_pods()); - let row = model - .rows - .iter() - .find(|row| row.title == "Null Attention Planning") - .unwrap(); - - assert_eq!(row.status, "planning"); - assert_eq!(row.next_action, Some(NextUserAction::Clarify)); - assert_eq!(row.priority, ActionPriority::Background); - } - #[test] fn workspace_panel_defaults_missing_open_state_to_planning_and_displays_done_state() { let temp = TempDir::new().unwrap(); diff --git a/crates/yoi/src/ticket_cli.rs b/crates/yoi/src/ticket_cli.rs index 9f65efb6..f6245135 100644 --- a/crates/yoi/src/ticket_cli.rs +++ b/crates/yoi/src/ticket_cli.rs @@ -988,18 +988,6 @@ fn parse_list_limit(value: &str) -> Result { fn ticket_cli_hints(ticket: &TicketSummary) -> String { let mut hints = Vec::new(); - if let Some(attention) = ticket.attention_required.as_deref() { - hints.push(format!( - "attention:{}", - truncate_inline(attention, LIST_HINT_MAX_CHARS) - )); - } - if let Some(action) = ticket.action_required.as_deref() { - hints.push(format!( - "action:{}", - truncate_inline(action, LIST_HINT_MAX_CHARS) - )); - } if let Some(readiness) = ticket.readiness.as_deref() { hints.push(format!( "readiness:{}", diff --git a/docs/development/work-items.md b/docs/development/work-items.md index ced58796..53bc146a 100644 --- a/docs/development/work-items.md +++ b/docs/development/work-items.md @@ -208,7 +208,7 @@ Routing classifications include: - `spike_needed` - `implementation_ready` - `review_needed` -- `blocked_action_required` +- `blocked_by_dependency_or_missing_authority` - `close_ready` - `closed_or_noop`