ticketlens 0.20.1 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -156,6 +156,7 @@ Three system templates ship out of the box:
156
156
  ticketlens triage # Scan assigned tickets — interactive
157
157
  ticketlens triage --profile=acme # Explicit profile
158
158
  ticketlens triage --stale=3 # Aging threshold: 3 days (default: 5)
159
+ ticketlens triage --sort=priority # Sort by priority first, then urgency (default: urgency)
159
160
  ticketlens triage --status="Code Review,QA" # Override statuses to scan
160
161
  ticketlens triage --assignee="Jane Dev" # Another dev's tickets [Team]
161
162
  ticketlens triage --sprint="Sprint 12" # Filter by sprint [Team]
@@ -180,6 +181,8 @@ ticketlens triage --static # Static table, no interactive mo
180
181
 
181
182
  `--stale=N` controls both categories. Unanswered comments older than N days downgrade from "needs response" to "aging" automatically.
182
183
 
184
+ `--sort=priority|urgency` controls display order for this run only. `priority` groups tickets by Jira priority (Highest → Lowest, unknown last) with urgency as the tiebreaker; `urgency` is today's default ordering. Set a permanent default by adding `"sortBy": "priority"` to a profile in `~/.ticketlens/profiles.json` — the flag overrides it for a single run, same relationship `--stale=N` has to `staleDays`. This is a personal, per-profile preference — it does not sync with the Console's own sort-order setting on the Account page, which only affects the Console queue view.
185
+
183
186
  Interactive mode: `↑/↓` navigate, `Enter` open in browser, `p` switch profile, `q/Esc` exit. Columns adapt to terminal width.
184
187
 
185
188
  Status mismatch auto-fix: if configured statuses don't match Jira's exact casing, triage shows a diff and offers to update your profile:
@@ -600,6 +603,7 @@ ticketlens triage # Scan assigned tickets — intera
600
603
  ticketlens triage --profile=acme # Explicit profile
601
604
  ticketlens triage --stale=3 # Needs-response window: 3 days (default: 5)
602
605
  ticketlens triage --stale=10 # More lenient — only flag very stale tickets
606
+ ticketlens triage --sort=priority # Sort by priority first, then urgency (default: urgency)
603
607
  ticketlens triage --status="Code Review,QA Testing" # Scan these statuses only
604
608
  ticketlens triage --static # Static table output (no interactive mode)
605
609
  ticketlens triage --plain # Plain markdown — pipe to LLM or file
@@ -839,7 +843,7 @@ ticketlens triage --push # Push snapshot to the Console qu
839
843
  ticketlens triage --share # Generate a 24h share URL — paste into Slack, no login needed for recipients
840
844
  ```
841
845
 
842
- `--push` syncs the scored snapshot to the TicketLens Console after each triage run. The queue page at `/console/queue` shows the latest snapshot for every team profile — no manual refresh needed.
846
+ `--push` syncs the scored snapshot to the TicketLens Console after each triage run. The queue page at `/console/queue` shows the latest snapshot for every team profile — no manual refresh needed. Unlike the terminal view, the pushed snapshot includes every assigned ticket except ones excluded by a local `ignore` custom rule — so a manager's Console-configured priority-based notify/schedule rule can match a ticket you're actively working on, not just stale or awaiting-response ones.
843
847
 
844
848
  `--share` generates a signed URL valid for 24 hours. Recipients open it in any browser — no account, no install. The asymmetry is the product: you run one command, everyone sees the same snapshot.
845
849
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ticketlens",
3
- "version": "0.20.1",
3
+ "version": "0.21.0",
4
4
  "description": "Jira CLI for developers — fetch ticket context, triage your queue, and stop tab-switching. Zero dependencies, all local.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,4 +1,4 @@
1
- <!-- jtb-skill-version: 0.20.1 -->
1
+ <!-- jtb-skill-version: 0.21.0 -->
2
2
  ---
3
3
  name: jtb
4
4
  description: Fetch a Jira ticket's full context (description, comments, linked issues, code references) and assemble a structured TicketBrief for implementation planning. Use when user types /jtb, mentions a Jira ticket key, or wants to plan work from a Jira ticket.
@@ -28,6 +28,7 @@ Fetches a Jira ticket and produces a structured brief with code references, then
28
28
  /jtb PROD-1234 --template=full # apply full template (all sections, default)
29
29
  /jtb triage # scan your assigned tickets for attention
30
30
  /jtb triage --stale=3 # custom aging threshold (days)
31
+ /jtb triage --sort=priority # sort by priority first, then urgency (default: urgency)
31
32
  /jtb triage --status=CR,QA # only check specific statuses
32
33
  /jtb triage --profile=acme # explicit profile override
33
34
  /jtb triage --all # triage all configured profiles at once, merged (Pro)
@@ -349,6 +350,12 @@ Non-Pro users push with `compliance_status: unknown` — no data is lost, and th
349
350
 
350
351
  `--push` now includes each ticket's `priority` and `labels` in the payload (previously omitted). This feeds two Console features under `/console/admin/rules`: real-value suggestions for the Priority/Label fields when building custom attention rules (previously only Status/Key prefix had them), and the `notify`/`schedule` custom-rule actions, which match on priority/label server-side. No new flag — this is automatic on every `--push`.
351
352
 
353
+ ### triage --push ticket set (Team)
354
+
355
+ `--push` sends every assigned ticket except ones excluded by a local `ignore` custom rule — this includes tickets that are `clear` (recently updated, no unanswered comment) and therefore hidden from the terminal view. Terminal display, `--digest`, `--export`, and `--share` remain unchanged (they still show only needs-response/aging/stale tickets — what needs *your* attention right now). The wider push set exists so a manager's Console-configured priority-based `notify`/`schedule` rule can match a ticket you're actively working on correctly, not just one that's stale or awaiting a response.
356
+
357
+ Terminal output (all formats) now also shows each ticket's Priority column alongside Status.
358
+
352
359
  ### --plain / --styled
353
360
 
354
361
  Control output formatting:
@@ -74,7 +74,7 @@ export async function run(args, envOrOpts = process.env, fetcher = globalThis.fe
74
74
  args,
75
75
  ['--help', '-h', '--static', '--plain', '--styled', '--profile=', '--stale=', '--status=',
76
76
  '--assignee=', '--sprint=', '--export=', '--digest', '--push', '--share', '--all', '--save=',
77
- '--project=', '--label=', '--priority='],
77
+ '--project=', '--label=', '--priority=', '--sort='],
78
78
  { hints: ['--depth=', '--no-attachments', '--no-cache'] }
79
79
  );
80
80
  if (validatedArgs === null) { process.exitCode = 1; return; }
@@ -86,6 +86,8 @@ export async function run(args, envOrOpts = process.env, fetcher = globalThis.fe
86
86
  const staleArg = args.find(a => a.startsWith('--stale='));
87
87
  const staleDays = staleArg ? parseInt(staleArg.split('=')[1], 10) : 5;
88
88
 
89
+ const sortArg = args.find(a => a.startsWith('--sort='));
90
+
89
91
  const statusArg = args.find(a => a.startsWith('--status='));
90
92
  const assigneeArg = args.find(a => a.startsWith('--assignee='));
91
93
  const sprintArg = args.find(a => a.startsWith('--sprint='));
@@ -426,7 +428,8 @@ export async function run(args, envOrOpts = process.env, fetcher = globalThis.fe
426
428
 
427
429
  const scored = tickets.map(t => scoreAttention(t, effectiveUser, { staleDays, customRules: conn.attentionRules, staleRule: conn.staleRule ?? null }));
428
430
  const actionable = scored.filter(s => s.urgency !== 'clear' && s.urgency !== 'ignore');
429
- const sorted = sortByUrgency(actionable);
431
+ const sortBy = sortArg ? sortArg.split('=')[1] : conn.sortBy;
432
+ const sorted = sortByUrgency(actionable, { sortBy });
430
433
  const rawTicketMap = new Map(tickets.map(t => [t.key, t]));
431
434
 
432
435
  // Always save a daily snapshot for history tracking (non-fatal)
@@ -497,8 +500,11 @@ export async function run(args, envOrOpts = process.env, fetcher = globalThis.fe
497
500
  const printFn = opts.print ?? ((s) => process.stdout.write(s));
498
501
  let cliActivity = null;
499
502
  try { cliActivity = readAndResetActivity(configDir ?? DEFAULT_CONFIG_DIR); } catch { /* non-fatal */ }
503
+ // Server-side notify/schedule rules must see actively-worked ('clear') tickets too —
504
+ // only a ticket the user explicitly told the CLI to ignore is excluded from push.
505
+ const pushable = sortByUrgency(scored.filter(s => s.urgency !== 'ignore'));
500
506
  await pushFn({
501
- sorted,
507
+ sorted: pushable,
502
508
  rawTicketMap,
503
509
  profile: profileName ?? 'default',
504
510
  baseUrl: conn.baseUrl,
@@ -111,10 +111,10 @@ export function scoreAttention(ticket, currentUser, opts = {}) {
111
111
  for (const rule of customRules) {
112
112
  const action = applyCustomRule(ticket, rule);
113
113
  if (action === 'ignore') {
114
- return { ticketKey: ticket.key, summary: ticket.summary, status: ticket.status, urgency: 'ignore', reason: rule.reason ?? 'Ignored by custom rule', lastComment: null };
114
+ return { ticketKey: ticket.key, summary: ticket.summary, status: ticket.status, priority: ticket.priority ?? null, urgency: 'ignore', reason: rule.reason ?? 'Ignored by custom rule', lastComment: null };
115
115
  }
116
116
  if (action === 'force-urgent') {
117
- return { ticketKey: ticket.key, summary: ticket.summary, status: ticket.status, urgency: 'needs-response', reason: rule.reason ?? 'Flagged by custom rule', lastComment: null };
117
+ return { ticketKey: ticket.key, summary: ticket.summary, status: ticket.status, priority: ticket.priority ?? null, urgency: 'needs-response', reason: rule.reason ?? 'Flagged by custom rule', lastComment: null };
118
118
  }
119
119
  }
120
120
  }
@@ -135,6 +135,7 @@ export function scoreAttention(ticket, currentUser, opts = {}) {
135
135
  ticketKey: ticket.key,
136
136
  summary: ticket.summary,
137
137
  status: ticket.status,
138
+ priority: ticket.priority ?? null,
138
139
  urgency: 'aging',
139
140
  reason: `${lastComment.author} commented ${Math.floor(daysSinceComment)}d ago`,
140
141
  lastComment,
@@ -146,6 +147,7 @@ export function scoreAttention(ticket, currentUser, opts = {}) {
146
147
  ticketKey: ticket.key,
147
148
  summary: ticket.summary,
148
149
  status: ticket.status,
150
+ priority: ticket.priority ?? null,
149
151
  urgency: 'needs-response',
150
152
  reason: `${lastComment.author} commented`,
151
153
  lastComment,
@@ -161,6 +163,7 @@ export function scoreAttention(ticket, currentUser, opts = {}) {
161
163
  ticketKey: ticket.key,
162
164
  summary: ticket.summary,
163
165
  status: ticket.status,
166
+ priority: ticket.priority ?? null,
164
167
  urgency: 'aging',
165
168
  reason: `No activity for ${Math.floor(daysSinceUpdate)} days`,
166
169
  lastComment,
@@ -182,6 +185,7 @@ export function scoreAttention(ticket, currentUser, opts = {}) {
182
185
  ticketKey: ticket.key,
183
186
  summary: ticket.summary,
184
187
  status: ticket.status,
188
+ priority: ticket.priority ?? null,
185
189
  urgency: 'stale',
186
190
  reason: `Stuck in '${ticket.status}' for ${Math.floor(daysInCurrentStatus)}d`,
187
191
  lastComment,
@@ -194,6 +198,7 @@ export function scoreAttention(ticket, currentUser, opts = {}) {
194
198
  ticketKey: ticket.key,
195
199
  summary: ticket.summary,
196
200
  status: ticket.status,
201
+ priority: ticket.priority ?? null,
197
202
  urgency: 'clear',
198
203
  reason: 'Up to date',
199
204
  lastComment,
@@ -202,8 +207,24 @@ export function scoreAttention(ticket, currentUser, opts = {}) {
202
207
 
203
208
  export const URGENCY_ORDER = { 'needs-response': 0, 'aging': 1, 'stale': 2, 'clear': 3 };
204
209
 
205
- export function sortByUrgency(scores) {
210
+ // Standard Jira priority names, case-insensitive. Synonyms for the top tier (some
211
+ // instances/plugins use "Urgent" or "Blocker" instead of "Highest") share rank 0.
212
+ // Anything not in this map (custom priority scheme, or missing) sorts last.
213
+ export const PRIORITY_ORDER = { highest: 0, urgent: 0, blocker: 0, high: 1, medium: 2, low: 3, lowest: 4 };
214
+
215
+ function priorityRank(priority) {
216
+ if (!priority) return Infinity;
217
+ const rank = PRIORITY_ORDER[priority.toLowerCase()];
218
+ return rank === undefined ? Infinity : rank;
219
+ }
220
+
221
+ export function sortByUrgency(scores, opts = {}) {
222
+ const { sortBy } = opts;
206
223
  return [...scores].sort((a, b) => {
224
+ if (sortBy === 'priority') {
225
+ const priorityDiff = priorityRank(a.priority) - priorityRank(b.priority);
226
+ if (priorityDiff !== 0) return priorityDiff;
227
+ }
207
228
  const orderDiff = URGENCY_ORDER[a.urgency] - URGENCY_ORDER[b.urgency];
208
229
  if (orderDiff !== 0) return orderDiff;
209
230
  // Within same urgency, sort by most recent activity (lastComment date or ticket updated)
@@ -151,12 +151,12 @@ export function assembleTriageSummary(scoredTickets, opts = {}) {
151
151
  const ago = t.lastComment ? timeAgo(t.lastComment.created) : '';
152
152
  const commenter = t.lastComment?.author ?? 'Unknown';
153
153
  const snippet = t.lastComment?.body ? truncate(t.lastComment.body, 60) : '';
154
- return [String(i + 1), t.ticketKey, truncate(t.summary, 50), t.status, commenter, ago, snippet];
154
+ return [String(i + 1), t.ticketKey, truncate(t.summary, 50), t.status, t.priority || '—', commenter, ago, snippet];
155
155
  });
156
156
  const table = formatTable(
157
- ['#', 'Ticket', 'Summary', 'Status', 'From', 'When', 'Comment'],
157
+ ['#', 'Ticket', 'Summary', 'Status', 'Priority', 'From', 'When', 'Comment'],
158
158
  tableRows,
159
- { maxWidths: { 2: 50, 6: 60 } },
159
+ { maxWidths: { 2: 50, 7: 60 } },
160
160
  );
161
161
  sections.push(`Needs Response (${needsResponse.length})\n\n${table}`);
162
162
  }
@@ -166,10 +166,10 @@ export function assembleTriageSummary(scoredTickets, opts = {}) {
166
166
  const tableRows = aging.map((t, i) => {
167
167
  allKeys.push(t.ticketKey);
168
168
  const days = t.daysSinceUpdate ?? '?';
169
- return [String(agingOffset + i + 1), t.ticketKey, truncate(t.summary, 50), t.status, `${days}d`];
169
+ return [String(agingOffset + i + 1), t.ticketKey, truncate(t.summary, 50), t.status, t.priority || '—', `${days}d`];
170
170
  });
171
171
  const table = formatTable(
172
- ['#', 'Ticket', 'Summary', 'Status', 'Idle'],
172
+ ['#', 'Ticket', 'Summary', 'Status', 'Priority', 'Idle'],
173
173
  tableRows,
174
174
  { maxWidths: { 2: 50 } },
175
175
  );
@@ -181,10 +181,10 @@ export function assembleTriageSummary(scoredTickets, opts = {}) {
181
181
  const tableRows = stale.map((t, i) => {
182
182
  allKeys.push(t.ticketKey);
183
183
  const days = t.daysInCurrentStatus ?? '?';
184
- return [String(staleOffset + i + 1), t.ticketKey, truncate(t.summary, 50), t.status, `${days}d`];
184
+ return [String(staleOffset + i + 1), t.ticketKey, truncate(t.summary, 50), t.status, t.priority || '—', `${days}d`];
185
185
  });
186
186
  const table = formatTable(
187
- ['#', 'Ticket', 'Summary', 'Status', 'Stuck'],
187
+ ['#', 'Ticket', 'Summary', 'Status', 'Priority', 'Stuck'],
188
188
  tableRows,
189
189
  { maxWidths: { 2: 50 } },
190
190
  );
@@ -84,6 +84,7 @@ export function printHelp({ stream = process.stdout } = {}) {
84
84
  // "--assignee=NAME"=15, "--sprint=NAME"=13, "--static"=8, "--plain"=7 → target=19
85
85
  ` ${s.brand('--profile')}=${s.dim('NAME')} Use a specific Jira profile`,
86
86
  ` ${s.brand('--stale')}=${s.dim('N')} Aging threshold in days ${s.dim('(default: 5)')}`,
87
+ ` ${s.brand('--sort')}=${s.dim('ORDER')} Sort order ${s.dim('(priority|urgency, default: urgency)')}`,
87
88
  ` ${s.brand('--status')}=${s.dim('X,Y')} Override statuses to scan`,
88
89
  ` ${s.brand('--assignee')}=${s.dim('NAME')} Triage another dev's tickets ${s.dim('[Team]')}`,
89
90
  ` ${s.brand('--sprint')}=${s.dim('NAME')} Filter by sprint name ${s.dim('[Team]')}`,
@@ -107,6 +108,7 @@ export function printHelp({ stream = process.stdout } = {}) {
107
108
  ` ${s.dim('$')} ticketlens get PROJ-123 --depth=0 --profile=myteam`,
108
109
  ` ${s.dim('$')} ticketlens triage`,
109
110
  ` ${s.dim('$')} ticketlens triage --profile=acme --stale=3`,
111
+ ` ${s.dim('$')} ticketlens triage --sort=priority`,
110
112
  ` ${s.dim('$')} ticketlens triage --static`,
111
113
  '',
112
114
  ` ${s.bold('CONFIGURATION')}`,
@@ -625,6 +627,7 @@ export function printTriageHelp({ stream = process.stdout } = {}) {
625
627
  // "--assignee=NAME"=15, "--sprint=NAME"=13, "--static"=8, "--plain"=7, "-h, --help"=10 → target=19
626
628
  ` ${s.brand('--profile')}=${s.dim('NAME')} Use a specific Jira profile`,
627
629
  ` ${s.brand('--stale')}=${s.dim('N')} Aging threshold in days ${s.dim('(default: 5)')}`,
630
+ ` ${s.brand('--sort')}=${s.dim('ORDER')} Sort order ${s.dim('(priority|urgency, default: urgency)')}`,
628
631
  ` ${s.brand('--status')}=${s.dim('X,Y')} Override statuses to scan`,
629
632
  ` ${s.brand('--assignee')}=${s.dim('NAME')} Triage another dev's tickets ${s.dim('[Team]')}`,
630
633
  ` ${s.brand('--sprint')}=${s.dim('NAME')} Filter by sprint name ${s.dim('[Team]')}`,
@@ -645,6 +648,7 @@ export function printTriageHelp({ stream = process.stdout } = {}) {
645
648
  '',
646
649
  ` ${s.dim('$')} ticketlens triage`,
647
650
  ` ${s.dim('$')} ticketlens triage --profile=acme`,
651
+ ` ${s.dim('$')} ticketlens triage --sort=priority`,
648
652
  ` ${s.dim('$')} ticketlens triage --stale=3 --status="Code Review,QA Testing"`,
649
653
  ` ${s.dim('$')} ticketlens triage --assignee="Jane Dev" --sprint="Sprint 12"`,
650
654
  ` ${s.dim('$')} ticketlens triage --project=MYPROJ --priority=High`,
@@ -54,9 +54,9 @@ export function runInteractiveList(tickets, opts = {}) {
54
54
  // Wide (≥150): all columns. Medium (≥100): no detail. Narrow (<100): key+title+status only.
55
55
  function getColLayout() {
56
56
  const w = process.stdout.columns || 120;
57
- if (w >= 150) return { key: 12, title: 45, status: 14, from: 14, when: 8, detail: 35 };
58
- if (w >= 100) return { key: 12, title: 35, status: 14, from: 14, when: 8, detail: 0 };
59
- return { key: 10, title: Math.max(20, w - 36), status: 12, from: 0, when: 0, detail: 0 };
57
+ if (w >= 150) return { key: 12, title: 45, status: 14, priority: 10, from: 14, when: 8, detail: 35 };
58
+ if (w >= 100) return { key: 12, title: 35, status: 14, priority: 10, from: 14, when: 8, detail: 0 };
59
+ return { key: 10, title: Math.max(20, w - 36), status: 12, priority: 0, from: 0, when: 0, detail: 0 };
60
60
  }
61
61
 
62
62
  function buildRow(ticket, index) {
@@ -71,6 +71,10 @@ export function runInteractiveList(tickets, opts = {}) {
71
71
 
72
72
  const parts = [` ${dot}`, key, title, status];
73
73
 
74
+ if (COL.priority > 0) {
75
+ parts.push(padRight(ticket.priority || '—', COL.priority));
76
+ }
77
+
74
78
  if (COL.from > 0) {
75
79
  const from = padRight(isNR ? (ticket.lastComment?.author ?? 'Unknown') : '', COL.from);
76
80
  const when = padRight(isNR && ticket.lastComment ? timeAgo(ticket.lastComment.created) : '', COL.when);
@@ -111,6 +115,11 @@ export function runInteractiveList(tickets, opts = {}) {
111
115
  const hdrParts = [` ${padRight('', 1)}`, padRight('Ticket', COL.key), padRight('Title', COL.title), padRight('Status', COL.status)];
112
116
  const sepParts = [` ${'\u2500'.repeat(1)}`, '\u2500'.repeat(COL.key), '\u2500'.repeat(COL.title), '\u2500'.repeat(COL.status)];
113
117
 
118
+ if (COL.priority > 0) {
119
+ hdrParts.push(padRight('Priority', COL.priority));
120
+ sepParts.push('\u2500'.repeat(COL.priority));
121
+ }
122
+
114
123
  if (COL.from > 0) {
115
124
  hdrParts.push(padRight('From', COL.from), padRight('When', COL.when));
116
125
  sepParts.push('\u2500'.repeat(COL.from), '\u2500'.repeat(COL.when));
@@ -233,6 +233,7 @@ export function resolveConnection(ticketKey, opts = {}) {
233
233
  ticketPrefixes: profile.ticketPrefixes || null,
234
234
  attentionRules: profile.attentionRules ?? null,
235
235
  staleRule: profile.staleRule ?? null,
236
+ sortBy: profile.sortBy ?? null,
236
237
  allowPrivateIp: profile.allowPrivateIp || false,
237
238
  source: 'profile',
238
239
  profileName: profile.name,
@@ -15,6 +15,12 @@ function statusColor(s, status) {
15
15
  if (/progress|review|testing|qa/.test(st)) return s.yellow(status);
16
16
  return status;
17
17
  }
18
+ function priorityColor(s, priority) {
19
+ const p = (priority || '').toLowerCase();
20
+ if (/highest|urgent|blocker/.test(p)) return s.red(priority);
21
+ if (/high/.test(p)) return s.yellow(priority);
22
+ return priority;
23
+ }
18
24
 
19
25
  export function styleTriageSummary(scoredTickets, opts = {}) {
20
26
  const { staleDays = 5, baseUrl, styled = true } = opts;
@@ -51,17 +57,19 @@ export function styleTriageSummary(scoredTickets, opts = {}) {
51
57
  return colorFn('●') + ' ' + key;
52
58
  };
53
59
 
60
+ const priorityCell = (t) => t.priority ? priorityColor(s, t.priority) : '—';
61
+
54
62
  if (needsResponse.length > 0) {
55
63
  const tableRows = needsResponse.map((t, i) => {
56
64
  const ago = t.lastComment ? timeAgo(t.lastComment.created) : '';
57
65
  const commenter = t.lastComment?.author ?? 'Unknown';
58
66
  const snippet = t.lastComment?.body ? truncate(t.lastComment.body, 40) : '';
59
- return [String(i + 1), ticketCell(t.ticketKey, s.red), truncate(t.summary, 45), t.status, commenter, ago, snippet];
67
+ return [String(i + 1), ticketCell(t.ticketKey, s.red), truncate(t.summary, 45), t.status, priorityCell(t), commenter, ago, snippet];
60
68
  });
61
69
  const table = formatTable(
62
- ['#', 'Ticket', 'Title', 'Status', 'From', 'When', 'Comment'],
70
+ ['#', 'Ticket', 'Title', 'Status', 'Priority', 'From', 'When', 'Comment'],
63
71
  tableRows,
64
- { maxWidths: { 2: 45, 6: 40 } },
72
+ { maxWidths: { 2: 45, 7: 40 } },
65
73
  );
66
74
  sections.push(table);
67
75
  }
@@ -70,10 +78,10 @@ export function styleTriageSummary(scoredTickets, opts = {}) {
70
78
  const agingOffset = needsResponse.length;
71
79
  const tableRows = aging.map((t, i) => {
72
80
  const days = t.daysSinceUpdate ?? '?';
73
- return [String(agingOffset + i + 1), ticketCell(t.ticketKey, s.yellow), truncate(t.summary, 45), t.status, `${days}d`];
81
+ return [String(agingOffset + i + 1), ticketCell(t.ticketKey, s.yellow), truncate(t.summary, 45), t.status, priorityCell(t), `${days}d`];
74
82
  });
75
83
  const table = formatTable(
76
- ['#', 'Ticket', 'Title', 'Status', 'Idle'],
84
+ ['#', 'Ticket', 'Title', 'Status', 'Priority', 'Idle'],
77
85
  tableRows,
78
86
  { maxWidths: { 2: 45 } },
79
87
  );
@@ -84,10 +92,10 @@ export function styleTriageSummary(scoredTickets, opts = {}) {
84
92
  const staleOffset = needsResponse.length + aging.length;
85
93
  const tableRows = stale.map((t, i) => {
86
94
  const days = t.daysInCurrentStatus ?? '?';
87
- return [String(staleOffset + i + 1), ticketCell(t.ticketKey, s.cyan), truncate(t.summary, 45), t.status, `${days}d`];
95
+ return [String(staleOffset + i + 1), ticketCell(t.ticketKey, s.cyan), truncate(t.summary, 45), t.status, priorityCell(t), `${days}d`];
88
96
  });
89
97
  const table = formatTable(
90
- ['#', 'Ticket', 'Title', 'Status', 'Stuck'],
98
+ ['#', 'Ticket', 'Title', 'Status', 'Priority', 'Stuck'],
91
99
  tableRows,
92
100
  { maxWidths: { 2: 45 } },
93
101
  );
@@ -138,7 +146,7 @@ export function styleBrief(ticket, codeRefs = null, opts = {}) {
138
146
  const meta = [
139
147
  `${s.dim('Type:')} ${ticket.type}`,
140
148
  `${s.dim('Status:')} ${statusColor(s, ticket.status)}`,
141
- `${s.dim('Priority:')} ${ticket.priority}`,
149
+ `${s.dim('Priority:')} ${ticket.priority ? priorityColor(s, ticket.priority) : '—'}`,
142
150
  `${s.dim('Assignee:')} ${ticket.assignee ?? 'Unassigned'}`,
143
151
  ];
144
152
  if (ticket.sprint) meta.push(`${s.dim('Sprint:')} ${ticket.sprint}`);