jirametrics 2.26.1 → 2.27

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 960ce78e94ec2c7e87875bd0dc6db2c16b11cee0e4c4dd5a22d8bf3bab1c893d
4
- data.tar.gz: f5b2d9aac4d9059654ad7a569d477b0b689c8d22f30760867aec9e13b3f1db61
3
+ metadata.gz: c87212eebf4e22145b8e66741174658bffc3cb1e47bd45bfe3f7590586a42200
4
+ data.tar.gz: 00fb6c4375e7e6858359fa754af42c697028198dedd2ae8100c98d0564f59db8
5
5
  SHA512:
6
- metadata.gz: 727e2f5b3e57e937eacd05d868ff1f7a75e16210f0e46e75117a88e56287fb765238753710289d9fb2f09b38d6e940f47d0ae2689be313b1282238727ddae0eb
7
- data.tar.gz: d60771157926c74a7ac45d12ea023eef1808b4ea027f8edd6fba6108a79b2f0df8bdb054bf58049cc8a3f3de44e4926dd0af0a40b95b25fb07ed1391d829418b
6
+ metadata.gz: aad3747a78dcf530df02244720c676632816a30688045d95c9a701ba946a5573f81a2e3955c5035e745fb197f5c6cff58491a63d35db68b3abf35817c78dc0b8
7
+ data.tar.gz: fcf17a8c2a4c91e4de45ff15150d1de9147c890d9e80354b490bec2b27fb17ef2b20e7bc6073c503e88fd0ad188c414002d8312500b22dd5207b043d3c253be7
@@ -377,7 +377,7 @@ class ChartBase
377
377
  end
378
378
 
379
379
  def seam_start type = 'chart'
380
- "\n<!-- seam-start | chart#{@@chart_counter} | #{self.class} | #{header_text} | #{type} -->"
380
+ "\n<!-- seam-start | chart#{@@chart_counter} | #{self.class} | #{header_text} | #{type} -->\n"
381
381
  end
382
382
 
383
383
  def seam_end type = 'chart'
@@ -69,6 +69,9 @@ class CumulativeFlowDiagram < ChartBase
69
69
  When the cursor is near the right edge and that point falls outside the visible date range,
70
70
  CT and TP cannot be calculated and are hidden; only WIP is shown.
71
71
  </div>
72
+ <div class="p">
73
+ See also: This article on [how to read a CFD](https://blog.mikebowler.ca/2026/03/27/cumulative-flow-diagram/).
74
+ </div>
72
75
  HTML
73
76
  instance_eval(&block)
74
77
  end
@@ -10,7 +10,7 @@
10
10
  class Exporter
11
11
  def aggregated_project name:, project_names:, settings: {}
12
12
  project name: name do
13
- puts name
13
+ file_system.log name
14
14
  file_prefix name
15
15
  self.settings.merge! stringify_keys(settings)
16
16
 
@@ -9,7 +9,7 @@ class Exporter
9
9
  show_experimental_charts: false, github_repos: nil
10
10
  exporter = self
11
11
  project name: name do
12
- puts name
12
+ file_system.log name
13
13
  file_prefix file_prefix
14
14
 
15
15
  self.anonymize if anonymize
@@ -35,7 +35,7 @@ class Exporter
35
35
  download do
36
36
  self.rolling_date_count(rolling_date_count) if rolling_date_count
37
37
  self.no_earlier_than(no_earlier_than) if no_earlier_than
38
- github_repo github_repos if github_repos
38
+ github_repo *github_repos if github_repos
39
39
  end
40
40
 
41
41
  issues.reject! do |issue|
@@ -3,13 +3,14 @@
3
3
  require 'json'
4
4
 
5
5
  class FileSystem
6
- attr_accessor :logfile, :logfile_name
6
+ attr_accessor :logfile, :logfile_name, :log_only
7
7
 
8
8
  def initialize
9
9
  # In almost all cases, this will be immediately replaced in the Exporter
10
10
  # but if we fail before we get that far, this will at least let a useful
11
11
  # error show up on the console.
12
12
  @logfile = $stdout
13
+ @log_only = false
13
14
  end
14
15
 
15
16
  # Effectively the same as File.read except it forces the encoding to UTF-8
@@ -59,7 +60,7 @@ class FileSystem
59
60
 
60
61
  logfile.puts message
61
62
  logfile.puts more if more
62
- return unless also_write_to_stderr
63
+ return if log_only || !also_write_to_stderr
63
64
 
64
65
  # Obscure edge-case where we're trying to log something before logging is even
65
66
  # set up. Quick escape here so that we don't dump the error twice.
@@ -70,23 +71,29 @@ class FileSystem
70
71
 
71
72
  def log_start message
72
73
  logfile.puts message
73
- return if logfile == $stdout
74
+ return if log_only || logfile == $stdout
74
75
 
75
76
  $stderr.print message
76
77
  $stderr.flush
77
78
  end
78
79
 
79
80
  def start_progress
81
+ return if log_only
82
+
80
83
  $stderr.print ' '
81
84
  $stderr.flush
82
85
  end
83
86
 
84
87
  def progress_dot
88
+ return if log_only
89
+
85
90
  $stderr.print '.'
86
91
  $stderr.flush
87
92
  end
88
93
 
89
94
  def end_progress
95
+ return if log_only
96
+
90
97
  $stderr.puts '' # rubocop:disable Style/StderrPuts
91
98
  end
92
99
 
@@ -64,9 +64,10 @@ class GithubGateway
64
64
  raw_pr['body']
65
65
  ]
66
66
 
67
- sources.compact
68
- .flat_map { |s| s.scan(@issue_key_pattern) }
69
- .uniq
67
+ keys = sources.compact.flat_map { |s| s.scan(@issue_key_pattern) }.uniq
68
+ return keys unless keys.empty?
69
+
70
+ commit_messages_for(raw_pr['number']).flat_map { |msg| msg.scan(@issue_key_pattern) }.uniq
70
71
  end
71
72
 
72
73
  def extract_reviews raw_reviews
@@ -83,11 +84,19 @@ class GithubGateway
83
84
 
84
85
  private
85
86
 
87
+ def commit_messages_for pr_number
88
+ args = ['pr', 'view', pr_number.to_s, '--json', 'commits', '--repo', @repo]
89
+ result = run_command(args)
90
+ (result['commits'] || []).flat_map do |commit|
91
+ [commit['messageHeadline'], commit['messageBody']].compact
92
+ end
93
+ end
94
+
86
95
  def build_issue_key_pattern
87
96
  return nil if @project_keys.empty?
88
97
 
89
98
  keys_pattern = @project_keys.map { |k| Regexp.escape(k) }.join('|')
90
- Regexp.new("\\b(?:#{keys_pattern})-\\d+\\b")
99
+ Regexp.new("\\b(?:#{keys_pattern})-\\d+(?![A-Za-z0-9])")
91
100
  end
92
101
 
93
102
  def run_command args
@@ -28,15 +28,20 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'), {
28
28
  max: "<%= (date_range.end + 1).to_s %>"
29
29
  },
30
30
  y: {
31
+ min: 0,
32
+ max: <%= (@highest_y_value * 1.1).ceil %>,
31
33
  scaleLabel: {
32
- display: true,
33
- min: 0,
34
- max: <%= @highest_y_value %>
34
+ display: true
35
35
  },
36
36
  <%= render_axis_title :y %>
37
37
  grid: {
38
38
  color: <%= CssVariable['--grid-line-color'].to_json %>
39
39
  },
40
+ ticks: {
41
+ callback: function(value, index, ticks) {
42
+ return index === ticks.length - 1 ? null : value;
43
+ }
44
+ }
40
45
  }
41
46
  },
42
47
  plugins: {
@@ -0,0 +1,305 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'mcp'
4
+ require 'mcp/server/transports/stdio_transport'
5
+
6
+ class McpServer
7
+ def initialize projects:, timezone_offset: '+00:00'
8
+ @projects = projects
9
+ @timezone_offset = timezone_offset
10
+ end
11
+
12
+ def run
13
+ server = MCP::Server.new(
14
+ name: 'jirametrics',
15
+ version: Gem.loaded_specs['jirametrics']&.version&.to_s || '0.0.0',
16
+ tools: [AgingWorkTool, CompletedWorkTool, NotYetStartedTool],
17
+ server_context: { projects: @projects, timezone_offset: @timezone_offset }
18
+ )
19
+
20
+ transport = MCP::Server::Transports::StdioTransport.new(server)
21
+ transport.open
22
+ end
23
+
24
+ HISTORY_FILTER_SCHEMA = {
25
+ history_field: {
26
+ type: 'string',
27
+ description: 'When combined with history_value, only return issues where this field ever had that value ' \
28
+ '(e.g. "priority", "status"). Both history_field and history_value must be provided together.'
29
+ },
30
+ history_value: {
31
+ type: 'string',
32
+ description: 'The value to look for in the change history of history_field (e.g. "Highest", "Done").'
33
+ },
34
+ ever_blocked: {
35
+ type: 'boolean',
36
+ description: 'When true, only return issues that were ever blocked. Blocked includes flagged items, ' \
37
+ 'issues in blocked statuses, and blocking issue links.'
38
+ },
39
+ ever_stalled: {
40
+ type: 'boolean',
41
+ description: 'When true, only return issues that were ever stalled. Stalled means the issue sat ' \
42
+ 'inactive for longer than the stalled threshold, or entered a stalled status.'
43
+ },
44
+ currently_blocked: {
45
+ type: 'boolean',
46
+ description: 'When true, only return issues that are currently blocked (as of the data end date).'
47
+ },
48
+ currently_stalled: {
49
+ type: 'boolean',
50
+ description: 'When true, only return issues that are currently stalled (as of the data end date).'
51
+ }
52
+ }.freeze
53
+
54
+ def self.flow_efficiency_percent issue, end_time
55
+ active_time, total_time = issue.flow_efficiency_numbers(end_time: end_time)
56
+ total_time.positive? ? (active_time / total_time * 100).round(1) : nil
57
+ end
58
+
59
+ def self.matches_blocked_stalled?(bsc, ever_blocked, ever_stalled, currently_blocked, currently_stalled)
60
+ return false if ever_blocked && bsc.none?(&:blocked?)
61
+ return false if ever_stalled && bsc.none?(&:stalled?)
62
+ return false if currently_blocked && !bsc.last&.blocked?
63
+ return false if currently_stalled && !bsc.last&.stalled?
64
+
65
+ true
66
+ end
67
+
68
+ def self.matches_history?(issue, end_time, history_field, history_value,
69
+ ever_blocked, ever_stalled, currently_blocked, currently_stalled)
70
+ return false if history_field && history_value &&
71
+ issue.changes.none? { |c| c.field == history_field && c.value == history_value }
72
+
73
+ if ever_blocked || ever_stalled || currently_blocked || currently_stalled
74
+ bsc = issue.blocked_stalled_changes(end_time: end_time)
75
+ return false unless matches_blocked_stalled?(bsc, ever_blocked, ever_stalled,
76
+ currently_blocked, currently_stalled)
77
+ end
78
+
79
+ true
80
+ end
81
+
82
+ class AgingWorkTool < MCP::Tool
83
+ tool_name 'aging_work'
84
+ description 'Returns all issues that have been started but not yet completed (work in progress), ' \
85
+ 'sorted from oldest to newest. Age is the number of days since the issue was started.'
86
+
87
+ input_schema(
88
+ type: 'object',
89
+ properties: {
90
+ min_age_days: {
91
+ type: 'integer',
92
+ description: 'Only return issues at least this many days old. Omit to return all ages.'
93
+ },
94
+ project: {
95
+ type: 'string',
96
+ description: 'Only return issues from this project name. Omit to return all projects.'
97
+ },
98
+ **HISTORY_FILTER_SCHEMA
99
+ }
100
+ )
101
+
102
+ def self.call(server_context:, min_age_days: nil, project: nil,
103
+ history_field: nil, history_value: nil, ever_blocked: nil, ever_stalled: nil,
104
+ currently_blocked: nil, currently_stalled: nil)
105
+ rows = []
106
+
107
+ server_context[:projects].each do |project_name, project_data|
108
+ next if project && project_name != project
109
+
110
+ today = project_data[:today]
111
+ project_data[:issues].each do |issue|
112
+ started, stopped = issue.started_stopped_times
113
+ next unless started && !stopped
114
+
115
+ age = (today - started.to_date).to_i + 1
116
+ next if min_age_days && age < min_age_days
117
+ unless McpServer.matches_history?(issue, project_data[:end_time],
118
+ history_field, history_value, ever_blocked, ever_stalled,
119
+ currently_blocked, currently_stalled)
120
+ next
121
+ end
122
+
123
+ rows << {
124
+ key: issue.key,
125
+ summary: issue.summary,
126
+ status: issue.status.name,
127
+ type: issue.type,
128
+ age_days: age,
129
+ flow_efficiency: McpServer.flow_efficiency_percent(issue, project_data[:end_time]),
130
+ project: project_name
131
+ }
132
+ end
133
+ end
134
+
135
+ rows.sort_by! { |r| -r[:age_days] }
136
+
137
+ if rows.empty?
138
+ text = 'No aging work found.'
139
+ else
140
+ lines = rows.map do |r|
141
+ fe = r[:flow_efficiency] ? " | FE: #{r[:flow_efficiency]}%" : ''
142
+ "#{r[:key]} | #{r[:project]} | #{r[:type]} | #{r[:status]} | Age: #{r[:age_days]}d#{fe} | #{r[:summary]}"
143
+ end
144
+ text = lines.join("\n")
145
+ end
146
+
147
+ MCP::Tool::Response.new([{ type: 'text', text: text }])
148
+ end
149
+ end
150
+
151
+ class CompletedWorkTool < MCP::Tool
152
+ tool_name 'completed_work'
153
+ description 'Returns issues that have been completed, sorted most recently completed first. ' \
154
+ 'Includes cycle time (days from start to completion).'
155
+
156
+ input_schema(
157
+ type: 'object',
158
+ properties: {
159
+ days_back: {
160
+ type: 'integer',
161
+ description: 'Only return issues completed within this many days of the data end date. Omit to return all.'
162
+ },
163
+ project: {
164
+ type: 'string',
165
+ description: 'Only return issues from this project name. Omit to return all projects.'
166
+ },
167
+ completed_status: {
168
+ type: 'string',
169
+ description: 'Only return issues whose status at completion matches this value (e.g. "Cancelled", "Done").'
170
+ },
171
+ completed_resolution: {
172
+ type: 'string',
173
+ description: 'Only return issues whose resolution at completion matches this value (e.g. "Won\'t Do").'
174
+ },
175
+ **HISTORY_FILTER_SCHEMA
176
+ }
177
+ )
178
+
179
+ def self.build_row issue, project_name, started, stopped, cutoff, completed_status, completed_resolution,
180
+ end_time, history_field, history_value, ever_blocked, ever_stalled,
181
+ currently_blocked, currently_stalled
182
+ completed_date = stopped.to_date
183
+ return nil if cutoff && completed_date < cutoff
184
+
185
+ status_at_done, resolution_at_done = issue.status_resolution_at_done
186
+ return nil if completed_status && status_at_done&.name != completed_status
187
+ return nil if completed_resolution && completed_resolution != resolution_at_done
188
+ return nil unless McpServer.matches_history?(issue, end_time,
189
+ history_field, history_value, ever_blocked, ever_stalled,
190
+ currently_blocked, currently_stalled)
191
+
192
+ cycle_time = started ? (completed_date - started.to_date).to_i + 1 : nil
193
+ {
194
+ key: issue.key,
195
+ summary: issue.summary,
196
+ type: issue.type,
197
+ completed_date: completed_date,
198
+ cycle_time_days: cycle_time,
199
+ flow_efficiency: McpServer.flow_efficiency_percent(issue, stopped),
200
+ status_at_done: status_at_done&.name,
201
+ resolution_at_done: resolution_at_done,
202
+ project: project_name
203
+ }
204
+ end
205
+
206
+ def self.call(server_context:, days_back: nil, project: nil,
207
+ completed_status: nil, completed_resolution: nil,
208
+ history_field: nil, history_value: nil, ever_blocked: nil, ever_stalled: nil,
209
+ currently_blocked: nil, currently_stalled: nil)
210
+ rows = []
211
+
212
+ server_context[:projects].each do |project_name, project_data|
213
+ next if project && project_name != project
214
+
215
+ today = project_data[:today]
216
+ cutoff = today - days_back if days_back
217
+
218
+ project_data[:issues].each do |issue|
219
+ started, stopped = issue.started_stopped_times
220
+ next unless stopped
221
+
222
+ row = build_row(issue, project_name, started, stopped, cutoff, completed_status, completed_resolution,
223
+ project_data[:end_time], history_field, history_value, ever_blocked, ever_stalled,
224
+ currently_blocked, currently_stalled)
225
+ rows << row if row
226
+ end
227
+ end
228
+
229
+ rows.sort_by! { |r| -r[:completed_date].to_time.to_i }
230
+
231
+ if rows.empty?
232
+ text = 'No completed work found.'
233
+ else
234
+ lines = rows.map do |r|
235
+ ct = r[:cycle_time_days] ? "#{r[:cycle_time_days]}d" : 'unknown'
236
+ fe = r[:flow_efficiency] ? " | FE: #{r[:flow_efficiency]}%" : ''
237
+ completion = [r[:status_at_done], r[:resolution_at_done]].compact.join(' / ')
238
+ "#{r[:key]} | #{r[:project]} | #{r[:type]} | #{r[:completed_date]} | " \
239
+ "Cycle time: #{ct}#{fe} | #{completion} | #{r[:summary]}"
240
+ end
241
+ text = lines.join("\n")
242
+ end
243
+
244
+ MCP::Tool::Response.new([{ type: 'text', text: text }])
245
+ end
246
+ end
247
+
248
+ class NotYetStartedTool < MCP::Tool
249
+ tool_name 'not_yet_started'
250
+ description 'Returns issues that have not yet been started (backlog items), sorted by creation date oldest first.'
251
+
252
+ input_schema(
253
+ type: 'object',
254
+ properties: {
255
+ project: {
256
+ type: 'string',
257
+ description: 'Only return issues from this project name. Omit to return all projects.'
258
+ },
259
+ **HISTORY_FILTER_SCHEMA
260
+ }
261
+ )
262
+
263
+ def self.call(server_context:, project: nil,
264
+ history_field: nil, history_value: nil, ever_blocked: nil, ever_stalled: nil,
265
+ currently_blocked: nil, currently_stalled: nil)
266
+ rows = []
267
+
268
+ server_context[:projects].each do |project_name, project_data|
269
+ next if project && project_name != project
270
+
271
+ project_data[:issues].each do |issue|
272
+ started, stopped = issue.started_stopped_times
273
+ next if started || stopped
274
+ unless McpServer.matches_history?(issue, project_data[:end_time],
275
+ history_field, history_value, ever_blocked, ever_stalled,
276
+ currently_blocked, currently_stalled)
277
+ next
278
+ end
279
+
280
+ rows << {
281
+ key: issue.key,
282
+ summary: issue.summary,
283
+ status: issue.status.name,
284
+ type: issue.type,
285
+ created: issue.created.to_date,
286
+ project: project_name
287
+ }
288
+ end
289
+ end
290
+
291
+ rows.sort_by! { |r| r[:created] }
292
+
293
+ if rows.empty?
294
+ text = 'No unstarted work found.'
295
+ else
296
+ lines = rows.map do |r|
297
+ "#{r[:key]} | #{r[:project]} | #{r[:type]} | #{r[:status]} | Created: #{r[:created]} | #{r[:summary]}"
298
+ end
299
+ text = lines.join("\n")
300
+ end
301
+
302
+ MCP::Tool::Response.new([{ type: 'text', text: text }])
303
+ end
304
+ end
305
+ end
@@ -19,8 +19,8 @@ class PullRequestCycleTimeHistogram < TimeBasedHistogram
19
19
  HTML
20
20
 
21
21
  init_configuration_block(block) do
22
- grouping_rules do |pull_request, _rule|
23
- rules.label = pull_request.repo
22
+ grouping_rules do |pull_request, rule|
23
+ rule.label = pull_request.repo
24
24
  end
25
25
  end
26
26
  end
@@ -18,8 +18,8 @@ class PullRequestCycleTimeScatterplot < TimeBasedScatterplot
18
18
  HTML
19
19
 
20
20
  init_configuration_block(block) do
21
- grouping_rules do |pull_request, _rule|
22
- rules.label = pull_request.repo
21
+ grouping_rules do |pull_request, rule|
22
+ rule.label = pull_request.repo
23
23
  end
24
24
  end
25
25
  end
@@ -48,8 +48,15 @@ class PullRequestCycleTimeScatterplot < TimeBasedScatterplot
48
48
  end
49
49
 
50
50
  def y_value pull_request
51
- divisor = { minutes: 60, hours: 3600, days: 86_400 }[@cycletime_unit]
52
- ((pull_request.closed_at - pull_request.opened_at) / divisor).round
51
+ if @cycletime_unit == :days
52
+ tz = timezone_offset || '+00:00'
53
+ opened = pull_request.opened_at.getlocal(tz).to_date
54
+ closed = pull_request.closed_at.getlocal(tz).to_date
55
+ (closed - opened).to_i + 1
56
+ else
57
+ divisor = { minutes: 60, hours: 3600 }[@cycletime_unit]
58
+ ((pull_request.closed_at - pull_request.opened_at) / divisor).round
59
+ end
53
60
  end
54
61
 
55
62
  def label_cycletime value
@@ -62,7 +69,8 @@ class PullRequestCycleTimeScatterplot < TimeBasedScatterplot
62
69
 
63
70
  def title_value pull_request, rules: nil
64
71
  age_label = label_cycletime y_value(pull_request)
65
- "#{pull_request.title} | #{rules.label} | Age:#{age_label}#{lines_changed_text(pull_request)}"
72
+ keys = pull_request.issue_keys.join(', ')
73
+ "#{keys} | #{pull_request.title} | #{rules.label} | Age:#{age_label}#{lines_changed_text(pull_request)}"
66
74
  end
67
75
 
68
76
  def lines_changed_text pull_request
@@ -60,6 +60,11 @@ class Stitcher < HtmlGenerator
60
60
  if matches[:seam] == 'start'
61
61
  content = +''
62
62
  else
63
+ if content.nil? || content.strip.empty?
64
+ file_system.warning "Seam found with no content in #{filename.inspect}: " \
65
+ "id=#{matches[:id].strip.inspect}, class=#{matches[:clazz].strip.inspect}, " \
66
+ "title=#{matches[:title].strip.inspect}"
67
+ end
63
68
  @all_stitches << Stitcher::StitchContent.new(
64
69
  file: filename, title: matches[:title], type: matches[:type], content: content
65
70
  )
data/lib/jirametrics.rb CHANGED
@@ -52,6 +52,34 @@ class JiraMetrics < Thor
52
52
  Exporter.instance.info(key, name_filter: options[:name] || '*')
53
53
  end
54
54
 
55
+ option :config
56
+ option :name
57
+ desc 'mcp', 'Start in MCP (Model Context Protocol) server mode'
58
+ def mcp
59
+ load_config options[:config]
60
+ require 'jirametrics/mcp_server'
61
+
62
+ Exporter.instance.file_system.log_only = true
63
+
64
+ projects = {}
65
+ Exporter.instance.each_project_config(name_filter: options[:name] || '*') do |project|
66
+ project.evaluate_next_level
67
+ project.run load_only: true
68
+ projects[project.name || 'default'] = {
69
+ issues: project.issues,
70
+ today: project.time_range.end.to_date,
71
+ end_time: project.time_range.end
72
+ }
73
+ rescue StandardError => e
74
+ next if e.message.start_with? 'This is an aggregated project'
75
+ next if e.message.start_with? 'No data found'
76
+
77
+ raise
78
+ end
79
+
80
+ McpServer.new(projects: projects, timezone_offset: Exporter.instance.timezone_offset).run
81
+ end
82
+
55
83
  option :config
56
84
  desc 'stitch', 'Dump information about one issue'
57
85
  def stitch stitch_file = 'stitcher.erb'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jirametrics
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.26.1
4
+ version: '2.27'
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mike Bowler
@@ -9,6 +9,34 @@ bindir: bin
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: mutant-rspec
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '0'
19
+ type: :development
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: mcp
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
12
40
  - !ruby/object:Gem::Dependency
13
41
  name: random-word
14
42
  requirement: !ruby/object:Gem::Requirement
@@ -131,6 +159,7 @@ files:
131
159
  - lib/jirametrics/issue_link.rb
132
160
  - lib/jirametrics/issue_printer.rb
133
161
  - lib/jirametrics/jira_gateway.rb
162
+ - lib/jirametrics/mcp_server.rb
134
163
  - lib/jirametrics/project_config.rb
135
164
  - lib/jirametrics/pull_request.rb
136
165
  - lib/jirametrics/pull_request_cycle_time_histogram.rb