ticketlens 0.20.1 → 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 +1 -1
- package/package.json +1 -1
- package/skills/jtb/SKILL.md +7 -1
- package/skills/jtb/scripts/fetch-my-tickets.mjs +4 -1
- package/skills/jtb/scripts/lib/attention-scorer.mjs +7 -2
- package/skills/jtb/scripts/lib/brief-assembler.mjs +7 -7
- package/skills/jtb/scripts/lib/interactive-list.mjs +12 -3
- package/skills/jtb/scripts/lib/styled-assembler.mjs +16 -8
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
package/skills/jtb/SKILL.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<!-- jtb-skill-version: 0.20.
|
|
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,
|
|
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,
|
|
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}`);
|