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 +5 -1
- package/package.json +1 -1
- package/skills/jtb/SKILL.md +8 -1
- package/skills/jtb/scripts/fetch-my-tickets.mjs +9 -3
- package/skills/jtb/scripts/lib/attention-scorer.mjs +24 -3
- package/skills/jtb/scripts/lib/brief-assembler.mjs +7 -7
- package/skills/jtb/scripts/lib/help.mjs +4 -0
- package/skills/jtb/scripts/lib/interactive-list.mjs +12 -3
- package/skills/jtb/scripts/lib/profile-resolver.mjs +1 -0
- package/skills/jtb/scripts/lib/styled-assembler.mjs +16 -8
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
package/skills/jtb/SKILL.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<!-- jtb-skill-version: 0.
|
|
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
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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}`);
|