diff --git a/apps/decodex/src/agent/tracker_tool_bridge/tests.rs b/apps/decodex/src/agent/tracker_tool_bridge/tests.rs
index 47c62a79..fcbcf038 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 46e9631f..dc4af931 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 9f9212ca..725ec7ac 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 be5ef68a..b4afe03a 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 81ac144b..4567be90 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 80e140f6..61c7d551 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 393e20ef..505b99e9 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 f8b0b99f..3576a8cc 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 205f72b0..377c7ddc 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 d8d4a0e8..55cb993f 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 b12f8632..e0ee99ea 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 97e08b08..54bbc1ed 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);