code_healer 0.1.18 → 0.1.20
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 +4 -4
- data/CHANGELOG.md +29 -1
- data/bin/start_claude_session +16 -0
- data/code_healer.gemspec +2 -0
- data/config/code_healer.yml.example +33 -1
- data/lib/code_healer/business_context_manager.rb +57 -3
- data/lib/code_healer/claude_code_evolution_handler.rb +16 -14
- data/lib/code_healer/config_manager.rb +53 -1
- data/lib/code_healer/evolution_job.rb +1 -1
- data/lib/code_healer/healing_job.rb +45 -1
- data/lib/code_healer/mcp_server.rb +222 -2
- data/lib/code_healer/mcp_tools.rb +390 -5
- data/lib/code_healer/pull_request_creator.rb +1 -1
- data/lib/code_healer/setup.rb +155 -4
- data/lib/code_healer/version.rb +1 -1
- data/lib/code_healer.rb +4 -1
- metadata +23 -2
@@ -1,3 +1,6 @@
|
|
1
|
+
require 'httparty'
|
2
|
+
require 'cgi'
|
3
|
+
|
1
4
|
module CodeHealer
|
2
5
|
# Tool for analyzing errors with context
|
3
6
|
class ErrorAnalysisTool < MCP::Tool
|
@@ -1522,9 +1525,14 @@ module CodeHealer
|
|
1522
1525
|
return { error: "Ticket ID required" } unless ticket_id
|
1523
1526
|
|
1524
1527
|
begin
|
1525
|
-
#
|
1526
|
-
if
|
1527
|
-
ticket_data =
|
1528
|
+
# Try real JIRA API first, fallback to simulation if it fails
|
1529
|
+
if jira_api_available?
|
1530
|
+
ticket_data = get_real_jira_ticket(ticket_id)
|
1531
|
+
|
1532
|
+
if ticket_data[:error]
|
1533
|
+
puts "⚠️ Real JIRA API failed, falling back to simulation: #{ticket_data[:error]}"
|
1534
|
+
ticket_data = simulate_jira_ticket(ticket_id)
|
1535
|
+
end
|
1528
1536
|
|
1529
1537
|
{
|
1530
1538
|
ticket_id: ticket_id,
|
@@ -1590,8 +1598,13 @@ module CodeHealer
|
|
1590
1598
|
return { error: "Project key required" } unless project_key
|
1591
1599
|
|
1592
1600
|
begin
|
1593
|
-
if
|
1594
|
-
project_data =
|
1601
|
+
if jira_api_available?
|
1602
|
+
project_data = get_real_jira_project(project_key)
|
1603
|
+
|
1604
|
+
if project_data[:error]
|
1605
|
+
puts "⚠️ Real JIRA API failed, falling back to simulation: #{project_data[:error]}"
|
1606
|
+
project_data = simulate_jira_project(project_key)
|
1607
|
+
end
|
1595
1608
|
|
1596
1609
|
{
|
1597
1610
|
project_key: project_key,
|
@@ -1845,6 +1858,378 @@ module CodeHealer
|
|
1845
1858
|
priority_distribution: { "High": 20, "Medium": 60, "Low": 20 }
|
1846
1859
|
}
|
1847
1860
|
end
|
1861
|
+
|
1862
|
+
# Real JIRA API integration methods
|
1863
|
+
def self.jira_api_available?
|
1864
|
+
# Check if JIRA configuration is available
|
1865
|
+
ENV['JIRA_URL'] && ENV['JIRA_USERNAME'] && ENV['JIRA_API_TOKEN']
|
1866
|
+
end
|
1867
|
+
|
1868
|
+
def self.jira_api_call(endpoint, method = :get, body = nil)
|
1869
|
+
return { error: "JIRA configuration not available" } unless jira_api_available?
|
1870
|
+
|
1871
|
+
begin
|
1872
|
+
url = "#{ENV['JIRA_URL']}/rest/api/2#{endpoint}"
|
1873
|
+
auth = { username: ENV['JIRA_USERNAME'], password: ENV['JIRA_API_TOKEN'] }
|
1874
|
+
|
1875
|
+
options = {
|
1876
|
+
basic_auth: auth,
|
1877
|
+
headers: { 'Content-Type' => 'application/json' }
|
1878
|
+
}
|
1879
|
+
|
1880
|
+
case method
|
1881
|
+
when :get
|
1882
|
+
response = HTTParty.get(url, options)
|
1883
|
+
when :post
|
1884
|
+
response = HTTParty.post(url, options.merge(body: body.to_json))
|
1885
|
+
when :put
|
1886
|
+
response = HTTParty.put(url, options.merge(body: body.to_json))
|
1887
|
+
else
|
1888
|
+
return { error: "Unsupported HTTP method: #{method}" }
|
1889
|
+
end
|
1890
|
+
|
1891
|
+
if response.success?
|
1892
|
+
JSON.parse(response.body)
|
1893
|
+
else
|
1894
|
+
{ error: "JIRA API error: #{response.code} - #{response.body}" }
|
1895
|
+
end
|
1896
|
+
rescue => e
|
1897
|
+
{ error: "JIRA API call failed: #{e.message}" }
|
1898
|
+
end
|
1899
|
+
end
|
1900
|
+
|
1901
|
+
def self.get_real_jira_ticket(ticket_id)
|
1902
|
+
endpoint = "/issue/#{ticket_id}"
|
1903
|
+
response = jira_api_call(endpoint)
|
1904
|
+
|
1905
|
+
return response if response['error']
|
1906
|
+
|
1907
|
+
# Transform Jira API response to our format
|
1908
|
+
{
|
1909
|
+
id: response['key'],
|
1910
|
+
summary: response['fields']['summary'],
|
1911
|
+
description: response['fields']['description'],
|
1912
|
+
status: response['fields']['status']['name'],
|
1913
|
+
priority: response['fields']['priority']['name'],
|
1914
|
+
assignee: response['fields']['assignee']&.dig('emailAddress'),
|
1915
|
+
reporter: response['fields']['reporter']&.dig('emailAddress'),
|
1916
|
+
created: response['fields']['created'],
|
1917
|
+
updated: response['fields']['updated'],
|
1918
|
+
issue_type: response['fields']['issuetype']['name'],
|
1919
|
+
project: response['fields']['project']['key'],
|
1920
|
+
components: response['fields']['components']&.map { |c| c['name'] } || [],
|
1921
|
+
labels: response['fields']['labels'] || [],
|
1922
|
+
comments: response['fields']['comment']&.dig('comments')&.map { |c|
|
1923
|
+
{ author: c['author']['emailAddress'], body: c['body'], created: c['created'] }
|
1924
|
+
} || [],
|
1925
|
+
attachments: response['fields']['attachment']&.map { |a| a['filename'] } || [],
|
1926
|
+
related_issues: [], # Jira doesn't provide this directly
|
1927
|
+
time_tracking: {
|
1928
|
+
original_estimate: response['fields']['timeoriginalestimate'],
|
1929
|
+
time_spent: response['fields']['timespent'],
|
1930
|
+
remaining_estimate: response['fields']['timeestimate']
|
1931
|
+
}
|
1932
|
+
}
|
1933
|
+
end
|
1934
|
+
|
1935
|
+
def self.get_real_jira_search(query, project_key, issue_type, status, assignee)
|
1936
|
+
jql_parts = []
|
1937
|
+
jql_parts << "project = #{project_key}" if project_key
|
1938
|
+
jql_parts << "issuetype = #{issue_type}" if issue_type
|
1939
|
+
jql_parts << "status = #{status}" if status
|
1940
|
+
jql_parts << "assignee = #{assignee}" if assignee
|
1941
|
+
jql_parts << "text ~ \"#{query}\"" if query
|
1942
|
+
|
1943
|
+
jql = jql_parts.join(" AND ")
|
1944
|
+
endpoint = "/search?jql=#{CGI.escape(jql)}&maxResults=10"
|
1945
|
+
|
1946
|
+
response = jira_api_call(endpoint)
|
1947
|
+
return [] if response['error']
|
1948
|
+
|
1949
|
+
response['issues']&.map do |issue|
|
1950
|
+
{
|
1951
|
+
id: issue['key'],
|
1952
|
+
summary: issue['fields']['summary'],
|
1953
|
+
status: issue['fields']['status']['name'],
|
1954
|
+
priority: issue['fields']['priority']['name'],
|
1955
|
+
assignee: issue['fields']['assignee']&.dig('emailAddress'),
|
1956
|
+
issue_type: issue['fields']['issuetype']['name'],
|
1957
|
+
created: issue['fields']['created'],
|
1958
|
+
updated: issue['fields']['updated']
|
1959
|
+
}
|
1960
|
+
end || []
|
1961
|
+
end
|
1962
|
+
|
1963
|
+
def self.get_real_jira_project(project_key)
|
1964
|
+
endpoint = "/project/#{project_key}"
|
1965
|
+
response = jira_api_call(endpoint)
|
1966
|
+
|
1967
|
+
return response if response['error']
|
1968
|
+
|
1969
|
+
{
|
1970
|
+
key: response['key'],
|
1971
|
+
name: response['name'],
|
1972
|
+
description: response['description'],
|
1973
|
+
lead: response['lead']['emailAddress'],
|
1974
|
+
url: "#{ENV['JIRA_URL']}/browse/#{project_key}",
|
1975
|
+
components: response['components']&.map { |c| c['name'] } || [],
|
1976
|
+
issue_types: response['issueTypes']&.map { |it| it['name'] } || [],
|
1977
|
+
statuses: response['statuses']&.map { |s| s['name'] } || [],
|
1978
|
+
versions: response['versions']&.map { |v| v['name'] } || [],
|
1979
|
+
permissions: response['permissions'] || []
|
1980
|
+
}
|
1981
|
+
end
|
1982
|
+
|
1983
|
+
# Fallback to simulation if real API fails
|
1984
|
+
def self.simulate_jira_available?
|
1985
|
+
# Check if JIRA configuration is available
|
1986
|
+
ENV['JIRA_URL'] && ENV['JIRA_USERNAME'] && ENV['JIRA_API_TOKEN']
|
1987
|
+
end
|
1988
|
+
end
|
1989
|
+
|
1990
|
+
# Tool for Confluence integration and business context retrieval
|
1991
|
+
class ConfluenceIntegrationTool < MCP::Tool
|
1992
|
+
description "Integrates with Confluence to retrieve PRDs, business requirements, and documentation for business context"
|
1993
|
+
input_schema(
|
1994
|
+
properties: {
|
1995
|
+
action: { type: "string", enum: ["search_documents", "get_document", "get_space_info", "search_prd"] },
|
1996
|
+
search_query: { type: "string" },
|
1997
|
+
space_key: { type: "string" },
|
1998
|
+
document_id: { type: "string" },
|
1999
|
+
server_context: { type: "object" }
|
2000
|
+
},
|
2001
|
+
required: ["action"]
|
2002
|
+
)
|
2003
|
+
annotations(
|
2004
|
+
title: "Confluence Integration Tool",
|
2005
|
+
read_only_hint: true,
|
2006
|
+
destructive_hint: false,
|
2007
|
+
idempotent_hint: true,
|
2008
|
+
open_world_hint: false
|
2009
|
+
)
|
2010
|
+
|
2011
|
+
def self.call(action:, search_query: nil, space_key: nil, document_id: nil, server_context:)
|
2012
|
+
case action
|
2013
|
+
when "search_documents"
|
2014
|
+
search_confluence_documents(search_query, space_key)
|
2015
|
+
when "get_document"
|
2016
|
+
get_confluence_document(document_id)
|
2017
|
+
when "get_space_info"
|
2018
|
+
get_confluence_space_info(space_key)
|
2019
|
+
when "search_prd"
|
2020
|
+
search_prd_documents(search_query, space_key)
|
2021
|
+
else
|
2022
|
+
MCP::Tool::Response.new([{ type: "text", text: { error: "Unknown action: #{action}" }.to_json }])
|
2023
|
+
end
|
2024
|
+
end
|
2025
|
+
|
2026
|
+
private
|
2027
|
+
|
2028
|
+
def self.search_confluence_documents(query, space_key)
|
2029
|
+
if confluence_api_available?
|
2030
|
+
get_real_confluence_search(query, space_key)
|
2031
|
+
else
|
2032
|
+
simulate_confluence_search(query, space_key)
|
2033
|
+
end
|
2034
|
+
end
|
2035
|
+
|
2036
|
+
def self.get_confluence_document(document_id)
|
2037
|
+
if confluence_api_available?
|
2038
|
+
get_real_confluence_document(document_id)
|
2039
|
+
else
|
2040
|
+
simulate_confluence_document(document_id)
|
2041
|
+
end
|
2042
|
+
end
|
2043
|
+
|
2044
|
+
def self.get_confluence_space_info(space_key)
|
2045
|
+
if confluence_api_available?
|
2046
|
+
get_real_confluence_space_info(space_key)
|
2047
|
+
else
|
2048
|
+
simulate_confluence_space_info(space_key)
|
2049
|
+
end
|
2050
|
+
end
|
2051
|
+
|
2052
|
+
def self.search_prd_documents(query, space_key)
|
2053
|
+
if confluence_api_available?
|
2054
|
+
get_real_confluence_prd_search(query, space_key)
|
2055
|
+
else
|
2056
|
+
simulate_confluence_prd_search(query, space_key)
|
2057
|
+
end
|
2058
|
+
end
|
2059
|
+
|
2060
|
+
# Real Confluence API calls
|
2061
|
+
def self.confluence_api_available?
|
2062
|
+
ENV['CONFLUENCE_URL'] && ENV['CONFLUENCE_USERNAME'] && ENV['CONFLUENCE_API_TOKEN']
|
2063
|
+
end
|
2064
|
+
|
2065
|
+
def self.confluence_api_call(endpoint, params = {})
|
2066
|
+
return nil unless confluence_api_available?
|
2067
|
+
|
2068
|
+
url = "#{ENV['CONFLUENCE_URL']}/rest/api#{endpoint}"
|
2069
|
+
auth = { username: ENV['CONFLUENCE_USERNAME'], password: ENV['CONFLUENCE_API_TOKEN'] }
|
2070
|
+
|
2071
|
+
begin
|
2072
|
+
response = HTTParty.get(url, basic_auth: auth, query: params)
|
2073
|
+
response.success? ? response.parsed_response : nil
|
2074
|
+
rescue => e
|
2075
|
+
puts "⚠️ Confluence API call failed: #{e.message}"
|
2076
|
+
nil
|
2077
|
+
end
|
2078
|
+
end
|
2079
|
+
|
2080
|
+
def self.get_real_confluence_search(query, space_key)
|
2081
|
+
params = {
|
2082
|
+
cql: "space = #{space_key} AND text ~ \"#{query}\"",
|
2083
|
+
limit: 10,
|
2084
|
+
expand: "content"
|
2085
|
+
}
|
2086
|
+
|
2087
|
+
data = confluence_api_call('/content/search', params)
|
2088
|
+
return { error: "Failed to search Confluence" } unless data
|
2089
|
+
|
2090
|
+
documents = data['results']&.map do |result|
|
2091
|
+
{
|
2092
|
+
id: result['id'],
|
2093
|
+
title: result['title'],
|
2094
|
+
type: result['type'],
|
2095
|
+
space_key: result['space']['key'],
|
2096
|
+
url: "#{ENV['CONFLUENCE_URL']}/wiki#{result['_links']['webui']}",
|
2097
|
+
content: result['excerpt'] || result['title'],
|
2098
|
+
labels: result['metadata']&.dig('labels', 'results')&.map { |l| l['name'] } || []
|
2099
|
+
}
|
2100
|
+
end || []
|
2101
|
+
|
2102
|
+
MCP::Tool::Response.new([{ type: "text", text: { documents: documents }.to_json }])
|
2103
|
+
end
|
2104
|
+
|
2105
|
+
def self.get_real_confluence_document(document_id)
|
2106
|
+
data = confluence_api_call("/content/#{document_id}")
|
2107
|
+
return { error: "Failed to get document" } unless data
|
2108
|
+
|
2109
|
+
document = {
|
2110
|
+
id: data['id'],
|
2111
|
+
title: data['title'],
|
2112
|
+
type: data['type'],
|
2113
|
+
space_key: data['space']['key'],
|
2114
|
+
url: "#{ENV['CONFLUENCE_URL']}/wiki#{data['_links']['webui']}",
|
2115
|
+
content: data['body']&.dig('storage', 'value') || data['title'],
|
2116
|
+
labels: data['metadata']&.dig('labels', 'results')&.map { |l| l['name'] } || []
|
2117
|
+
}
|
2118
|
+
|
2119
|
+
MCP::Tool::Response.new([{ type: "text", text: { document: document }.to_json }])
|
2120
|
+
end
|
2121
|
+
|
2122
|
+
def self.get_real_confluence_space_info(space_key)
|
2123
|
+
data = confluence_api_call("/space/#{space_key}")
|
2124
|
+
return { error: "Failed to get space info" } unless data
|
2125
|
+
|
2126
|
+
space_info = {
|
2127
|
+
key: data['key'],
|
2128
|
+
name: data['name'],
|
2129
|
+
type: data['type'],
|
2130
|
+
description: data['description'],
|
2131
|
+
url: "#{ENV['CONFLUENCE_URL']}/wiki/spaces/#{space_key}",
|
2132
|
+
permissions: data['permissions'] || []
|
2133
|
+
}
|
2134
|
+
|
2135
|
+
MCP::Tool::Response.new([{ type: "text", text: { space: space_info }.to_json }])
|
2136
|
+
end
|
2137
|
+
|
2138
|
+
def self.get_real_confluence_prd_search(query, space_key)
|
2139
|
+
params = {
|
2140
|
+
cql: "space = #{space_key} AND (text ~ \"PRD\" OR text ~ \"Product Requirements\" OR text ~ \"#{query}\")",
|
2141
|
+
limit: 5,
|
2142
|
+
expand: "content"
|
2143
|
+
}
|
2144
|
+
|
2145
|
+
data = confluence_api_call('/content/search', params)
|
2146
|
+
return { error: "Failed to search PRD documents" } unless data
|
2147
|
+
|
2148
|
+
prd_documents = data['results']&.map do |result|
|
2149
|
+
{
|
2150
|
+
id: result['id'],
|
2151
|
+
title: result['title'],
|
2152
|
+
type: result['type'],
|
2153
|
+
space_key: result['space']['key'],
|
2154
|
+
url: "#{ENV['CONFLUENCE_URL']}/wiki#{result['_links']['webui']}",
|
2155
|
+
content: result['excerpt'] || result['title'],
|
2156
|
+
is_prd: result['title'].include?('PRD') || result['title'].include?('Product Requirements'),
|
2157
|
+
labels: result['metadata']&.dig('labels', 'results')&.map { |l| l['name'] } || []
|
2158
|
+
}
|
2159
|
+
end || []
|
2160
|
+
|
2161
|
+
MCP::Tool::Response.new([{ type: "text", text: { documents: prd_documents }.to_json }])
|
2162
|
+
end
|
2163
|
+
|
2164
|
+
# Simulation methods for when Confluence API is not available
|
2165
|
+
def self.simulate_confluence_search(query, space_key)
|
2166
|
+
documents = [
|
2167
|
+
{
|
2168
|
+
id: "sim-1",
|
2169
|
+
title: "Business Rules for #{space_key}",
|
2170
|
+
type: "page",
|
2171
|
+
space_key: space_key,
|
2172
|
+
url: "https://confluence.example.com/wiki/spaces/#{space_key}/pages/sim-1",
|
2173
|
+
content: "Simulated business rules document for #{space_key} project",
|
2174
|
+
labels: ["business-rules", "requirements"]
|
2175
|
+
},
|
2176
|
+
{
|
2177
|
+
id: "sim-2",
|
2178
|
+
title: "Process Documentation",
|
2179
|
+
type: "page",
|
2180
|
+
space_key: space_key,
|
2181
|
+
url: "https://confluence.example.com/wiki/spaces/#{space_key}/pages/sim-2",
|
2182
|
+
content: "Simulated process documentation for #{space_key}",
|
2183
|
+
labels: ["process", "workflow"]
|
2184
|
+
}
|
2185
|
+
]
|
2186
|
+
|
2187
|
+
MCP::Tool::Response.new([{ type: "text", text: { documents: documents }.to_json }])
|
2188
|
+
end
|
2189
|
+
|
2190
|
+
def self.simulate_confluence_document(document_id)
|
2191
|
+
document = {
|
2192
|
+
id: document_id,
|
2193
|
+
title: "Simulated Confluence Document",
|
2194
|
+
type: "page",
|
2195
|
+
space_key: "DGTL",
|
2196
|
+
url: "https://confluence.example.com/wiki/spaces/DGTL/pages/#{document_id}",
|
2197
|
+
content: "This is a simulated Confluence document content for testing purposes.",
|
2198
|
+
labels: ["simulated", "test"]
|
2199
|
+
}
|
2200
|
+
|
2201
|
+
MCP::Tool::Response.new([{ type: "text", text: { document: document }.to_json }])
|
2202
|
+
end
|
2203
|
+
|
2204
|
+
def self.simulate_confluence_space_info(space_key)
|
2205
|
+
space_info = {
|
2206
|
+
key: space_key,
|
2207
|
+
name: "#{space_key} Project Space",
|
2208
|
+
type: "global",
|
2209
|
+
description: "Simulated Confluence space for #{space_key} project",
|
2210
|
+
url: "https://confluence.example.com/wiki/spaces/#{space_key}",
|
2211
|
+
permissions: ["view", "edit", "create"]
|
2212
|
+
}
|
2213
|
+
|
2214
|
+
MCP::Tool::Response.new([{ type: "text", text: { space: space_info }.to_json }])
|
2215
|
+
end
|
2216
|
+
|
2217
|
+
def self.simulate_confluence_prd_search(query, space_key)
|
2218
|
+
prd_documents = [
|
2219
|
+
{
|
2220
|
+
id: "prd-1",
|
2221
|
+
title: "Product Requirements Document - #{space_key}",
|
2222
|
+
type: "page",
|
2223
|
+
space_key: space_key,
|
2224
|
+
url: "https://confluence.example.com/wiki/spaces/#{space_key}/pages/prd-1",
|
2225
|
+
content: "Simulated PRD for #{space_key} project with requirements and business rules",
|
2226
|
+
is_prd: true,
|
2227
|
+
labels: ["PRD", "requirements", "product"]
|
2228
|
+
}
|
2229
|
+
]
|
2230
|
+
|
2231
|
+
MCP::Tool::Response.new([{ type: "text", text: { documents: prd_documents }.to_json }])
|
2232
|
+
end
|
1848
2233
|
end
|
1849
2234
|
|
1850
2235
|
# Tool for intelligent context analysis and runtime decision making
|
@@ -27,7 +27,7 @@ module CodeHealer
|
|
27
27
|
puts "Creating pull request for branch: #{branch_name}"
|
28
28
|
|
29
29
|
# Get target branch from configuration
|
30
|
-
target_branch = ConfigManager.pr_target_branch
|
30
|
+
target_branch = ConfigManager.pr_target_branch
|
31
31
|
puts "📋 Target branch for PR: #{target_branch}"
|
32
32
|
|
33
33
|
pr = client.create_pull_request(
|
data/lib/code_healer/setup.rb
CHANGED
@@ -252,11 +252,51 @@ github_token = ask_for_input("Enter your GitHub personal access token (or press
|
|
252
252
|
github_repo = ask_for_input("Enter your GitHub repository (username/repo or full URL):")
|
253
253
|
github_repo_url = normalize_repository_url(github_repo, github_token)
|
254
254
|
|
255
|
+
# Jira Configuration
|
256
|
+
puts
|
257
|
+
puts "🎫 Jira Configuration:"
|
258
|
+
puts "Configure Jira integration for business context during healing operations."
|
259
|
+
puts "Get your API token at: https://id.atlassian.com/manage-profile/security/api-tokens"
|
260
|
+
puts
|
261
|
+
|
262
|
+
enable_jira = ask_for_yes_no("Enable Jira integration for business context?", default: false)
|
263
|
+
|
264
|
+
if enable_jira
|
265
|
+
jira_url = ask_for_input("Enter your Jira instance URL (e.g., https://your-company.atlassian.net):")
|
266
|
+
jira_username = ask_for_input("Enter your Jira username/email:")
|
267
|
+
jira_api_token = ask_for_input("Enter your Jira API token:")
|
268
|
+
jira_project_key = ask_for_input("Enter your default Jira project key (e.g., DGTL):")
|
269
|
+
|
270
|
+
# Validate Jira configuration
|
271
|
+
puts "🔍 Validating Jira configuration..."
|
272
|
+
if jira_url && jira_username && jira_api_token && jira_project_key
|
273
|
+
puts "✅ Jira configuration provided"
|
274
|
+
else
|
275
|
+
puts "⚠️ Incomplete Jira configuration - integration will be disabled"
|
276
|
+
enable_jira = false
|
277
|
+
end
|
278
|
+
else
|
279
|
+
jira_url = ""
|
280
|
+
jira_username = ""
|
281
|
+
jira_api_token = ""
|
282
|
+
jira_project_key = ""
|
283
|
+
end
|
284
|
+
|
255
285
|
# Git Branch Configuration
|
256
286
|
puts
|
257
287
|
puts "🌿 Git Branch Configuration:"
|
258
288
|
branch_prefix = ask_for_input("Enter branch prefix for healing branches (default: evolve):", default: "evolve")
|
259
|
-
|
289
|
+
|
290
|
+
# Detect the actual default branch from git
|
291
|
+
default_branch = "main"
|
292
|
+
if system("git rev-parse --verify master >/dev/null 2>&1")
|
293
|
+
default_branch = "master"
|
294
|
+
elsif system("git rev-parse --verify main >/dev/null 2>&1")
|
295
|
+
default_branch = "main"
|
296
|
+
end
|
297
|
+
|
298
|
+
puts "🔍 Detected default branch: #{default_branch}"
|
299
|
+
pr_target_branch = ask_for_input("Enter target branch for pull requests (default: #{default_branch}):", default: default_branch)
|
260
300
|
|
261
301
|
# Code Heal Directory Configuration
|
262
302
|
puts
|
@@ -297,6 +337,60 @@ puts
|
|
297
337
|
puts "💼 Business Context Setup:"
|
298
338
|
create_business_context = ask_for_yes_no("Would you like to create a business context file?", default: true)
|
299
339
|
|
340
|
+
# Business Context Strategy Configurationy
|
341
|
+
puts
|
342
|
+
puts "🔍 Business Context Strategy Configuration:"
|
343
|
+
puts "Choose how CodeHealer should get business context:"
|
344
|
+
puts "1. Jira MCP (Claude Terminal uses its own Jira MCP)"
|
345
|
+
puts "2. Markdown files (docs/business_rules.md)"
|
346
|
+
puts "3. Hybrid (both Jira MCP and Markdown)"
|
347
|
+
puts
|
348
|
+
|
349
|
+
business_context_strategy = ask_for_input("Enter business context strategy (1/2/3 or jira_mcp/markdown/hybrid):", default: "jira_mcp")
|
350
|
+
business_context_strategy = case business_context_strategy.downcase
|
351
|
+
when "1", "jira_mcp"
|
352
|
+
"jira_mcp"
|
353
|
+
when "2", "markdown"
|
354
|
+
"markdown"
|
355
|
+
when "3", "hybrid"
|
356
|
+
"hybrid"
|
357
|
+
else
|
358
|
+
"jira_mcp"
|
359
|
+
end
|
360
|
+
|
361
|
+
# Configure Confluence if selected
|
362
|
+
enable_confluence = false
|
363
|
+
confluence_url = ""
|
364
|
+
confluence_username = ""
|
365
|
+
confluence_api_token = ""
|
366
|
+
confluence_space_key = ""
|
367
|
+
|
368
|
+
if business_context_source == "confluence" || business_context_source == "hybrid"
|
369
|
+
puts
|
370
|
+
puts "📚 Confluence Configuration:"
|
371
|
+
puts "Configure Confluence integration for PRDs and documentation."
|
372
|
+
puts "Get your API token at: https://id.atlassian.com/manage-profile/security/api-tokens"
|
373
|
+
puts
|
374
|
+
|
375
|
+
enable_confluence = ask_for_yes_no("Enable Confluence integration for business context?", default: false)
|
376
|
+
|
377
|
+
if enable_confluence
|
378
|
+
confluence_url = ask_for_input("Enter your Confluence instance URL (e.g., https://your-company.atlassian.net/wiki):")
|
379
|
+
confluence_username = ask_for_input("Enter your Confluence username/email:")
|
380
|
+
confluence_api_token = ask_for_input("Enter your Confluence API token:")
|
381
|
+
confluence_space_key = ask_for_input("Enter your Confluence space key (e.g., DGTL):")
|
382
|
+
|
383
|
+
# Validate Confluence configuration
|
384
|
+
puts "🔍 Validating Confluence configuration..."
|
385
|
+
if confluence_url && confluence_username && confluence_api_token && confluence_space_key
|
386
|
+
puts "✅ Confluence configuration provided"
|
387
|
+
else
|
388
|
+
puts "⚠️ Incomplete Confluence configuration - integration will be disabled"
|
389
|
+
enable_confluence = false
|
390
|
+
end
|
391
|
+
end
|
392
|
+
end
|
393
|
+
|
300
394
|
# Evolution Strategy Configuration
|
301
395
|
puts
|
302
396
|
puts "🧠 Evolution Strategy Configuration:"
|
@@ -364,6 +458,18 @@ puts
|
|
364
458
|
GITHUB_TOKEN=#{github_token}
|
365
459
|
GITHUB_REPOSITORY=#{github_repo}
|
366
460
|
|
461
|
+
# Jira Configuration
|
462
|
+
JIRA_URL=#{jira_url}
|
463
|
+
JIRA_USERNAME=#{jira_username}
|
464
|
+
JIRA_API_TOKEN=#{jira_api_token}
|
465
|
+
JIRA_PROJECT_KEY=#{jira_project_key}
|
466
|
+
|
467
|
+
# Confluence Configuration
|
468
|
+
CONFLUENCE_URL=#{confluence_url}
|
469
|
+
CONFLUENCE_USERNAME=#{confluence_username}
|
470
|
+
CONFLUENCE_API_TOKEN=#{confluence_api_token}
|
471
|
+
CONFLUENCE_SPACE_KEY=#{confluence_space_key}
|
472
|
+
|
367
473
|
# Optional: Redis Configuration
|
368
474
|
REDIS_URL=redis://localhost:6379/0
|
369
475
|
ENV
|
@@ -426,8 +532,36 @@ create_file_with_content('.env', env_content, dry_run: options[:dry_run])
|
|
426
532
|
# Business Context Configuration
|
427
533
|
business_context:
|
428
534
|
enabled: true
|
429
|
-
|
430
|
-
|
535
|
+
strategy: "#{business_context_strategy}"
|
536
|
+
|
537
|
+
# Jira MCP Configuration
|
538
|
+
jira_mcp:
|
539
|
+
enabled: #{business_context_strategy == 'jira_mcp' || business_context_strategy == 'hybrid'}
|
540
|
+
project_key: "#{jira_project_key}"
|
541
|
+
search_tickets_on_error: true
|
542
|
+
include_business_rules: true
|
543
|
+
system_prompt: |
|
544
|
+
When fixing code, ALWAYS check Jira MCP for business context:
|
545
|
+
1. Search for tickets about the class/method you're fixing
|
546
|
+
2. Use Jira requirements to ensure your fix follows business rules
|
547
|
+
3. Reference specific Jira tickets in your explanation
|
548
|
+
4. Make sure fixes align with business requirements
|
549
|
+
|
550
|
+
# Markdown Configuration
|
551
|
+
markdown:
|
552
|
+
enabled: #{business_context_strategy == 'markdown' || business_context_strategy == 'hybrid'}
|
553
|
+
search_paths:
|
554
|
+
- "docs/business_rules.md"
|
555
|
+
- "docs/requirements.md"
|
556
|
+
- "business_requirements/"
|
557
|
+
include_patterns:
|
558
|
+
- "*.md"
|
559
|
+
- "*.txt"
|
560
|
+
|
561
|
+
# Hybrid Configuration
|
562
|
+
hybrid:
|
563
|
+
priority: ["jira_mcp", "markdown"]
|
564
|
+
combine_results: true
|
431
565
|
|
432
566
|
# OpenAI API configuration
|
433
567
|
api:
|
@@ -442,7 +576,9 @@ create_file_with_content('.env', env_content, dry_run: options[:dry_run])
|
|
442
576
|
auto_push: true
|
443
577
|
branch_prefix: "#{branch_prefix}"
|
444
578
|
commit_message_template: 'Fix {{class_name}}\#\#{{method_name}}: {{error_type}}'
|
445
|
-
|
579
|
+
|
580
|
+
# Pull Request target branch (for backward compatibility)
|
581
|
+
pr_target_branch: "#{pr_target_branch}"
|
446
582
|
|
447
583
|
# Pull Request Configuration
|
448
584
|
pull_request:
|
@@ -450,6 +586,16 @@ create_file_with_content('.env', env_content, dry_run: options[:dry_run])
|
|
450
586
|
auto_create: true
|
451
587
|
labels:
|
452
588
|
- "auto-fix"
|
589
|
+
|
590
|
+
# Jira Integration Configuration
|
591
|
+
jira:
|
592
|
+
enabled: #{enable_jira}
|
593
|
+
url: "#{jira_url}"
|
594
|
+
username: "#{jira_username}"
|
595
|
+
project_key: "#{jira_project_key}"
|
596
|
+
business_context_enabled: #{enable_jira}
|
597
|
+
search_tickets_on_error: true
|
598
|
+
include_ticket_details: true
|
453
599
|
- "self-evolving"
|
454
600
|
- "bug-fix"
|
455
601
|
|
@@ -578,6 +724,11 @@ puts " - All features are pre-configured and ready to use"
|
|
578
724
|
puts "🌍 Environment Variables:"
|
579
725
|
puts " - Add 'gem \"dotenv-rails\"' to your Gemfile for automatic .env loading"
|
580
726
|
puts " - Or export variables manually: export GITHUB_TOKEN=your_token"
|
727
|
+
if enable_jira
|
728
|
+
puts " - Jira integration: export JIRA_URL=#{jira_url}"
|
729
|
+
puts " - Jira credentials: export JIRA_USERNAME=#{jira_username}"
|
730
|
+
puts " - Jira project: export JIRA_PROJECT_KEY=#{jira_project_key}"
|
731
|
+
end
|
581
732
|
puts " - Or load .env file in your application.rb: load '.env' if File.exist?('.env')"
|
582
733
|
puts
|
583
734
|
puts "CodeHealer will now automatically detect and heal errors in your application!"
|
data/lib/code_healer/version.rb
CHANGED
data/lib/code_healer.rb
CHANGED
@@ -8,12 +8,13 @@ require 'openai'
|
|
8
8
|
require 'sidekiq'
|
9
9
|
require 'git'
|
10
10
|
require 'octokit'
|
11
|
+
require 'open3'
|
11
12
|
|
12
13
|
# CodeHealer - AI-Powered Code Healing and Self-Repair System
|
13
14
|
module CodeHealer
|
14
15
|
class Error < StandardError; end
|
15
16
|
|
16
|
-
|
17
|
+
|
17
18
|
end
|
18
19
|
|
19
20
|
# Autoload all the main classes
|
@@ -88,5 +89,7 @@ if defined?(Rails)
|
|
88
89
|
puts "⚠️ Claude preload failed: #{e.message}"
|
89
90
|
end
|
90
91
|
end
|
92
|
+
|
93
|
+
|
91
94
|
end
|
92
95
|
end
|