ticketlens 0.20.0 → 0.20.2

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
@@ -839,7 +839,7 @@ ticketlens triage --push # Push snapshot to the Console qu
839
839
  ticketlens triage --share # Generate a 24h share URL — paste into Slack, no login needed for recipients
840
840
  ```
841
841
 
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.
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. 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
843
 
844
844
  `--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
845
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ticketlens",
3
- "version": "0.20.0",
3
+ "version": "0.20.2",
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.0 -->
1
+ <!-- jtb-skill-version: 0.20.2 -->
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.
@@ -349,6 +349,12 @@ Non-Pro users push with `compliance_status: unknown` — no data is lost, and th
349
349
 
350
350
  `--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
351
 
352
+ ### triage --push ticket set (Team)
353
+
354
+ `--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.
355
+
356
+ Terminal output (all formats) now also shows each ticket's Priority column alongside Status.
357
+
352
358
  ### --plain / --styled
353
359
 
354
360
  Control output formatting:
@@ -497,8 +497,11 @@ export async function run(args, envOrOpts = process.env, fetcher = globalThis.fe
497
497
  const printFn = opts.print ?? ((s) => process.stdout.write(s));
498
498
  let cliActivity = null;
499
499
  try { cliActivity = readAndResetActivity(configDir ?? DEFAULT_CONFIG_DIR); } catch { /* non-fatal */ }
500
+ // Server-side notify/schedule rules must see actively-worked ('clear') tickets too —
501
+ // only a ticket the user explicitly told the CLI to ignore is excluded from push.
502
+ const pushable = sortByUrgency(scored.filter(s => s.urgency !== 'ignore'));
500
503
  await pushFn({
501
- sorted,
504
+ sorted: pushable,
502
505
  rawTicketMap,
503
506
  profile: profileName ?? 'default',
504
507
  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,
@@ -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
  );
@@ -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));
@@ -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}`);