ticket: remove action attention fields
This commit is contained in:
parent
41133e0cd5
commit
3afdd894d8
|
|
@ -41,7 +41,7 @@ Intake は以下を行う。
|
||||||
- 作成または refinement する Ticket が、実装・レビュー・検証・完了判断を単独で行える concrete work item であるか確認する。
|
- 作成または refinement する Ticket が、実装・レビュー・検証・完了判断を単独で行える concrete work item であるか確認する。
|
||||||
- 広い依頼を分割する場合は、進捗コンテナとしての umbrella Ticket ではなく、concrete Ticket / Objective context / split decision record に責務を分ける。
|
- 広い依頼を分割する場合は、進捗コンテナとしての umbrella Ticket ではなく、concrete Ticket / Objective context / split decision record に責務を分ける。
|
||||||
- Objective-to-Ticket links を提案する場合は canonical opaque Ticket ID だけを使い、dependency / blocking / ordering relation として扱わない。
|
- 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 として提案しない。
|
- canonical ID は Ticket 作成/storage が opaque な path-derived value として割り当てるため、Intake はユーザー向け metadata として提案しない。
|
||||||
- background / requirements / acceptance criteria / escalation conditions を整理する。
|
- background / requirements / acceptance criteria / escalation conditions を整理する。
|
||||||
- binding decisions / invariants と implementation latitude を分けて書く。
|
- binding decisions / invariants と implementation latitude を分けて書く。
|
||||||
|
|
@ -229,7 +229,7 @@ canonical ID は作成時に storage が opaque/path-derived value として割
|
||||||
新規 Ticket の場合:
|
新規 Ticket の場合:
|
||||||
|
|
||||||
- `TicketCreate` を使う。
|
- `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 で明記する。
|
- body に readiness / open questions / risk flags と、binding decisions / invariants、implementation latitude、escalation conditions を Markdown で明記する。
|
||||||
|
|
||||||
既存 Ticket refinement の場合:
|
既存 Ticket refinement の場合:
|
||||||
|
|
|
||||||
|
|
@ -214,7 +214,7 @@ Action:
|
||||||
- `TicketComment` に review target と確認観点を記録する。
|
- `TicketComment` に review target と確認観点を記録する。
|
||||||
- blocker 未解決のまま merge-ready としない。
|
- blocker 未解決のまま merge-ready としない。
|
||||||
|
|
||||||
### `blocked_action_required`
|
### `blocked_by_dependency_or_missing_authority`
|
||||||
|
|
||||||
人間判断または外部イベント待ち。
|
人間判断または外部イベント待ち。
|
||||||
|
|
||||||
|
|
@ -229,7 +229,7 @@ Action:
|
||||||
|
|
||||||
- 必要な判断・外部 action を短く書く。
|
- 必要な判断・外部 action を短く書く。
|
||||||
- `TicketComment` に blocked reason と next question を記録する。
|
- `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`
|
### `close_ready`
|
||||||
|
|
||||||
|
|
@ -393,7 +393,7 @@ IntentPacket が短く書けない場合、`implementation_ready` ではなく `
|
||||||
- `spike_needed` → read-only investigation plan / Pod(許可後)
|
- `spike_needed` → read-only investigation plan / Pod(許可後)
|
||||||
- `implementation_ready` → `multi-agent-workflow`
|
- `implementation_ready` → `multi-agent-workflow`
|
||||||
- `review_needed` → reviewer Pod / review 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
|
- `close_ready` → close workflow / maintainer decision
|
||||||
|
|
||||||
## 完了条件
|
## 完了条件
|
||||||
|
|
@ -410,7 +410,7 @@ IntentPacket が短く書けない場合、`implementation_ready` ではなく `
|
||||||
|
|
||||||
- unattended scheduler。
|
- unattended scheduler。
|
||||||
- LeaseStore / queue persistence。
|
- LeaseStore / queue persistence。
|
||||||
- action-required dashboard UI。
|
- queue/dashboard UI。
|
||||||
- automatic Pod spawning policy。
|
- automatic Pod spawning policy。
|
||||||
- TicketUpdate tool の導入。
|
- TicketUpdate tool の導入。
|
||||||
- external tracker integration。
|
- external tracker integration。
|
||||||
|
|
|
||||||
|
|
@ -481,9 +481,7 @@ pub struct NewTicket {
|
||||||
pub assignee: Option<String>,
|
pub assignee: Option<String>,
|
||||||
pub readiness: Option<String>,
|
pub readiness: Option<String>,
|
||||||
pub risk_flags: Vec<String>,
|
pub risk_flags: Vec<String>,
|
||||||
pub action_required: Option<String>,
|
|
||||||
pub workflow_state: Option<TicketWorkflowState>,
|
pub workflow_state: Option<TicketWorkflowState>,
|
||||||
pub attention_required: Option<String>,
|
|
||||||
pub queued_by: Option<String>,
|
pub queued_by: Option<String>,
|
||||||
pub queued_at: Option<String>,
|
pub queued_at: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
@ -501,9 +499,7 @@ impl NewTicket {
|
||||||
assignee: None,
|
assignee: None,
|
||||||
readiness: None,
|
readiness: None,
|
||||||
risk_flags: Vec::new(),
|
risk_flags: Vec::new(),
|
||||||
action_required: None,
|
|
||||||
workflow_state: None,
|
workflow_state: None,
|
||||||
attention_required: None,
|
|
||||||
queued_by: None,
|
queued_by: None,
|
||||||
queued_at: None,
|
queued_at: None,
|
||||||
}
|
}
|
||||||
|
|
@ -744,10 +740,8 @@ pub struct TicketMeta {
|
||||||
pub assignee: Option<String>,
|
pub assignee: Option<String>,
|
||||||
pub readiness: Option<String>,
|
pub readiness: Option<String>,
|
||||||
pub risk_flags: Vec<String>,
|
pub risk_flags: Vec<String>,
|
||||||
pub action_required: Option<String>,
|
|
||||||
pub workflow_state: TicketWorkflowState,
|
pub workflow_state: TicketWorkflowState,
|
||||||
pub workflow_state_explicit: bool,
|
pub workflow_state_explicit: bool,
|
||||||
pub attention_required: Option<String>,
|
|
||||||
pub queued_by: Option<String>,
|
pub queued_by: Option<String>,
|
||||||
pub queued_at: Option<String>,
|
pub queued_at: Option<String>,
|
||||||
pub raw: BTreeMap<String, String>,
|
pub raw: BTreeMap<String, String>,
|
||||||
|
|
@ -763,10 +757,8 @@ pub struct TicketSummary {
|
||||||
pub priority: String,
|
pub priority: String,
|
||||||
pub labels: Vec<String>,
|
pub labels: Vec<String>,
|
||||||
pub readiness: Option<String>,
|
pub readiness: Option<String>,
|
||||||
pub action_required: Option<String>,
|
|
||||||
pub workflow_state: TicketWorkflowState,
|
pub workflow_state: TicketWorkflowState,
|
||||||
pub workflow_state_explicit: bool,
|
pub workflow_state_explicit: bool,
|
||||||
pub attention_required: Option<String>,
|
|
||||||
pub queued_by: Option<String>,
|
pub queued_by: Option<String>,
|
||||||
pub queued_at: Option<String>,
|
pub queued_at: Option<String>,
|
||||||
pub updated_at: Option<String>,
|
pub updated_at: Option<String>,
|
||||||
|
|
@ -1290,10 +1282,8 @@ impl TicketBackend for LocalTicketBackend {
|
||||||
priority: meta.priority,
|
priority: meta.priority,
|
||||||
labels: meta.labels,
|
labels: meta.labels,
|
||||||
readiness: meta.readiness,
|
readiness: meta.readiness,
|
||||||
action_required: meta.action_required,
|
|
||||||
workflow_state: meta.workflow_state,
|
workflow_state: meta.workflow_state,
|
||||||
workflow_state_explicit: meta.workflow_state_explicit,
|
workflow_state_explicit: meta.workflow_state_explicit,
|
||||||
attention_required: meta.attention_required,
|
|
||||||
queued_by: meta.queued_by,
|
queued_by: meta.queued_by,
|
||||||
queued_at: meta.queued_at,
|
queued_at: meta.queued_at,
|
||||||
updated_at: meta.updated_at,
|
updated_at: meta.updated_at,
|
||||||
|
|
@ -1377,18 +1367,6 @@ impl TicketBackend for LocalTicketBackend {
|
||||||
if !input.risk_flags.is_empty() {
|
if !input.risk_flags.is_empty() {
|
||||||
fields.push(("risk_flags".to_string(), labels_yaml(&input.risk_flags)));
|
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 {
|
if let Some(queued_by) = input.queued_by {
|
||||||
fields.push((
|
fields.push((
|
||||||
"queued_by".to_string(),
|
"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() {
|
if parsed.frontmatter.get(obsolete).is_some() {
|
||||||
report.push_error(
|
report.push_error(
|
||||||
format!(
|
format!(
|
||||||
|
|
@ -1994,12 +1981,10 @@ struct TicketItemFrontmatter {
|
||||||
assignee: Option<String>,
|
assignee: Option<String>,
|
||||||
readiness: Option<String>,
|
readiness: Option<String>,
|
||||||
risk_flags: Vec<String>,
|
risk_flags: Vec<String>,
|
||||||
action_required: Option<String>,
|
|
||||||
workflow_state: Option<TicketWorkflowState>,
|
workflow_state: Option<TicketWorkflowState>,
|
||||||
workflow_state_explicit: bool,
|
workflow_state_explicit: bool,
|
||||||
state: Option<TicketWorkflowState>,
|
state: Option<TicketWorkflowState>,
|
||||||
state_explicit: bool,
|
state_explicit: bool,
|
||||||
attention_required: Option<String>,
|
|
||||||
queued_by: Option<String>,
|
queued_by: Option<String>,
|
||||||
queued_at: Option<String>,
|
queued_at: Option<String>,
|
||||||
raw: BTreeMap<String, String>,
|
raw: BTreeMap<String, String>,
|
||||||
|
|
@ -2103,12 +2088,10 @@ fn parse_ticket_frontmatter(content: &str) -> std::result::Result<TicketItemFron
|
||||||
assignee: yaml_string(&mapping, "assignee")?,
|
assignee: yaml_string(&mapping, "assignee")?,
|
||||||
readiness: yaml_string(&mapping, "readiness")?,
|
readiness: yaml_string(&mapping, "readiness")?,
|
||||||
risk_flags: yaml_string_list(&mapping, "risk_flags")?,
|
risk_flags: yaml_string_list(&mapping, "risk_flags")?,
|
||||||
action_required: yaml_string(&mapping, "action_required")?,
|
|
||||||
workflow_state,
|
workflow_state,
|
||||||
workflow_state_explicit,
|
workflow_state_explicit,
|
||||||
state,
|
state,
|
||||||
state_explicit,
|
state_explicit,
|
||||||
attention_required: yaml_string(&mapping, "attention_required")?,
|
|
||||||
queued_by: yaml_string(&mapping, "queued_by")?,
|
queued_by: yaml_string(&mapping, "queued_by")?,
|
||||||
queued_at: yaml_string(&mapping, "queued_at")?,
|
queued_at: yaml_string(&mapping, "queued_at")?,
|
||||||
raw,
|
raw,
|
||||||
|
|
@ -2233,10 +2216,8 @@ fn ticket_meta(frontmatter: TicketItemFrontmatter, id: String) -> TicketMeta {
|
||||||
assignee: frontmatter.assignee,
|
assignee: frontmatter.assignee,
|
||||||
readiness: frontmatter.readiness,
|
readiness: frontmatter.readiness,
|
||||||
risk_flags: frontmatter.risk_flags,
|
risk_flags: frontmatter.risk_flags,
|
||||||
action_required: frontmatter.action_required,
|
|
||||||
workflow_state,
|
workflow_state,
|
||||||
workflow_state_explicit: frontmatter.state_explicit,
|
workflow_state_explicit: frontmatter.state_explicit,
|
||||||
attention_required: frontmatter.attention_required,
|
|
||||||
queued_by: frontmatter.queued_by,
|
queued_by: frontmatter.queued_by,
|
||||||
queued_at: frontmatter.queued_at,
|
queued_at: frontmatter.queued_at,
|
||||||
raw: frontmatter.raw,
|
raw: frontmatter.raw,
|
||||||
|
|
@ -3548,8 +3529,6 @@ updated_at: 2026-06-05T00:00:00Z
|
||||||
assignee: null
|
assignee: null
|
||||||
readiness: implementation-ready
|
readiness: implementation-ready
|
||||||
risk_flags: [low, local]
|
risk_flags: [low, local]
|
||||||
action_required: none
|
|
||||||
attention_required: none
|
|
||||||
queued_by: workspace-panel
|
queued_by: workspace-panel
|
||||||
queued_at: 2026-06-05T00:01:00Z
|
queued_at: 2026-06-05T00:01:00Z
|
||||||
---
|
---
|
||||||
|
|
@ -3563,10 +3542,8 @@ queued_at: 2026-06-05T00:01:00Z
|
||||||
assert!(meta.labels.is_empty());
|
assert!(meta.labels.is_empty());
|
||||||
assert_eq!(meta.readiness.as_deref(), Some("implementation-ready"));
|
assert_eq!(meta.readiness.as_deref(), Some("implementation-ready"));
|
||||||
assert_eq!(meta.risk_flags, vec!["low", "local"]);
|
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_eq!(meta.workflow_state, TicketWorkflowState::Ready);
|
||||||
assert!(meta.workflow_state_explicit);
|
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_by.as_deref(), Some("workspace-panel"));
|
||||||
assert_eq!(meta.queued_at.as_deref(), Some("2026-06-05T00:01:00Z"));
|
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(
|
let frontmatter = parse_ticket_frontmatter(
|
||||||
r#"risk_flags: [low, local]
|
r#"risk_flags: [low, local]
|
||||||
assignee: ~
|
assignee: ~
|
||||||
attention_required: null
|
|
||||||
action_required: "null"
|
|
||||||
readiness: "~"
|
readiness: "~"
|
||||||
state: planning
|
state: planning
|
||||||
"#,
|
"#,
|
||||||
|
|
@ -3587,8 +3562,6 @@ state: planning
|
||||||
assert!(meta.labels.is_empty());
|
assert!(meta.labels.is_empty());
|
||||||
assert_eq!(meta.risk_flags, vec!["low", "local"]);
|
assert_eq!(meta.risk_flags, vec!["low", "local"]);
|
||||||
assert_eq!(meta.assignee, None);
|
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.readiness.as_deref(), Some("~"));
|
||||||
assert_eq!(meta.workflow_state, TicketWorkflowState::Planning);
|
assert_eq!(meta.workflow_state, TicketWorkflowState::Planning);
|
||||||
assert!(meta.workflow_state_explicit);
|
assert!(meta.workflow_state_explicit);
|
||||||
|
|
@ -3641,6 +3614,8 @@ state: planning
|
||||||
"workflow_state:",
|
"workflow_state:",
|
||||||
"kind:",
|
"kind:",
|
||||||
"labels:",
|
"labels:",
|
||||||
|
"action_required:",
|
||||||
|
"attention_required:",
|
||||||
] {
|
] {
|
||||||
assert!(
|
assert!(
|
||||||
!item.contains(obsolete),
|
!item.contains(obsolete),
|
||||||
|
|
@ -3680,8 +3655,6 @@ state: planning
|
||||||
let mut input = NewTicket::new("123");
|
let mut input = NewTicket::new("123");
|
||||||
input.risk_flags = vec!["1".into(), "42".into()];
|
input.risk_flags = vec!["1".into(), "42".into()];
|
||||||
input.assignee = Some("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 ticket = backend.create(input).unwrap();
|
||||||
|
|
||||||
let record = backend.show(TicketIdOrSlug::Id(ticket.id.clone())).unwrap();
|
let record = backend.show(TicketIdOrSlug::Id(ticket.id.clone())).unwrap();
|
||||||
|
|
@ -3689,8 +3662,6 @@ state: planning
|
||||||
assert!(record.meta.labels.is_empty());
|
assert!(record.meta.labels.is_empty());
|
||||||
assert_eq!(record.meta.risk_flags, vec!["1", "42"]);
|
assert_eq!(record.meta.risk_flags, vec!["1", "42"]);
|
||||||
assert_eq!(record.meta.assignee.as_deref(), Some("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"))
|
let item = fs::read_to_string(tmp.path().join("tickets").join(&ticket.id).join("item.md"))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
@ -3698,8 +3669,8 @@ state: planning
|
||||||
assert!(!item.contains("labels:"), "{item}");
|
assert!(!item.contains("labels:"), "{item}");
|
||||||
assert!(item.contains("risk_flags: ['1', '42']"), "{item}");
|
assert!(item.contains("risk_flags: ['1', '42']"), "{item}");
|
||||||
assert!(item.contains("assignee: '42'"), "{item}");
|
assert!(item.contains("assignee: '42'"), "{item}");
|
||||||
assert!(item.contains("attention_required: '0'"), "{item}");
|
assert!(!item.contains("attention_required:"), "{item}");
|
||||||
assert!(item.contains("action_required: 'true'"), "{item}");
|
assert!(!item.contains("action_required:"), "{item}");
|
||||||
|
|
||||||
let report = backend.doctor().unwrap();
|
let report = backend.doctor().unwrap();
|
||||||
assert!(report.is_ok(), "{:?}", report.diagnostics);
|
assert!(report.is_ok(), "{:?}", report.diagnostics);
|
||||||
|
|
@ -4118,7 +4089,7 @@ state: planning
|
||||||
fs::create_dir_all(root.join("20260609-000000-001/artifacts")).unwrap();
|
fs::create_dir_all(root.join("20260609-000000-001/artifacts")).unwrap();
|
||||||
fs::write(
|
fs::write(
|
||||||
root.join("20260609-000000-001/item.md"),
|
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();
|
.unwrap();
|
||||||
fs::write(
|
fs::write(
|
||||||
|
|
@ -4141,6 +4112,8 @@ state: planning
|
||||||
assert!(messages.contains("obsolete current frontmatter field 'workflow_state'"));
|
assert!(messages.contains("obsolete current frontmatter field 'workflow_state'"));
|
||||||
assert!(messages.contains("obsolete current frontmatter field 'kind'"));
|
assert!(messages.contains("obsolete current frontmatter field 'kind'"));
|
||||||
assert!(messages.contains("obsolete current frontmatter field 'labels'"));
|
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"));
|
assert!(messages.contains("review event missing valid status"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -125,15 +125,9 @@ struct TicketCreateParams {
|
||||||
/// Optional risk flag frontmatter values.
|
/// Optional risk flag frontmatter values.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
risk_flags: Vec<String>,
|
risk_flags: Vec<String>,
|
||||||
/// Optional action-required frontmatter value.
|
|
||||||
#[serde(default)]
|
|
||||||
action_required: Option<String>,
|
|
||||||
/// Optional state frontmatter value. Defaults to `planning`.
|
/// Optional state frontmatter value. Defaults to `planning`.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
state: Option<TicketWorkflowStateParam>,
|
state: Option<TicketWorkflowStateParam>,
|
||||||
/// Optional attention_required overlay frontmatter value.
|
|
||||||
#[serde(default)]
|
|
||||||
attention_required: Option<String>,
|
|
||||||
/// Optional queued_by frontmatter value.
|
/// Optional queued_by frontmatter value.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
queued_by: Option<String>,
|
queued_by: Option<String>,
|
||||||
|
|
@ -590,9 +584,7 @@ impl Tool for TicketCreateTool {
|
||||||
input.assignee = params.assignee;
|
input.assignee = params.assignee;
|
||||||
input.readiness = params.readiness;
|
input.readiness = params.readiness;
|
||||||
input.risk_flags = params.risk_flags;
|
input.risk_flags = params.risk_flags;
|
||||||
input.action_required = params.action_required;
|
|
||||||
input.workflow_state = params.state.map(TicketWorkflowStateParam::into_state);
|
input.workflow_state = params.state.map(TicketWorkflowStateParam::into_state);
|
||||||
input.attention_required = params.attention_required;
|
|
||||||
input.queued_by = params.queued_by;
|
input.queued_by = params.queued_by;
|
||||||
input.queued_at = params.queued_at;
|
input.queued_at = params.queued_at;
|
||||||
|
|
||||||
|
|
@ -1051,18 +1043,6 @@ fn ticket_summary_json(ticket: TicketSummary) -> TicketListTicketOutput {
|
||||||
|
|
||||||
fn ticket_list_hints(ticket: &TicketSummary) -> Vec<String> {
|
fn ticket_list_hints(ticket: &TicketSummary) -> Vec<String> {
|
||||||
let mut hints = Vec::new();
|
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() {
|
if let Some(readiness) = ticket.readiness.as_deref() {
|
||||||
hints.push(format!(
|
hints.push(format!(
|
||||||
"readiness:{}",
|
"readiness:{}",
|
||||||
|
|
@ -1183,8 +1163,6 @@ fn ticket_json(
|
||||||
"assignee": ticket.meta.assignee,
|
"assignee": ticket.meta.assignee,
|
||||||
"readiness": ticket.meta.readiness,
|
"readiness": ticket.meta.readiness,
|
||||||
"risk_flags": ticket.meta.risk_flags,
|
"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_by": ticket.meta.queued_by,
|
||||||
"queued_at": ticket.meta.queued_at,
|
"queued_at": ticket.meta.queued_at,
|
||||||
},
|
},
|
||||||
|
|
@ -1486,6 +1464,8 @@ mod tests {
|
||||||
let created_text = created_json.to_string();
|
let created_text = created_json.to_string();
|
||||||
assert!(!created_text.contains("legacy_ticket"));
|
assert!(!created_text.contains("legacy_ticket"));
|
||||||
assert!(!created_text.contains("needs_preflight"));
|
assert!(!created_text.contains("needs_preflight"));
|
||||||
|
assert!(!created_text.contains("action_required"));
|
||||||
|
assert!(!created_text.contains("attention_required"));
|
||||||
|
|
||||||
let listed = list
|
let listed = list
|
||||||
.execute(
|
.execute(
|
||||||
|
|
@ -1512,6 +1492,8 @@ mod tests {
|
||||||
assert!(shown_content.contains("Created by tool"));
|
assert!(shown_content.contains("Created by tool"));
|
||||||
assert!(!shown_content.contains("legacy_ticket"));
|
assert!(!shown_content.contains("legacy_ticket"));
|
||||||
assert!(!shown_content.contains("needs_preflight"));
|
assert!(!shown_content.contains("needs_preflight"));
|
||||||
|
assert!(!shown_content.contains("action_required"));
|
||||||
|
assert!(!shown_content.contains("attention_required"));
|
||||||
|
|
||||||
let report = doctor
|
let report = doctor
|
||||||
.execute(&json!({}).to_string(), Default::default())
|
.execute(&json!({}).to_string(), Default::default())
|
||||||
|
|
@ -1529,8 +1511,8 @@ mod tests {
|
||||||
"Long Title {}",
|
"Long Title {}",
|
||||||
"x".repeat(LIST_TITLE_MAX_CHARS + 40)
|
"x".repeat(LIST_TITLE_MAX_CHARS + 40)
|
||||||
));
|
));
|
||||||
ticket.attention_required = Some(format!(
|
ticket.readiness = Some(format!(
|
||||||
"Needs attention {}",
|
"Ready after review {}",
|
||||||
"a".repeat(LIST_HINT_MAX_CHARS + 40)
|
"a".repeat(LIST_HINT_MAX_CHARS + 40)
|
||||||
));
|
));
|
||||||
backend.create(ticket).unwrap();
|
backend.create(ticket).unwrap();
|
||||||
|
|
@ -1544,7 +1526,7 @@ mod tests {
|
||||||
assert!(title.chars().count() <= LIST_TITLE_MAX_CHARS);
|
assert!(title.chars().count() <= LIST_TITLE_MAX_CHARS);
|
||||||
assert!(title.ends_with("..."));
|
assert!(title.ends_with("..."));
|
||||||
let hint = listed_json["tickets"][0]["hints"][0].as_str().unwrap();
|
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("..."));
|
assert!(hint.ends_with("..."));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2214,6 +2196,8 @@ mod tests {
|
||||||
.to_string();
|
.to_string();
|
||||||
assert!(!create_schema.contains("legacy_ticket"));
|
assert!(!create_schema.contains("legacy_ticket"));
|
||||||
assert!(!create_schema.contains("needs_preflight"));
|
assert!(!create_schema.contains("needs_preflight"));
|
||||||
|
assert!(!create_schema.contains("action_required"));
|
||||||
|
assert!(!create_schema.contains("attention_required"));
|
||||||
let plan_record_schema = tools
|
let plan_record_schema = tools
|
||||||
.iter()
|
.iter()
|
||||||
.map(|definition| definition().0)
|
.map(|definition| definition().0)
|
||||||
|
|
|
||||||
|
|
@ -2109,18 +2109,6 @@ fn panel_close_blocker(ticket: &ticket::Ticket) -> Option<String> {
|
||||||
ticket.meta.workflow_state.as_str()
|
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() {
|
if ticket.resolution.is_some() {
|
||||||
return Some(format!(
|
return Some(format!(
|
||||||
"Close blocked for Ticket {ticket_id}: resolution.md already exists; no close was recorded."
|
"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<String> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
fn non_empty_ticket_field(value: Option<&str>) -> Option<&str> {
|
|
||||||
value.map(str::trim).filter(|value| !value.is_empty())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn panel_close_resolution(
|
fn panel_close_resolution(
|
||||||
ticket: &ticket::Ticket,
|
ticket: &ticket::Ticket,
|
||||||
record_language: Option<&str>,
|
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]
|
#[tokio::test]
|
||||||
async fn ticket_close_action_blocks_existing_resolution_without_moving_ticket() {
|
async fn ticket_close_action_blocks_existing_resolution_without_moving_ticket() {
|
||||||
let (temp, ticket_id, backend) = done_ticket_workspace("panel-close-resolution");
|
let (temp, ticket_id, backend) = done_ticket_workspace("panel-close-resolution");
|
||||||
|
|
@ -3489,8 +3431,8 @@ mod tests {
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let backend = LocalTicketBackend::new(temp.path().join(".yoi/tickets"));
|
let backend = LocalTicketBackend::new(temp.path().join(".yoi/tickets"));
|
||||||
let mut ticket = NewTicket::new("Needs Human Reply");
|
let mut ticket = NewTicket::new("Ready Ticket");
|
||||||
ticket.action_required = Some("answer intake question".to_string());
|
ticket.workflow_state = Some(TicketWorkflowState::Ready);
|
||||||
backend.create(ticket).unwrap();
|
backend.create(ticket).unwrap();
|
||||||
let list = PodList::from_sources(
|
let list = PodList::from_sources(
|
||||||
PodVisibilitySource::ResumePicker,
|
PodVisibilitySource::ResumePicker,
|
||||||
|
|
@ -3502,7 +3444,7 @@ mod tests {
|
||||||
let panel = build_workspace_panel(temp.path(), &list);
|
let panel = build_workspace_panel(temp.path(), &list);
|
||||||
let mut app = app_with_panel(list, panel);
|
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);
|
assert_eq!(app.selected_open_eligibility(), OpenEligibility::Disabled);
|
||||||
let lines = list_lines(&app, 100, 6)
|
let lines = list_lines(&app, 100, 6)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
|
@ -3510,7 +3452,7 @@ mod tests {
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
let ticket_line = lines
|
let ticket_line = lines
|
||||||
.iter()
|
.iter()
|
||||||
.position(|line| line.contains("Needs Human Reply"))
|
.position(|line| line.contains("Ready Ticket"))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let pod_line = lines.iter().position(|line| line.contains("idle")).unwrap();
|
let pod_line = lines.iter().position(|line| line.contains("idle")).unwrap();
|
||||||
assert!(ticket_line < pod_line);
|
assert!(ticket_line < pod_line);
|
||||||
|
|
@ -4885,7 +4827,6 @@ mod tests {
|
||||||
workflow_state: TicketWorkflowState::parse(state)
|
workflow_state: TicketWorkflowState::parse(state)
|
||||||
.unwrap_or(TicketWorkflowState::Planning),
|
.unwrap_or(TicketWorkflowState::Planning),
|
||||||
workflow_state_explicit: true,
|
workflow_state_explicit: true,
|
||||||
attention_required: None,
|
|
||||||
next_action: Some(next_action),
|
next_action: Some(next_action),
|
||||||
updated_at: None,
|
updated_at: None,
|
||||||
latest_event_kind: Some("implementation_report".to_string()),
|
latest_event_kind: Some("implementation_report".to_string()),
|
||||||
|
|
|
||||||
|
|
@ -233,7 +233,6 @@ pub(crate) struct TicketPanelEntry {
|
||||||
pub(crate) priority: String,
|
pub(crate) priority: String,
|
||||||
pub(crate) workflow_state: TicketWorkflowState,
|
pub(crate) workflow_state: TicketWorkflowState,
|
||||||
pub(crate) workflow_state_explicit: bool,
|
pub(crate) workflow_state_explicit: bool,
|
||||||
pub(crate) attention_required: Option<String>,
|
|
||||||
pub(crate) next_action: Option<NextUserAction>,
|
pub(crate) next_action: Option<NextUserAction>,
|
||||||
pub(crate) updated_at: Option<String>,
|
pub(crate) updated_at: Option<String>,
|
||||||
pub(crate) latest_event_kind: Option<String>,
|
pub(crate) latest_event_kind: Option<String>,
|
||||||
|
|
@ -620,10 +619,8 @@ fn ticket_summary_from_meta(meta: &TicketMeta) -> TicketSummary {
|
||||||
priority: meta.priority.clone(),
|
priority: meta.priority.clone(),
|
||||||
labels: meta.labels.clone(),
|
labels: meta.labels.clone(),
|
||||||
readiness: meta.readiness.clone(),
|
readiness: meta.readiness.clone(),
|
||||||
action_required: meta.action_required.clone(),
|
|
||||||
workflow_state: meta.workflow_state,
|
workflow_state: meta.workflow_state,
|
||||||
workflow_state_explicit: meta.workflow_state_explicit,
|
workflow_state_explicit: meta.workflow_state_explicit,
|
||||||
attention_required: meta.attention_required.clone(),
|
|
||||||
queued_by: meta.queued_by.clone(),
|
queued_by: meta.queued_by.clone(),
|
||||||
queued_at: meta.queued_at.clone(),
|
queued_at: meta.queued_at.clone(),
|
||||||
updated_at: meta.updated_at.clone(),
|
updated_at: meta.updated_at.clone(),
|
||||||
|
|
@ -669,7 +666,6 @@ fn ticket_row(
|
||||||
priority: summary.priority.clone(),
|
priority: summary.priority.clone(),
|
||||||
workflow_state: summary.workflow_state,
|
workflow_state: summary.workflow_state,
|
||||||
workflow_state_explicit: summary.workflow_state_explicit,
|
workflow_state_explicit: summary.workflow_state_explicit,
|
||||||
attention_required: summary.attention_required.clone(),
|
|
||||||
next_action: derived.action,
|
next_action: derived.action,
|
||||||
updated_at: summary.updated_at.clone(),
|
updated_at: summary.updated_at.clone(),
|
||||||
latest_event_kind: latest_event.map(|event| event.kind.as_str().to_string()),
|
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 {
|
match summary.workflow_state {
|
||||||
TicketWorkflowState::Ready => DerivedTicketState {
|
TicketWorkflowState::Ready => DerivedTicketState {
|
||||||
kind: PanelRowKind::Ticket,
|
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<String> {
|
fn ticket_subtitle(entry: &TicketPanelEntry) -> Option<String> {
|
||||||
let mut parts = vec![format!("{} · {}", entry.id, entry.workflow_state.as_str())];
|
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() {
|
if let Some(claim) = entry.local_claim.as_ref() {
|
||||||
parts.push(format!(
|
parts.push(format!(
|
||||||
"claim: {} ({})",
|
"claim: {} ({})",
|
||||||
|
|
@ -1044,9 +1017,7 @@ mod tests {
|
||||||
fn workspace_panel_without_ticket_config_is_pod_only() {
|
fn workspace_panel_without_ticket_config_is_pod_only() {
|
||||||
let temp = TempDir::new().unwrap();
|
let temp = TempDir::new().unwrap();
|
||||||
let backend = LocalTicketBackend::new(temp.path().join(".yoi/tickets"));
|
let backend = LocalTicketBackend::new(temp.path().join(".yoi/tickets"));
|
||||||
create_ticket(&backend, "Hidden Without Config", |input| {
|
create_ticket(&backend, "Hidden Without Config", |_| {});
|
||||||
input.action_required = Some("answer me".to_string());
|
|
||||||
});
|
|
||||||
|
|
||||||
let model = build_workspace_panel(temp.path(), &live_pods(&["idle"]));
|
let model = build_workspace_panel(temp.path(), &live_pods(&["idle"]));
|
||||||
|
|
||||||
|
|
@ -1068,37 +1039,22 @@ mod tests {
|
||||||
create_ticket(&backend, "Ready Ticket", |input| {
|
create_ticket(&backend, "Ready Ticket", |input| {
|
||||||
input.workflow_state = Some(TicketWorkflowState::Ready);
|
input.workflow_state = Some(TicketWorkflowState::Ready);
|
||||||
});
|
});
|
||||||
create_ticket(&backend, "Needs User", |input| {
|
create_ticket(&backend, "Planning Ticket", |_| {});
|
||||||
input.workflow_state = Some(TicketWorkflowState::Ready);
|
|
||||||
input.attention_required = Some("answer clarification".to_string());
|
|
||||||
});
|
|
||||||
|
|
||||||
let model = build_workspace_panel(temp.path(), &empty_pods());
|
let model = build_workspace_panel(temp.path(), &empty_pods());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
model.composer.available_targets,
|
model.composer.available_targets,
|
||||||
vec![ComposerTarget::Companion, ComposerTarget::TicketIntake]
|
vec![ComposerTarget::Companion, ComposerTarget::TicketIntake]
|
||||||
);
|
);
|
||||||
let rows = model
|
let row = model
|
||||||
.rows
|
.rows
|
||||||
.iter()
|
.iter()
|
||||||
.map(|row| {
|
.find(|row| row.title == "Ready Ticket")
|
||||||
(
|
.unwrap();
|
||||||
row.title.as_str(),
|
|
||||||
row.status.as_str(),
|
|
||||||
row.priority,
|
|
||||||
row.next_action,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
assert_eq!(rows[0].0, "Needs User");
|
assert_eq!(row.status, "ready");
|
||||||
assert_eq!(rows[0].1, "ready");
|
assert_eq!(row.priority, ActionPriority::ReadyForQueue);
|
||||||
assert_eq!(rows[0].2, ActionPriority::UserReply);
|
assert_eq!(row.next_action, Some(NextUserAction::Queue));
|
||||||
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));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[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]
|
#[test]
|
||||||
fn workspace_panel_defaults_missing_open_state_to_planning_and_displays_done_state() {
|
fn workspace_panel_defaults_missing_open_state_to_planning_and_displays_done_state() {
|
||||||
let temp = TempDir::new().unwrap();
|
let temp = TempDir::new().unwrap();
|
||||||
|
|
|
||||||
|
|
@ -988,18 +988,6 @@ fn parse_list_limit(value: &str) -> Result<usize, TicketCliError> {
|
||||||
|
|
||||||
fn ticket_cli_hints(ticket: &TicketSummary) -> String {
|
fn ticket_cli_hints(ticket: &TicketSummary) -> String {
|
||||||
let mut hints = Vec::new();
|
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() {
|
if let Some(readiness) = ticket.readiness.as_deref() {
|
||||||
hints.push(format!(
|
hints.push(format!(
|
||||||
"readiness:{}",
|
"readiness:{}",
|
||||||
|
|
|
||||||
|
|
@ -208,7 +208,7 @@ Routing classifications include:
|
||||||
- `spike_needed`
|
- `spike_needed`
|
||||||
- `implementation_ready`
|
- `implementation_ready`
|
||||||
- `review_needed`
|
- `review_needed`
|
||||||
- `blocked_action_required`
|
- `blocked_by_dependency_or_missing_authority`
|
||||||
- `close_ready`
|
- `close_ready`
|
||||||
- `closed_or_noop`
|
- `closed_or_noop`
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user