@aaronsb/jira-cloud-mcp 0.5.5 → 0.5.8
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.
|
@@ -134,7 +134,7 @@ export class JiraClient {
|
|
|
134
134
|
people: people.length > 0 ? people : undefined,
|
|
135
135
|
};
|
|
136
136
|
}
|
|
137
|
-
async getIssue(issueKey, includeComments = false, includeAttachments = false, customFieldMeta) {
|
|
137
|
+
async getIssue(issueKey, includeComments = false, includeAttachments = false, customFieldMeta, includeHistory = false) {
|
|
138
138
|
const fields = [...this.issueFields];
|
|
139
139
|
if (includeAttachments) {
|
|
140
140
|
fields.push('attachment');
|
|
@@ -147,13 +147,32 @@ export class JiraClient {
|
|
|
147
147
|
}
|
|
148
148
|
}
|
|
149
149
|
}
|
|
150
|
+
const expands = [];
|
|
151
|
+
if (includeComments)
|
|
152
|
+
expands.push('comments');
|
|
153
|
+
if (includeHistory)
|
|
154
|
+
expands.push('changelog');
|
|
150
155
|
const params = {
|
|
151
156
|
issueIdOrKey: issueKey,
|
|
152
157
|
fields,
|
|
153
|
-
expand:
|
|
158
|
+
expand: expands.length > 0 ? expands.join(',') : undefined,
|
|
154
159
|
};
|
|
155
160
|
const issue = await this.client.issues.getIssue(params);
|
|
156
161
|
const issueDetails = this.mapIssueFields(issue);
|
|
162
|
+
// Extract status transitions from changelog
|
|
163
|
+
if (includeHistory && issue.changelog?.histories) {
|
|
164
|
+
const histories = issue.changelog.histories;
|
|
165
|
+
issueDetails.statusHistory = histories
|
|
166
|
+
.flatMap(h => h.items
|
|
167
|
+
.filter(item => item.field === 'status')
|
|
168
|
+
.map(item => ({
|
|
169
|
+
date: h.created,
|
|
170
|
+
from: item.fromString || '',
|
|
171
|
+
to: item.toString || '',
|
|
172
|
+
author: h.author?.displayName || 'Unknown',
|
|
173
|
+
})))
|
|
174
|
+
.sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime());
|
|
175
|
+
}
|
|
157
176
|
// Extract custom field values using catalog metadata
|
|
158
177
|
if (customFieldMeta) {
|
|
159
178
|
const rawFields = issue.fields;
|
|
@@ -4,11 +4,12 @@ import { analysisNextSteps } from '../utils/next-steps.js';
|
|
|
4
4
|
import { normalizeArgs } from '../utils/normalize-args.js';
|
|
5
5
|
const ALL_METRICS = ['points', 'time', 'schedule', 'cycle', 'distribution'];
|
|
6
6
|
const VALID_GROUP_BY = ['project', 'assignee', 'priority', 'issuetype'];
|
|
7
|
-
const
|
|
7
|
+
const MAX_ISSUES_HARD = 500; // absolute ceiling for detail metrics — beyond this, context explodes
|
|
8
|
+
const MAX_ISSUES_DEFAULT = 100;
|
|
8
9
|
const CUBE_SAMPLE_PCT = 0.2; // 20% of total issues
|
|
9
10
|
const CUBE_SAMPLE_MIN = 50; // floor — enough for rare dimension values
|
|
10
11
|
const CUBE_SAMPLE_MAX = 500; // ceiling — proven fast with lean search
|
|
11
|
-
const
|
|
12
|
+
const DEFAULT_GROUP_LIMIT = 20;
|
|
12
13
|
const MAX_COUNT_QUERIES = 150; // ADR-206 budget: max count API calls per execution
|
|
13
14
|
const STANDARD_MEASURES = 6; // total, open, overdue, high+, created_7d, resolved_7d
|
|
14
15
|
// ── Helpers ────────────────────────────────────────────────────────────
|
|
@@ -58,11 +59,14 @@ function countBy(items, keyFn) {
|
|
|
58
59
|
function sumBy(items, valueFn) {
|
|
59
60
|
return items.reduce((sum, item) => sum + (valueFn(item) || 0), 0);
|
|
60
61
|
}
|
|
61
|
-
function mapToString(map, separator = ' | ') {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
62
|
+
function mapToString(map, separator = ' | ', limit) {
|
|
63
|
+
const sorted = [...map.entries()].sort((a, b) => b[1] - a[1]);
|
|
64
|
+
const capped = limit ? sorted.slice(0, limit) : sorted;
|
|
65
|
+
const result = capped.map(([k, v]) => `${k}: ${v}`).join(separator);
|
|
66
|
+
if (limit && sorted.length > limit) {
|
|
67
|
+
return `${result} | (+${sorted.length - limit} more — use groupLimit to see all)`;
|
|
68
|
+
}
|
|
69
|
+
return result;
|
|
66
70
|
}
|
|
67
71
|
function median(values) {
|
|
68
72
|
if (values.length === 0)
|
|
@@ -255,16 +259,16 @@ export function renderCycle(issues, now) {
|
|
|
255
259
|
}
|
|
256
260
|
return lines.join('\n');
|
|
257
261
|
}
|
|
258
|
-
export function renderDistribution(issues) {
|
|
262
|
+
export function renderDistribution(issues, groupLimit = DEFAULT_GROUP_LIMIT) {
|
|
259
263
|
const lines = ['## Distribution', ''];
|
|
260
264
|
const byStatus = countBy(issues, i => i.status);
|
|
261
|
-
lines.push(`**By status:** ${mapToString(byStatus)}`);
|
|
265
|
+
lines.push(`**By status:** ${mapToString(byStatus, ' | ', groupLimit)}`);
|
|
262
266
|
const byAssignee = countBy(issues, i => i.assignee || 'Unassigned');
|
|
263
|
-
lines.push(`**By assignee:** ${mapToString(byAssignee)}`);
|
|
267
|
+
lines.push(`**By assignee:** ${mapToString(byAssignee, ' | ', groupLimit)}`);
|
|
264
268
|
const byPriority = countBy(issues, i => i.priority || 'None');
|
|
265
|
-
lines.push(`**By priority:** ${mapToString(byPriority)}`);
|
|
269
|
+
lines.push(`**By priority:** ${mapToString(byPriority, ' | ', groupLimit)}`);
|
|
266
270
|
const byType = countBy(issues, i => i.issueType || 'Unknown');
|
|
267
|
-
lines.push(`**By type:** ${mapToString(byType)}`);
|
|
271
|
+
lines.push(`**By type:** ${mapToString(byType, ' | ', groupLimit)}`);
|
|
268
272
|
return lines.join('\n');
|
|
269
273
|
}
|
|
270
274
|
/** Extract project keys from JQL like "project in (AA, GC, GD)" or "project = AA" */
|
|
@@ -408,7 +412,7 @@ function maxGroupsForBudget(implicitCount) {
|
|
|
408
412
|
const queriesPerGroup = STANDARD_MEASURES + implicitCount;
|
|
409
413
|
return Math.floor(MAX_COUNT_QUERIES / queriesPerGroup);
|
|
410
414
|
}
|
|
411
|
-
async function handleSummary(jiraClient, jql, groupBy, compute) {
|
|
415
|
+
async function handleSummary(jiraClient, jql, groupBy, compute, groupLimit = DEFAULT_GROUP_LIMIT) {
|
|
412
416
|
const lines = [];
|
|
413
417
|
lines.push(`# Summary: ${jql}`);
|
|
414
418
|
lines.push(`As of ${formatDateShort(new Date())} — counts are exact (no sampling cap)`);
|
|
@@ -416,21 +420,24 @@ async function handleSummary(jiraClient, jql, groupBy, compute) {
|
|
|
416
420
|
const implicitDefs = buildImplicitMeasures(jiraClient.customFieldIds);
|
|
417
421
|
const neededImplicits = compute ? detectImplicitMeasures(compute, implicitDefs) : [];
|
|
418
422
|
const groupBudget = maxGroupsForBudget(neededImplicits.length);
|
|
419
|
-
|
|
423
|
+
// Effective group cap: user's preference vs API query budget (whichever is smaller)
|
|
424
|
+
const effectiveGroupCap = Math.min(groupLimit, groupBudget);
|
|
425
|
+
if (groupBy === 'project' && extractProjectKeys(jql).length > 0) {
|
|
426
|
+
// Fast path: project keys are explicit in JQL — no sampling needed
|
|
420
427
|
let keys = extractProjectKeys(jql);
|
|
421
|
-
|
|
422
|
-
throw new McpError(ErrorCode.InvalidParams, 'groupBy "project" requires project keys in JQL (e.g., project in (AA, GC))');
|
|
423
|
-
}
|
|
424
|
-
const capped = keys.length > groupBudget;
|
|
428
|
+
const capped = keys.length > effectiveGroupCap;
|
|
425
429
|
if (capped)
|
|
426
|
-
keys = keys.slice(0,
|
|
430
|
+
keys = keys.slice(0, effectiveGroupCap);
|
|
427
431
|
const remaining = removeProjectClause(jql);
|
|
428
432
|
const rows = await batchParallel(keys.map(k => () => buildCountRow(jiraClient, k, remaining ? `project = ${k} AND (${remaining})` : `project = ${k}`, neededImplicits, implicitDefs)), ROW_BATCH_SIZE);
|
|
429
433
|
rows.sort((a, b) => b.unresolved - a.unresolved);
|
|
430
434
|
lines.push('');
|
|
431
435
|
lines.push(renderSummaryTable(rows, compute));
|
|
432
436
|
if (capped) {
|
|
433
|
-
|
|
437
|
+
const reason = effectiveGroupCap < groupBudget
|
|
438
|
+
? `groupLimit=${effectiveGroupCap} — increase groupLimit to see more`
|
|
439
|
+
: `${MAX_COUNT_QUERIES}-query budget`;
|
|
440
|
+
lines.push(`*Capped at ${effectiveGroupCap} groups (${reason})*`);
|
|
434
441
|
}
|
|
435
442
|
}
|
|
436
443
|
else if (groupBy) {
|
|
@@ -439,23 +446,29 @@ async function handleSummary(jiraClient, jql, groupBy, compute) {
|
|
|
439
446
|
if (issues.length === 0) {
|
|
440
447
|
throw new McpError(ErrorCode.InvalidParams, `No issues matched JQL — cannot discover ${groupBy} values`);
|
|
441
448
|
}
|
|
442
|
-
const dims = extractDimensions(issues);
|
|
449
|
+
const dims = extractDimensions(issues, groupLimit);
|
|
443
450
|
const dim = dims.find(d => d.name === groupBy);
|
|
444
451
|
if (!dim || dim.values.length === 0) {
|
|
445
452
|
throw new McpError(ErrorCode.InvalidParams, `No ${groupBy} values found in sampled issues`);
|
|
446
453
|
}
|
|
447
|
-
// Cap groups to
|
|
448
|
-
const cappedValues = dim.values.slice(0,
|
|
454
|
+
// Cap groups to effective limit
|
|
455
|
+
const cappedValues = dim.values.slice(0, effectiveGroupCap);
|
|
449
456
|
const jqlClause = groupByJqlClause(groupBy, cappedValues);
|
|
450
457
|
const rows = await batchParallel(cappedValues.map((value, idx) => () => buildCountRow(jiraClient, value, `(${jql}) AND ${jqlClause[idx]}`, neededImplicits, implicitDefs)), ROW_BATCH_SIZE);
|
|
451
458
|
rows.sort((a, b) => b.unresolved - a.unresolved);
|
|
452
459
|
lines.push('');
|
|
453
460
|
lines.push(renderSummaryTable(rows, compute));
|
|
454
461
|
if (dim.count > cappedValues.length) {
|
|
455
|
-
const
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
462
|
+
const reasons = [];
|
|
463
|
+
if (cappedValues.length < dim.values.length) {
|
|
464
|
+
reasons.push(effectiveGroupCap < groupBudget
|
|
465
|
+
? `groupLimit=${effectiveGroupCap} — increase groupLimit to see more`
|
|
466
|
+
: `${MAX_COUNT_QUERIES}-query budget`);
|
|
467
|
+
}
|
|
468
|
+
else {
|
|
469
|
+
reasons.push(`from ${issues.length}-issue sample`);
|
|
470
|
+
}
|
|
471
|
+
lines.push(`*Showing top ${cappedValues.length} of ${dim.count} ${groupBy} values (${reasons.join(', ')})*`);
|
|
459
472
|
}
|
|
460
473
|
}
|
|
461
474
|
else {
|
|
@@ -464,6 +477,11 @@ async function handleSummary(jiraClient, jql, groupBy, compute) {
|
|
|
464
477
|
lines.push('');
|
|
465
478
|
lines.push(renderSummaryTable([row], compute));
|
|
466
479
|
}
|
|
480
|
+
// Workload interpretation hint — steer LLMs to decompose before reporting raw numbers
|
|
481
|
+
if (groupBy === 'assignee') {
|
|
482
|
+
lines.push('');
|
|
483
|
+
lines.push('*Interpretation tip: High open counts per person may include backlog, review, and future-planned work — not just active tasks. Before reporting workload, break down by status: `metrics: ["summary"], groupBy: "issuetype"` scoped to one assignee to distinguish active work from queued/backlog items.*');
|
|
484
|
+
}
|
|
467
485
|
return lines.join('\n');
|
|
468
486
|
}
|
|
469
487
|
/** Detect which implicit measures are referenced by compute expressions */
|
|
@@ -472,7 +490,7 @@ function detectImplicitMeasures(compute, implicitMeasureDefs) {
|
|
|
472
490
|
return Object.keys(implicitMeasureDefs).filter(name => refs.has(name));
|
|
473
491
|
}
|
|
474
492
|
/** Extract distinct dimension values from sampled issues */
|
|
475
|
-
export function extractDimensions(issues) {
|
|
493
|
+
export function extractDimensions(issues, groupLimit = DEFAULT_GROUP_LIMIT) {
|
|
476
494
|
const dims = [
|
|
477
495
|
{ name: 'project', extractor: i => i.key.split('-')[0] },
|
|
478
496
|
{ name: 'status', extractor: i => i.status },
|
|
@@ -486,10 +504,10 @@ export function extractDimensions(issues) {
|
|
|
486
504
|
const val = extractor(issue);
|
|
487
505
|
counts.set(val, (counts.get(val) || 0) + 1);
|
|
488
506
|
}
|
|
489
|
-
// Sort by count descending, cap at
|
|
507
|
+
// Sort by count descending, cap at groupLimit
|
|
490
508
|
const sorted = [...counts.entries()]
|
|
491
509
|
.sort((a, b) => b[1] - a[1])
|
|
492
|
-
.slice(0,
|
|
510
|
+
.slice(0, groupLimit);
|
|
493
511
|
return {
|
|
494
512
|
name,
|
|
495
513
|
values: sorted.map(([v]) => v),
|
|
@@ -521,7 +539,7 @@ export function renderCubeSetup(jql, sampleSize, dimensions) {
|
|
|
521
539
|
lines.push('');
|
|
522
540
|
lines.push(`## Suggested Cubes (budget: ${MAX_COUNT_QUERIES} queries)`);
|
|
523
541
|
for (const dim of dimensions) {
|
|
524
|
-
const groups = Math.min(dim.count,
|
|
542
|
+
const groups = Math.min(dim.count, DEFAULT_GROUP_LIMIT);
|
|
525
543
|
const queries = groups * STANDARD_MEASURES;
|
|
526
544
|
const estSeconds = Math.max(1, Math.round(queries / 12)); // ~12 parallel queries/sec
|
|
527
545
|
const withinBudget = queries <= MAX_COUNT_QUERIES;
|
|
@@ -596,6 +614,9 @@ export async function handleAnalysisRequest(jiraClient, request) {
|
|
|
596
614
|
if (args.compute && Array.isArray(args.compute) && args.compute.length > 0) {
|
|
597
615
|
compute = parseComputeList(args.compute);
|
|
598
616
|
}
|
|
617
|
+
// Parse groupLimit — soft cap on group/dimension values (no hard cap for summary metrics)
|
|
618
|
+
const rawGroupLimit = Number(args.groupLimit);
|
|
619
|
+
const groupLimit = rawGroupLimit > 0 ? rawGroupLimit : DEFAULT_GROUP_LIMIT;
|
|
599
620
|
// Cube setup — discover dimensions from sample, no issue fetching
|
|
600
621
|
if (hasCubeSetup) {
|
|
601
622
|
const cubeText = await handleCubeSetup(jiraClient, jql);
|
|
@@ -609,8 +630,8 @@ export async function handleAnalysisRequest(jiraClient, request) {
|
|
|
609
630
|
}
|
|
610
631
|
// If only summary requested, skip issue fetching entirely
|
|
611
632
|
if (hasSummary && fetchMetrics.length === 0) {
|
|
612
|
-
const summaryText = await handleSummary(jiraClient, jql, groupBy, compute);
|
|
613
|
-
const nextSteps = analysisNextSteps(jql, []);
|
|
633
|
+
const summaryText = await handleSummary(jiraClient, jql, groupBy, compute, groupLimit);
|
|
634
|
+
const nextSteps = analysisNextSteps(jql, [], false, groupBy);
|
|
614
635
|
return {
|
|
615
636
|
content: [{
|
|
616
637
|
type: 'text',
|
|
@@ -618,8 +639,7 @@ export async function handleAnalysisRequest(jiraClient, request) {
|
|
|
618
639
|
}],
|
|
619
640
|
};
|
|
620
641
|
}
|
|
621
|
-
const
|
|
622
|
-
const maxResults = Math.min(Number(args.maxResults) || DEFAULT_MAX, MAX_ISSUES);
|
|
642
|
+
const maxResults = Math.min(Number(args.maxResults) || MAX_ISSUES_DEFAULT, MAX_ISSUES_HARD);
|
|
623
643
|
// Fetch issues using cursor-based pagination (50 per page, Jira enhanced search API)
|
|
624
644
|
const allIssues = [];
|
|
625
645
|
const seen = new Set();
|
|
@@ -658,7 +678,7 @@ export async function handleAnalysisRequest(jiraClient, request) {
|
|
|
658
678
|
const lines = [];
|
|
659
679
|
// Summary first if requested alongside other metrics
|
|
660
680
|
if (hasSummary) {
|
|
661
|
-
const summaryText = await handleSummary(jiraClient, jql, groupBy, compute);
|
|
681
|
+
const summaryText = await handleSummary(jiraClient, jql, groupBy, compute, groupLimit);
|
|
662
682
|
lines.push(summaryText);
|
|
663
683
|
}
|
|
664
684
|
// Header for fetch-based metrics
|
|
@@ -668,7 +688,7 @@ export async function handleAnalysisRequest(jiraClient, request) {
|
|
|
668
688
|
lines.push(`# Detail: ${jql}`);
|
|
669
689
|
lines.push(`Analyzed ${allIssues.length} issues (as of ${formatDateShort(now)})`);
|
|
670
690
|
if (truncated) {
|
|
671
|
-
lines.push(`*Results capped at ${maxResults} issues —
|
|
691
|
+
lines.push(`*Results capped at ${maxResults} issues — distributions below are approximate. For exact counts, use metrics: ["summary"] with groupBy instead.*`);
|
|
672
692
|
}
|
|
673
693
|
// Render requested metrics
|
|
674
694
|
const renderers = {
|
|
@@ -676,7 +696,7 @@ export async function handleAnalysisRequest(jiraClient, request) {
|
|
|
676
696
|
time: () => renderTime(allIssues),
|
|
677
697
|
schedule: () => renderSchedule(allIssues, now),
|
|
678
698
|
cycle: () => renderCycle(allIssues, now),
|
|
679
|
-
distribution: () => renderDistribution(allIssues),
|
|
699
|
+
distribution: () => renderDistribution(allIssues, groupLimit),
|
|
680
700
|
};
|
|
681
701
|
for (const metric of fetchMetrics) {
|
|
682
702
|
lines.push('');
|
|
@@ -684,7 +704,7 @@ export async function handleAnalysisRequest(jiraClient, request) {
|
|
|
684
704
|
}
|
|
685
705
|
}
|
|
686
706
|
// Next steps
|
|
687
|
-
const nextSteps = analysisNextSteps(jql, allIssues.slice(0, 3).map(i => i.key), truncated);
|
|
707
|
+
const nextSteps = analysisNextSteps(jql, allIssues.slice(0, 3).map(i => i.key), truncated, groupBy);
|
|
688
708
|
lines.push(nextSteps);
|
|
689
709
|
return {
|
|
690
710
|
content: [{
|
|
@@ -187,7 +187,8 @@ async function handleGetIssue(jiraClient, args) {
|
|
|
187
187
|
// Get issue with requested expansions and catalog custom fields
|
|
188
188
|
const includeComments = expansionOptions.comments || false;
|
|
189
189
|
const includeAttachments = expansionOptions.attachments || false;
|
|
190
|
-
const
|
|
190
|
+
const includeHistory = expansionOptions.history || false;
|
|
191
|
+
const issue = await jiraClient.getIssue(args.issueKey, includeComments, includeAttachments, getCatalogFieldMeta(), includeHistory);
|
|
191
192
|
// Get transitions if requested
|
|
192
193
|
let transitions = undefined;
|
|
193
194
|
if (expansionOptions.transitions) {
|
|
@@ -165,6 +165,20 @@ export function renderIssue(issue, transitions) {
|
|
|
165
165
|
lines.push(`${cf.name} (${cf.type}): ${displayValue}`);
|
|
166
166
|
}
|
|
167
167
|
}
|
|
168
|
+
// Status history (if requested via expand: ["history"])
|
|
169
|
+
if (issue.statusHistory && issue.statusHistory.length > 0) {
|
|
170
|
+
lines.push('');
|
|
171
|
+
lines.push('Status History:');
|
|
172
|
+
for (const h of issue.statusHistory) {
|
|
173
|
+
lines.push(`${formatDate(h.date)}: ${h.from} → ${h.to} (by ${h.author})`);
|
|
174
|
+
}
|
|
175
|
+
// Show time in current status
|
|
176
|
+
const last = issue.statusHistory[issue.statusHistory.length - 1];
|
|
177
|
+
const daysSince = Math.floor((Date.now() - new Date(last.date).getTime()) / (1000 * 60 * 60 * 24));
|
|
178
|
+
if (daysSince > 0) {
|
|
179
|
+
lines.push(`*In "${last.to}" for ${daysSince} days*`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
168
182
|
// Available transitions
|
|
169
183
|
if (transitions && transitions.length > 0) {
|
|
170
184
|
lines.push('');
|
|
@@ -326,7 +326,7 @@ export const toolSchemas = {
|
|
|
326
326
|
},
|
|
327
327
|
analyze_jira_issues: {
|
|
328
328
|
name: 'analyze_jira_issues',
|
|
329
|
-
description: 'Compute project metrics over issues selected by JQL.
|
|
329
|
+
description: 'Compute project metrics over issues selected by JQL. For counting and breakdown questions ("how many by status/assignee/priority"), use metrics: ["summary"] with groupBy — this gives exact counts with no issue cap. Use detail metrics (points, time, schedule, cycle, distribution) only when you need per-issue analysis; these are capped at maxResults issues. Always prefer this tool over manage_jira_filter or manage_jira_project for quantitative questions. Read jira://analysis/recipes for composition patterns.',
|
|
330
330
|
inputSchema: {
|
|
331
331
|
type: 'object',
|
|
332
332
|
properties: {
|
|
@@ -340,12 +340,12 @@ export const toolSchemas = {
|
|
|
340
340
|
type: 'string',
|
|
341
341
|
enum: ['summary', 'points', 'time', 'schedule', 'cycle', 'distribution', 'cube_setup'],
|
|
342
342
|
},
|
|
343
|
-
description: 'Which metric groups to compute. summary = exact
|
|
343
|
+
description: 'Which metric groups to compute. summary = exact counts via count API (no issue cap, fastest) — use with groupBy for "how many by assignee/status/priority" questions. distribution = approximate counts from fetched issues (capped by maxResults — use summary + groupBy instead when you need exact counts). cube_setup = discover dimensions before cube queries. points = earned value/SPI. time = effort estimates. schedule = overdue/risk. cycle = lead time/throughput. Default: all detail metrics. For counting/breakdown questions, always prefer summary + groupBy over distribution.',
|
|
344
344
|
},
|
|
345
345
|
groupBy: {
|
|
346
346
|
type: 'string',
|
|
347
347
|
enum: ['project', 'assignee', 'priority', 'issuetype'],
|
|
348
|
-
description: 'Split
|
|
348
|
+
description: 'Split counts by this dimension — produces a breakdown table. Use with metrics: ["summary"] for exact counts. This is the correct approach for "how many issues per assignee/priority/type" questions. "project" produces a per-project comparison.',
|
|
349
349
|
},
|
|
350
350
|
compute: {
|
|
351
351
|
type: 'array',
|
|
@@ -353,6 +353,11 @@ export const toolSchemas = {
|
|
|
353
353
|
description: 'Computed columns for cube execute. Each entry: "name = expr". Arithmetic (+,-,*,/), comparisons (>,<,>=,<=,==,!=). Column refs: total, open, overdue, high, created_7d, resolved_7d. Implicit measures resolved lazily: bugs, unassigned, no_due_date, no_estimate, no_start_date, no_labels, blocked, stale (untouched 60d+), stale_status (stuck in status 30d+), backlog_rot (undated+unassigned+untouched 60d+). Max 5 expressions. Example: ["bug_pct = bugs / total * 100", "rot_pct = backlog_rot / open * 100"].',
|
|
354
354
|
maxItems: 5,
|
|
355
355
|
},
|
|
356
|
+
groupLimit: {
|
|
357
|
+
type: 'integer',
|
|
358
|
+
description: 'Max groups/dimension values to show (default 20). Applies to summary groupBy rows and distribution breakdowns. No hard cap for summary metrics — increase freely for full visibility. For detail metrics the issue fetch is separately capped by maxResults.',
|
|
359
|
+
default: 20,
|
|
360
|
+
},
|
|
356
361
|
maxResults: {
|
|
357
362
|
type: 'integer',
|
|
358
363
|
description: 'Max issues to fetch for detail metrics (default 100, max 500). Does not apply to summary (which uses count API).',
|
|
@@ -125,14 +125,24 @@ export function boardNextSteps(operation, boardId) {
|
|
|
125
125
|
}
|
|
126
126
|
return steps.length > 0 ? formatSteps(steps) : '';
|
|
127
127
|
}
|
|
128
|
-
export function analysisNextSteps(jql, issueKeys, truncated = false) {
|
|
128
|
+
export function analysisNextSteps(jql, issueKeys, truncated = false, groupBy) {
|
|
129
129
|
const steps = [];
|
|
130
130
|
if (issueKeys.length > 0) {
|
|
131
131
|
steps.push({ description: 'Get details on a specific issue', tool: 'manage_jira_issue', example: { operation: 'get', issueKey: issueKeys[0] } });
|
|
132
132
|
}
|
|
133
|
+
// Contextual cross-dimension suggestions based on groupBy
|
|
134
|
+
if (groupBy === 'assignee') {
|
|
135
|
+
steps.push({ description: 'Break down a person\'s workload by status (active vs backlog vs review)', tool: 'analyze_jira_issues', example: { jql: `${jql} AND assignee = "<name>"`, metrics: ['summary'], groupBy: 'issuetype' } }, { description: 'Compare workload health with computed metrics', tool: 'analyze_jira_issues', example: { jql, metrics: ['summary'], groupBy: 'assignee', compute: ['overdue_pct = overdue / open * 100', 'stale_pct = stale / open * 100'] } });
|
|
136
|
+
}
|
|
137
|
+
else if (groupBy === 'issuetype' || groupBy === 'priority') {
|
|
138
|
+
steps.push({ description: 'See who owns these issues', tool: 'analyze_jira_issues', example: { jql, metrics: ['summary'], groupBy: 'assignee' } });
|
|
139
|
+
}
|
|
140
|
+
else if (groupBy === 'project') {
|
|
141
|
+
steps.push({ description: 'Drill into a project by assignee', tool: 'analyze_jira_issues', example: { jql: `${jql} AND project = <KEY>`, metrics: ['summary'], groupBy: 'assignee' } });
|
|
142
|
+
}
|
|
133
143
|
steps.push({ description: 'Discover dimensions for cube analysis', tool: 'analyze_jira_issues', example: { jql, metrics: ['cube_setup'] } }, { description: 'Add computed columns', tool: 'analyze_jira_issues', example: { jql, metrics: ['summary'], groupBy: 'project', compute: ['bug_pct = bugs / total * 100'] } }, { description: 'Narrow the analysis with refined JQL', tool: 'analyze_jira_issues', example: { jql: `${jql} AND priority = High` } }, { description: 'View the full issue list', tool: 'manage_jira_filter', example: { operation: 'execute_jql', jql } });
|
|
134
144
|
if (truncated) {
|
|
135
|
-
steps.push({ description: '
|
|
145
|
+
steps.push({ description: 'Distribution counts above are approximate (issue cap hit). For exact breakdowns use summary + groupBy', tool: 'analyze_jira_issues', example: { jql, metrics: ['summary'], groupBy: 'assignee' } }, { description: 'Or narrow JQL for precise detail metrics', tool: 'analyze_jira_issues', example: { jql: `${jql} AND assignee = currentUser()`, metrics: ['cycle'] } });
|
|
136
146
|
}
|
|
137
147
|
return formatSteps(steps) + '\n- Read `jira://analysis/recipes` for data cube patterns and compute DSL examples';
|
|
138
148
|
}
|
package/package.json
CHANGED