From 8ce3045794cbdf4f588f26bebb5e5c714789f107 Mon Sep 17 00:00:00 2001 From: Yvette Carlisle Date: Tue, 19 May 2026 00:00:37 +0800 Subject: [PATCH] {"schema":"decodex/commit/1","summary":"Show running lane authors","authority":"manual"} --- .../src/agent/tracker_tool_bridge/tests.rs | 1 + apps/decodex/src/archive_hygiene.rs | 1 + apps/decodex/src/manual.rs | 1 + .../src/orchestrator/operator_dashboard.html | 18 ++++++- apps/decodex/src/orchestrator/status.rs | 25 ++++++++++ apps/decodex/src/orchestrator/tests.rs | 1 + .../tests/operator/status/dashboard.rs | 2 + .../tests/operator/status/running_lanes.rs | 3 ++ .../tests/operator/status_support.rs | 4 ++ apps/decodex/src/orchestrator/types.rs | 3 ++ apps/decodex/src/tracker.rs | 1 + apps/decodex/src/tracker/linear.rs | 47 ++++++++++++++++++- 12 files changed, 105 insertions(+), 2 deletions(-) diff --git a/apps/decodex/src/agent/tracker_tool_bridge/tests.rs b/apps/decodex/src/agent/tracker_tool_bridge/tests.rs index 47c62a7..fcbcf03 100644 --- a/apps/decodex/src/agent/tracker_tool_bridge/tests.rs +++ b/apps/decodex/src/agent/tracker_tool_bridge/tests.rs @@ -305,6 +305,7 @@ fn sample_issue() -> TrackerIssue { #[cfg(test)] project_slug: Some(String::from("decodex")), title: String::from("Sample"), + author: None, description: String::from("Body"), priority: Some(3), created_at: String::from("2026-03-13T04:16:17.133Z"), diff --git a/apps/decodex/src/archive_hygiene.rs b/apps/decodex/src/archive_hygiene.rs index 46e9631..dc4af93 100644 --- a/apps/decodex/src/archive_hygiene.rs +++ b/apps/decodex/src/archive_hygiene.rs @@ -520,6 +520,7 @@ repo_root = "." #[cfg(test)] project_slug: Some(String::from("decodex")), title: format!("Issue {identifier}"), + author: None, description: String::new(), priority: None, created_at: String::from("2026-02-01T00:00:00Z"), diff --git a/apps/decodex/src/manual.rs b/apps/decodex/src/manual.rs index 9f9212c..725ec7a 100644 --- a/apps/decodex/src/manual.rs +++ b/apps/decodex/src/manual.rs @@ -3058,6 +3058,7 @@ exit 1\n", #[cfg(test)] project_slug: None, title: String::from("Sample issue"), + author: None, description: String::from(""), priority: None, created_at: String::from("2026-04-13T00:00:00Z"), diff --git a/apps/decodex/src/orchestrator/operator_dashboard.html b/apps/decodex/src/orchestrator/operator_dashboard.html index be5ef68..b4afe03 100644 --- a/apps/decodex/src/orchestrator/operator_dashboard.html +++ b/apps/decodex/src/orchestrator/operator_dashboard.html @@ -5615,6 +5615,16 @@

Run History

return facts.map(([label, value]) => renderRunMetaFact(label, value)).join(""); } + function runAuthor(run) { + return String(run?.author || run?.issue_author || "").trim(); + } + + function renderRunAuthorInline(run) { + const author = runAuthor(run); + + return author ? renderRunMetaFact("author", author) : ""; + } + function codexAccount(run) { const selected = run?.account || run?.codex_account || null; if (selected) { @@ -6486,7 +6496,11 @@

Run History

} function renderRunMetaLine(run) { - const items = [renderRunCodexAccountInline(run), renderRunTelemetryMetaItems(run)] + const items = [ + renderRunAuthorInline(run), + renderRunCodexAccountInline(run), + renderRunTelemetryMetaItems(run), + ] .filter(Boolean) .join(""); @@ -8591,6 +8605,7 @@

${escapeHtml(issueTitle)}

Debug Details
${field("Run", run.run_id)} + ${field("Author", runAuthor(run) || "none")} ${field("Attempt status", run.attempt_status || run.status)} ${field("Updated", formatTimestamp(run.updated_at))} ${field("Codex thread", runThreadSummary(run))} @@ -9010,6 +9025,7 @@

${escapeHtml(worktree.branch_name)}

for (const key of [ "issue_identifier", "title", + "author", "account", "accounts", "codex_account", diff --git a/apps/decodex/src/orchestrator/status.rs b/apps/decodex/src/orchestrator/status.rs index 81ac144..4567be9 100644 --- a/apps/decodex/src/orchestrator/status.rs +++ b/apps/decodex/src/orchestrator/status.rs @@ -108,6 +108,7 @@ struct OperatorHistoryLedgerRecord { struct OperatorIssueDisplayMetadata { issue_identifier: String, title: Option, + author: Option, } struct WorktreeOwnership { @@ -885,6 +886,7 @@ where OperatorIssueDisplayMetadata { issue_identifier: issue.identifier, title: Some(issue.title), + author: issue.author, }, ) }) @@ -980,6 +982,9 @@ fn apply_history_lane_issue_metadata( if let Some(title) = metadata.title.as_ref().filter(|title| !title.trim().is_empty()) { lane.title = Some(title.clone()); } + if let Some(author) = metadata.author.as_ref().filter(|author| !author.trim().is_empty()) { + lane.author = Some(author.clone()); + } } fn apply_run_issue_metadata( @@ -993,6 +998,9 @@ fn apply_run_issue_metadata( if let Some(title) = metadata.title.as_ref().filter(|title| !title.trim().is_empty()) { run.title = Some(title.clone()); } + if let Some(author) = metadata.author.as_ref().filter(|author| !author.trim().is_empty()) { + run.author = Some(author.clone()); + } } fn fill_missing_history_lane_issue_metadata( @@ -1013,6 +1021,11 @@ fn fill_missing_history_lane_issue_metadata( { lane.title = Some(title.clone()); } + if lane.author.as_ref().is_none_or(|author| author.trim().is_empty()) + && let Some(author) = metadata.author.as_ref().filter(|author| !author.trim().is_empty()) + { + lane.author = Some(author.clone()); + } } fn fill_missing_run_issue_metadata( @@ -1032,6 +1045,11 @@ fn fill_missing_run_issue_metadata( { run.title = Some(title.clone()); } + if run.author.as_ref().is_none_or(|author| author.trim().is_empty()) + && let Some(author) = metadata.author.as_ref().filter(|author| !author.trim().is_empty()) + { + run.author = Some(author.clone()); + } } fn hydrate_history_lanes_from_linear_ledger( @@ -1083,6 +1101,7 @@ fn hydrate_history_lane_from_ledger_records( let metadata = OperatorIssueDisplayMetadata { issue_identifier: record.record.issue_identifier.clone(), title: None, + author: None, }; fill_missing_history_lane_issue_metadata(lane, &metadata); @@ -1419,6 +1438,7 @@ where issue_id: issue.id, issue_identifier: issue.identifier, title: issue.title, + author: issue.author, state: issue.state.name, priority: issue.priority, created_at: issue.created_at, @@ -3362,6 +3382,7 @@ fn operator_run_status( issue_id: run.issue_id().to_owned(), issue_identifier, title: None, + author: None, attempt_number: run.attempt_number(), status, attempt_status: run.status().to_owned(), @@ -4094,6 +4115,7 @@ fn operator_history_lanes( issue_id: run.issue_id.clone(), issue_identifier: run.issue_identifier.clone(), title: run.title.clone(), + author: run.author.clone(), issue_key: operator_run_issue_key(run), attempt_count: 1, ledger_outcome: not_loaded_history_ledger_outcome(), @@ -4118,6 +4140,9 @@ fn hydrate_history_lane_from_run(lane: &mut OperatorHistoryLaneStatus, run: &Ope if lane.title.is_none() { lane.title = run.title.clone(); } + if lane.author.is_none() { + lane.author = run.author.clone(); + } } fn operator_run_group_key(run: &OperatorRunStatus) -> String { diff --git a/apps/decodex/src/orchestrator/tests.rs b/apps/decodex/src/orchestrator/tests.rs index 80e140f..61c7d55 100644 --- a/apps/decodex/src/orchestrator/tests.rs +++ b/apps/decodex/src/orchestrator/tests.rs @@ -459,6 +459,7 @@ fn sample_issue_with_project_slug_and_sort_fields( #[cfg(test)] project_slug: Some(_project_slug.to_owned()), title: String::from("Implement orchestration"), + author: Some(String::from("Yvette")), description: String::from("Body"), priority, created_at: created_at.to_owned(), diff --git a/apps/decodex/src/orchestrator/tests/operator/status/dashboard.rs b/apps/decodex/src/orchestrator/tests/operator/status/dashboard.rs index 393e20e..505b99e 100644 --- a/apps/decodex/src/orchestrator/tests/operator/status/dashboard.rs +++ b/apps/decodex/src/orchestrator/tests/operator/status/dashboard.rs @@ -416,6 +416,7 @@ fn operator_dashboard_renders_account_usage_controls() { assert!(response.contains("function codexAccount(run)")); assert!(response.contains("function codexAccounts(run)")); + assert!(response.contains("function renderRunAuthorInline(run)")); assert!(response.contains("function codexAccountDisplayName(account)")); assert!(response.contains("function codexAccountTokenLabel(refreshStatus)")); assert!(response.contains("function codexAccountWindowLabel(seconds)")); @@ -1496,6 +1497,7 @@ fn operator_dashboard_run_activity_preserves_snapshot_detail_fields() { assert!(response.contains("function snapshotWithLiveRunActivity(snapshot)")); assert!(response.contains("\"issue_identifier\"")); assert!(response.contains("\"title\"")); + assert!(response.contains("\"author\"")); assert!(response.contains("\"child_agent_activity\"")); assert!(response.contains("\"protocol_activity\"")); assert!(response.contains("!dashboardRunFieldHasValue(activityRun[key])")); diff --git a/apps/decodex/src/orchestrator/tests/operator/status/running_lanes.rs b/apps/decodex/src/orchestrator/tests/operator/status/running_lanes.rs index f8b0b99..3576a8c 100644 --- a/apps/decodex/src/orchestrator/tests/operator/status/running_lanes.rs +++ b/apps/decodex/src/orchestrator/tests/operator/status/running_lanes.rs @@ -216,14 +216,17 @@ fn live_operator_status_snapshot_hydrates_active_run_issue_display_metadata() { assert_eq!(active_run.project_id, config.service_id()); assert_eq!(active_run.issue_identifier.as_deref(), Some("XY-392")); assert_eq!(active_run.title.as_deref(), Some("Hydrate issue display metadata on run rows")); + assert_eq!(active_run.author.as_deref(), Some("Yvette")); assert_eq!(recent_run.issue_identifier.as_deref(), Some("XY-392")); assert_eq!(recent_run.title.as_deref(), Some("Hydrate issue display metadata on run rows")); + assert_eq!(recent_run.author.as_deref(), Some("Yvette")); assert_eq!(snapshot_json["active_runs"][0]["project_id"], "pubfi"); assert_eq!(snapshot_json["active_runs"][0]["issue_identifier"], "XY-392"); assert_eq!( snapshot_json["active_runs"][0]["title"], "Hydrate issue display metadata on run rows" ); + assert_eq!(snapshot_json["active_runs"][0]["author"], "Yvette"); } #[test] diff --git a/apps/decodex/src/orchestrator/tests/operator/status_support.rs b/apps/decodex/src/orchestrator/tests/operator/status_support.rs index 205f72b..377c7dd 100644 --- a/apps/decodex/src/orchestrator/tests/operator/status_support.rs +++ b/apps/decodex/src/orchestrator/tests/operator/status_support.rs @@ -209,6 +209,7 @@ fn operator_status_text_active_run() -> orchestrator::OperatorRunStatus { issue_id: String::from("issue-1"), issue_identifier: Some(String::from("PUB-101")), title: Some(String::from("Implement orchestration")), + author: Some(String::from("Yvette")), attempt_number: 1, status: String::from("running"), attempt_status: String::from("running"), @@ -312,6 +313,7 @@ fn operator_status_text_queued_candidates() -> Vec { issue_id: String::from("issue-1"), issue_identifier: String::from("PUB-101"), title: String::from("Running lane still has a backlog claim"), + author: Some(String::from("Yvette")), state: String::from("In Progress"), priority: Some(1), created_at: String::from("2026-03-14T09:57:00Z"), @@ -324,6 +326,7 @@ fn operator_status_text_queued_candidates() -> Vec { issue_id: String::from("issue-2"), issue_identifier: String::from("PUB-102"), title: String::from("Implement backlog surface"), + author: Some(String::from("Yvette")), state: String::from("Todo"), priority: Some(2), created_at: String::from("2026-03-14T09:58:00Z"), @@ -336,6 +339,7 @@ fn operator_status_text_queued_candidates() -> Vec { issue_id: String::from("issue-5"), issue_identifier: String::from("PUB-105"), title: String::from("Remove stale queue label"), + author: Some(String::from("Yvette")), state: String::from("Done"), priority: Some(3), created_at: String::from("2026-03-14T09:59:00Z"), diff --git a/apps/decodex/src/orchestrator/types.rs b/apps/decodex/src/orchestrator/types.rs index d8d4a0e..55cb993 100644 --- a/apps/decodex/src/orchestrator/types.rs +++ b/apps/decodex/src/orchestrator/types.rs @@ -721,6 +721,7 @@ struct OperatorHistoryLaneStatus { issue_id: String, issue_identifier: Option, title: Option, + author: Option, issue_key: String, attempt_count: usize, ledger_outcome: OperatorHistoryLedgerOutcome, @@ -753,6 +754,7 @@ struct OperatorRunStatus { issue_id: String, issue_identifier: Option, title: Option, + author: Option, attempt_number: i64, status: String, attempt_status: String, @@ -801,6 +803,7 @@ struct OperatorQueuedIssueStatus { issue_id: String, issue_identifier: String, title: String, + author: Option, state: String, priority: Option, created_at: String, diff --git a/apps/decodex/src/tracker.rs b/apps/decodex/src/tracker.rs index b12f863..e0ee99e 100644 --- a/apps/decodex/src/tracker.rs +++ b/apps/decodex/src/tracker.rs @@ -33,6 +33,7 @@ pub(crate) struct TrackerIssue { #[cfg(test)] pub(crate) project_slug: Option, pub(crate) title: String, + pub(crate) author: Option, pub(crate) description: String, pub(crate) priority: Option, pub(crate) created_at: String, diff --git a/apps/decodex/src/tracker/linear.rs b/apps/decodex/src/tracker/linear.rs index 97e08b0..54bbc1e 100644 --- a/apps/decodex/src/tracker/linear.rs +++ b/apps/decodex/src/tracker/linear.rs @@ -20,6 +20,11 @@ query IssuesWithLabel($labelName: String!, $after: String) { id identifier title + creator { + displayName + name + email + } description priority createdAt @@ -85,6 +90,11 @@ query IssueByIdentifier($issueIdentifier: String!) { id identifier title + creator { + displayName + name + email + } description priority createdAt @@ -146,6 +156,11 @@ query IssuesByIds($issueIds: [ID!], $after: String) { id identifier title + creator { + displayName + name + email + } description priority createdAt @@ -711,6 +726,7 @@ struct LinearIssue { id: String, identifier: String, title: String, + creator: Option, description: Option, priority: Option, #[serde(rename = "createdAt")] @@ -732,6 +748,14 @@ struct LinearTeam { labels: LabelConnection, } +#[derive(Deserialize)] +struct LinearUser { + #[serde(rename = "displayName")] + display_name: Option, + name: Option, + email: Option, +} + #[derive(Deserialize)] struct StateConnection { nodes: Vec, @@ -893,12 +917,15 @@ fn map_blockers(relations: &[LinearIssueRelation]) -> Vec { } fn map_issue(issue: LinearIssue, blockers: Vec) -> TrackerIssue { + let author = linear_user_display_name(issue.creator.as_ref()); + TrackerIssue { id: issue.id, identifier: issue.identifier, #[cfg(test)] project_slug: None, title: issue.title, + author, description: issue.description.unwrap_or_default(), priority: issue.priority, created_at: issue.created_at, @@ -933,13 +960,25 @@ fn map_issue(issue: LinearIssue, blockers: Vec) -> TrackerI } } +fn linear_user_display_name(user: Option<&LinearUser>) -> Option { + let user = user?; + + [&user.display_name, &user.name, &user.email] + .into_iter() + .filter_map(|value| value.as_deref()) + .map(str::trim) + .find(|value| !value.is_empty()) + .map(str::to_owned) +} + #[cfg(test)] mod tests { use serde_json::json; use crate::tracker::linear::{ GraphqlError, IssueRelationConnection, LabelConnection, LinearIssue, LinearIssueRelation, - LinearLabel, LinearRelatedIssue, LinearState, LinearTeam, PageInfo, StateConnection, + LinearLabel, LinearRelatedIssue, LinearState, LinearTeam, LinearUser, PageInfo, + StateConnection, }; #[test] @@ -948,6 +987,11 @@ mod tests { id: String::from("issue-1"), identifier: String::from("PUB-101"), title: String::from("Implement ordering"), + creator: Some(LinearUser { + display_name: Some(String::from("Yvette")), + name: Some(String::from("yvette")), + email: Some(String::from("yvette@example.com")), + }), description: Some(String::from("Body")), priority: Some(2), created_at: String::from("2026-03-13T04:16:17.133Z"), @@ -996,6 +1040,7 @@ mod tests { let mapped = super::map_issue(issue, blockers); assert_eq!(mapped.priority, Some(2)); + assert_eq!(mapped.author.as_deref(), Some("Yvette")); assert_eq!(mapped.created_at, "2026-03-13T04:16:17.133Z"); assert_eq!(mapped.updated_at, "2026-03-14T04:16:17.133Z"); assert_eq!(mapped.blockers.len(), 1);