@link-assistant/hive-mind 2.7.4 → 2.8.1
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/CHANGELOG.md +40 -0
- package/README.hi.md +29 -2
- package/README.md +29 -2
- package/README.ru.md +28 -2
- package/README.zh.md +27 -2
- package/package.json +3 -2
- package/src/codex-capability-preflight.lib.mjs +274 -0
- package/src/codex.lib.mjs +8 -2
- package/src/fix.ci-cd.lib.mjs +480 -0
- package/src/fix.mjs +242 -0
- package/src/isolation-runner.lib.mjs +4 -0
- package/src/locales/en.lino +6 -1
- package/src/locales/hi.lino +6 -1
- package/src/locales/ru.lino +6 -1
- package/src/locales/zh.lino +6 -1
- package/src/task.issue-creation.lib.mjs +31 -5
- package/src/telegram-bot.mjs +10 -85
- package/src/telegram-fix-command.lib.mjs +164 -0
- package/src/telegram-ui-messages.lib.mjs +11 -1
- package/src/telegram.config.lib.mjs +96 -0
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure helpers for the `/fix --ci-cd` command (issue #1733).
|
|
3
|
+
*
|
|
4
|
+
* `/fix --ci-cd <repository>` automatically:
|
|
5
|
+
* 1. detects the languages used in the target repository,
|
|
6
|
+
* 2. inspects the latest default-branch commit and its CI/CD runs,
|
|
7
|
+
* 3. creates a remediation issue (mirroring the `/task` issue-creation flow)
|
|
8
|
+
* that links the language-appropriate CI/CD pipeline templates and the
|
|
9
|
+
* CI/CD best-practices guide, and
|
|
10
|
+
* 4. hands the issue off to
|
|
11
|
+
* `/solve --development-log --deep-analysis --auto-merge`, forwarding every
|
|
12
|
+
* option that `/fix` itself does not consume (e.g. --tool, --model,
|
|
13
|
+
* --think).
|
|
14
|
+
*
|
|
15
|
+
* The issue title and body are taken from the standard prompt in
|
|
16
|
+
* https://github.com/link-assistant/web-capture/issues/139, omitting the
|
|
17
|
+
* its retired case-study paragraph in favor of `--development-log` and omitting
|
|
18
|
+
* paragraphs that `--deep-analysis` already injects into the AI prompt (issue
|
|
19
|
+
* #1733) — see `buildStandardPromptParagraphs` below.
|
|
20
|
+
*
|
|
21
|
+
* Everything that does not touch the network or the filesystem lives here so it
|
|
22
|
+
* can be unit-tested without GitHub access.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { KEEP_WORKING_PROMPT } from './solve.keep-working.detect.lib.mjs';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Canonical mapping from GitHub Linguist language names to the
|
|
29
|
+
* link-foundation AI-driven-development pipeline templates.
|
|
30
|
+
*
|
|
31
|
+
* Order in this array is the stable tie-breaker when two languages contribute
|
|
32
|
+
* an equal number of bytes. The PHP template was added per issue #1733.
|
|
33
|
+
*/
|
|
34
|
+
export const CI_CD_TEMPLATES = Object.freeze([
|
|
35
|
+
{
|
|
36
|
+
key: 'javascript',
|
|
37
|
+
label: 'JavaScript / TypeScript',
|
|
38
|
+
languages: ['JavaScript', 'TypeScript'],
|
|
39
|
+
repo: 'link-foundation/js-ai-driven-development-pipeline-template',
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
key: 'rust',
|
|
43
|
+
label: 'Rust',
|
|
44
|
+
languages: ['Rust'],
|
|
45
|
+
repo: 'link-foundation/rust-ai-driven-development-pipeline-template',
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
key: 'python',
|
|
49
|
+
label: 'Python',
|
|
50
|
+
languages: ['Python'],
|
|
51
|
+
repo: 'link-foundation/python-ai-driven-development-pipeline-template',
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
key: 'go',
|
|
55
|
+
label: 'Go',
|
|
56
|
+
languages: ['Go'],
|
|
57
|
+
repo: 'link-foundation/go-ai-driven-development-pipeline-template',
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
key: 'csharp',
|
|
61
|
+
label: 'C#',
|
|
62
|
+
languages: ['C#'],
|
|
63
|
+
repo: 'link-foundation/csharp-ai-driven-development-pipeline-template',
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
key: 'java',
|
|
67
|
+
label: 'Java',
|
|
68
|
+
languages: ['Java'],
|
|
69
|
+
repo: 'link-foundation/java-ai-driven-development-pipeline-template',
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
key: 'php',
|
|
73
|
+
label: 'PHP',
|
|
74
|
+
languages: ['PHP'],
|
|
75
|
+
repo: 'link-foundation/php-ai-driven-development-pipeline-template',
|
|
76
|
+
},
|
|
77
|
+
]);
|
|
78
|
+
|
|
79
|
+
export const CI_CD_BEST_PRACTICES_URL = 'https://github.com/link-assistant/hive-mind/blob/main/docs/CI-CD-BEST-PRACTICES.md';
|
|
80
|
+
|
|
81
|
+
/** Build a browser URL for a `owner/repo` slug. */
|
|
82
|
+
export function templateUrl(repo) {
|
|
83
|
+
return `https://github.com/${repo}`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Parse a `/fix` repository argument into a normalized descriptor.
|
|
88
|
+
* Returns null when the value is not a GitHub repository URL/shorthand.
|
|
89
|
+
*
|
|
90
|
+
* Self-contained on purpose: keeping this module free of the heavy
|
|
91
|
+
* `github.lib.mjs` import chain lets the pure helpers be unit-tested without
|
|
92
|
+
* network access. Accepts:
|
|
93
|
+
* - https://github.com/owner/repo (with optional .git / trailing slash)
|
|
94
|
+
* - github.com/owner/repo
|
|
95
|
+
* - owner/repo shorthand
|
|
96
|
+
* Rejects anything that points deeper than a repository (issues, pulls, …),
|
|
97
|
+
* contains whitespace, or is otherwise malformed.
|
|
98
|
+
*/
|
|
99
|
+
export function parseFixRepository(value) {
|
|
100
|
+
const candidate = String(value || '')
|
|
101
|
+
.trim()
|
|
102
|
+
.replace(/^[<([{]+/, '')
|
|
103
|
+
.replace(/[>\])}.,;:]+$/, '');
|
|
104
|
+
if (!candidate || /\s/.test(candidate)) return null;
|
|
105
|
+
|
|
106
|
+
// Normalize away an optional protocol, then require either a github.com host
|
|
107
|
+
// or a bare `owner/repo` shorthand. Any other host is rejected.
|
|
108
|
+
let withoutProtocol = candidate.replace(/^https?:\/\//i, '');
|
|
109
|
+
const hadProtocol = withoutProtocol !== candidate;
|
|
110
|
+
|
|
111
|
+
let pathPart;
|
|
112
|
+
if (/^github\.com\//i.test(withoutProtocol)) {
|
|
113
|
+
pathPart = withoutProtocol.replace(/^github\.com\//i, '');
|
|
114
|
+
} else if (!hadProtocol && !withoutProtocol.includes('.com/') && !/[^/]+\.[^/]+\//.test(withoutProtocol)) {
|
|
115
|
+
// Bare shorthand like `owner/repo`.
|
|
116
|
+
pathPart = withoutProtocol;
|
|
117
|
+
} else {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
pathPart = pathPart.replace(/\.git$/i, '').replace(/\/+$/, '');
|
|
122
|
+
|
|
123
|
+
const segments = pathPart.split('/').filter(Boolean);
|
|
124
|
+
if (segments.length !== 2) return null;
|
|
125
|
+
|
|
126
|
+
const [owner, repo] = segments;
|
|
127
|
+
if (!/^[A-Za-z0-9._-]+$/.test(owner) || !/^[A-Za-z0-9._-]+$/.test(repo)) return null;
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
owner,
|
|
131
|
+
repo,
|
|
132
|
+
fullName: `${owner}/${repo}`,
|
|
133
|
+
url: `https://github.com/${owner}/${repo}`,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Normalize the GitHub `/languages` response (a `{ "JavaScript": bytes }` map)
|
|
139
|
+
* or an array of names into a byte-sorted array of `{ name, bytes }`.
|
|
140
|
+
*/
|
|
141
|
+
export function normalizeLanguages(input) {
|
|
142
|
+
let entries = [];
|
|
143
|
+
if (Array.isArray(input)) {
|
|
144
|
+
entries = input.map(name => [String(name), 0]);
|
|
145
|
+
} else if (input && typeof input === 'object') {
|
|
146
|
+
entries = Object.entries(input).map(([name, bytes]) => [String(name), Number(bytes) || 0]);
|
|
147
|
+
}
|
|
148
|
+
return entries
|
|
149
|
+
.filter(([name]) => name)
|
|
150
|
+
.map(([name, bytes]) => ({ name, bytes }))
|
|
151
|
+
.sort((a, b) => b.bytes - a.bytes || a.name.localeCompare(b.name));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Map detected languages to CI/CD templates, sorted so that the templates for
|
|
156
|
+
* the most-used languages come first (issue #1733: "links to CI/CD templates
|
|
157
|
+
* should be sorted by detected languages in the target repository").
|
|
158
|
+
*
|
|
159
|
+
* Returns:
|
|
160
|
+
* - sortedTemplates: matched templates ordered by aggregate detected bytes
|
|
161
|
+
* - unmatchedLanguages: detected languages with no template (informational)
|
|
162
|
+
*/
|
|
163
|
+
export function mapLanguagesToTemplates(languages) {
|
|
164
|
+
const normalized = normalizeLanguages(languages);
|
|
165
|
+
|
|
166
|
+
const templateByLanguage = new Map();
|
|
167
|
+
for (const template of CI_CD_TEMPLATES) {
|
|
168
|
+
for (const language of template.languages) {
|
|
169
|
+
templateByLanguage.set(language.toLowerCase(), template);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const aggregate = new Map(); // template.key -> { template, bytes, languages: [] }
|
|
174
|
+
const unmatchedLanguages = [];
|
|
175
|
+
|
|
176
|
+
for (const { name, bytes } of normalized) {
|
|
177
|
+
const template = templateByLanguage.get(name.toLowerCase());
|
|
178
|
+
if (!template) {
|
|
179
|
+
unmatchedLanguages.push(name);
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
const existing = aggregate.get(template.key) || { template, bytes: 0, languages: [] };
|
|
183
|
+
existing.bytes += bytes;
|
|
184
|
+
existing.languages.push(name);
|
|
185
|
+
aggregate.set(template.key, existing);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const templateOrder = new Map(CI_CD_TEMPLATES.map((template, index) => [template.key, index]));
|
|
189
|
+
const sortedTemplates = [...aggregate.values()].sort((a, b) => b.bytes - a.bytes || templateOrder.get(a.template.key) - templateOrder.get(b.template.key));
|
|
190
|
+
|
|
191
|
+
return { sortedTemplates, unmatchedLanguages };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Title of the auto-generated remediation issue, taken exactly from the
|
|
196
|
+
* standard template issue https://github.com/link-assistant/web-capture/issues/139
|
|
197
|
+
* (issue #1733: "use title and description exactly"). The issue is created in
|
|
198
|
+
* the target repository itself, so it carries no repository suffix.
|
|
199
|
+
*/
|
|
200
|
+
export const CI_CD_ISSUE_TITLE = 'Check for all false positives, false negatives, warnings and errors in CI/CD and fix them all';
|
|
201
|
+
|
|
202
|
+
export function buildCiCdIssueTitle() {
|
|
203
|
+
return CI_CD_ISSUE_TITLE;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function shortSha(sha) {
|
|
207
|
+
return String(sha || '').slice(0, 7);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** Render the detected-languages section. */
|
|
211
|
+
export function buildLanguagesSection(languages) {
|
|
212
|
+
const normalized = normalizeLanguages(languages);
|
|
213
|
+
if (normalized.length === 0) {
|
|
214
|
+
return 'No languages were reported by the GitHub Linguist API for this repository.';
|
|
215
|
+
}
|
|
216
|
+
const total = normalized.reduce((sum, { bytes }) => sum + bytes, 0) || 1;
|
|
217
|
+
const lines = normalized.map(({ name, bytes }) => {
|
|
218
|
+
const percent = ((bytes / total) * 100).toFixed(1);
|
|
219
|
+
return `- **${name}** — ${percent}%`;
|
|
220
|
+
});
|
|
221
|
+
return lines.join('\n');
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** Render the recommended-templates section, sorted by detected languages. */
|
|
225
|
+
export function buildTemplatesSection(languages) {
|
|
226
|
+
const { sortedTemplates, unmatchedLanguages } = mapLanguagesToTemplates(languages);
|
|
227
|
+
const lines = [];
|
|
228
|
+
|
|
229
|
+
if (sortedTemplates.length === 0) {
|
|
230
|
+
lines.push('No language-specific template matched the detected languages. Review all templates and apply the closest match:');
|
|
231
|
+
lines.push('');
|
|
232
|
+
for (const template of CI_CD_TEMPLATES) {
|
|
233
|
+
lines.push(`- ${template.label}: [${template.repo}](${templateUrl(template.repo)})`);
|
|
234
|
+
}
|
|
235
|
+
} else {
|
|
236
|
+
lines.push('Apply the best practices from these templates, in priority order (most-used language first):');
|
|
237
|
+
lines.push('');
|
|
238
|
+
sortedTemplates.forEach((entry, index) => {
|
|
239
|
+
const detected = entry.languages.join(', ');
|
|
240
|
+
lines.push(`${index + 1}. **${entry.template.label}** — [${entry.template.repo}](${templateUrl(entry.template.repo)}) _(detected: ${detected})_`);
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (unmatchedLanguages.length > 0) {
|
|
245
|
+
lines.push('');
|
|
246
|
+
lines.push(`Other detected languages without a dedicated template: ${unmatchedLanguages.join(', ')}.`);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return lines.join('\n');
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/** Render the CI/CD runs section from the GitHub Actions API payload. */
|
|
253
|
+
export function buildRunsSection(runs, { emptyMessage } = {}) {
|
|
254
|
+
const list = Array.isArray(runs) ? runs : [];
|
|
255
|
+
if (list.length === 0) {
|
|
256
|
+
return emptyMessage || 'No CI/CD runs were found for the latest default-branch commit.';
|
|
257
|
+
}
|
|
258
|
+
const header = '| Workflow | Status | Conclusion | Run |\n| --- | --- | --- | --- |';
|
|
259
|
+
const rows = list.map(run => {
|
|
260
|
+
const name = run.name || run.workflowName || 'unknown';
|
|
261
|
+
const status = run.status || 'unknown';
|
|
262
|
+
const conclusion = run.conclusion || (status === 'completed' ? 'unknown' : 'in_progress');
|
|
263
|
+
const url = run.html_url || run.url || '';
|
|
264
|
+
const runLabel = url ? `[run](${url})` : '—';
|
|
265
|
+
return `| ${name} | ${status} | ${conclusion} | ${runLabel} |`;
|
|
266
|
+
});
|
|
267
|
+
return [header, ...rows].join('\n');
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** Count the runs that did not pass (failure/cancelled/timed_out/etc.). */
|
|
271
|
+
export function summarizeRunFailures(runs) {
|
|
272
|
+
const list = Array.isArray(runs) ? runs : [];
|
|
273
|
+
const passing = new Set(['success', 'neutral', 'skipped']);
|
|
274
|
+
const failing = list.filter(run => {
|
|
275
|
+
const conclusion = (run.conclusion || '').toLowerCase();
|
|
276
|
+
return run.status === 'completed' && conclusion && !passing.has(conclusion);
|
|
277
|
+
});
|
|
278
|
+
return { total: list.length, failing: failing.length };
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* The `/solve` options that `/fix` always turns on. `--development-log`
|
|
283
|
+
* replaces the retired collection paragraph; `--deep-analysis` provides the
|
|
284
|
+
* remaining instructions omitted from the generated issue body (issue #1733).
|
|
285
|
+
*/
|
|
286
|
+
export const SOLVE_OPTION_DEVELOPMENT_LOG = '--development-log';
|
|
287
|
+
export const SOLVE_OPTION_DEEP_ANALYSIS = '--deep-analysis';
|
|
288
|
+
export const FIX_FORWARDED_SOLVE_OPTIONS = Object.freeze([SOLVE_OPTION_DEVELOPMENT_LOG, SOLVE_OPTION_DEEP_ANALYSIS]);
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Paragraphs of the standard prompt, quoted from
|
|
292
|
+
* https://github.com/link-assistant/web-capture/issues/139.
|
|
293
|
+
*
|
|
294
|
+
* `providedBy` lists the `/solve` options that already inject an equivalent
|
|
295
|
+
* instruction into the AI prompt (see `buildDeepAnalysisPrompt`). A paragraph
|
|
296
|
+
* is dropped from the issue body when every option that provides it is passed
|
|
297
|
+
* to `/solve`.
|
|
298
|
+
*
|
|
299
|
+
* The deep-analysis wording below is the "bug" variant, which `/solve` emits
|
|
300
|
+
* only when the issue type is Bug — `/fix` therefore creates the issue with
|
|
301
|
+
* that type (see CI_CD_ISSUE_TYPE).
|
|
302
|
+
*
|
|
303
|
+
* The old case-study instruction from the upstream template is intentionally
|
|
304
|
+
* not represented here. `--development-log` is its replacement; generated
|
|
305
|
+
* issues must never offer or restore the superseded folder convention, even
|
|
306
|
+
* when callers request an otherwise unabridged prompt (PR #1929 feedback).
|
|
307
|
+
*/
|
|
308
|
+
export const DEBUG_OUTPUT_PARAGRAPH = 'If there is not enough data to find actual root cause, add debug output and verbose mode if not present, that will allow us to find root cause on next iteration.';
|
|
309
|
+
export const REPORT_UPSTREAM_PARAGRAPH = 'If issue related to any other repository/project, where we can report issues on GitHub, please do so. Each issue must contain reproducible examples, workarounds and suggestions for fix the issue in code. Also double check to fully apply requirements to entire codebase, so if we have issue in multiple places, it should be fixed in all them.';
|
|
310
|
+
|
|
311
|
+
/** Build the ordered, tagged paragraphs of the standard prompt. */
|
|
312
|
+
export function buildStandardPromptParagraphs({ templatesSorted } = {}) {
|
|
313
|
+
const templateLinks = (templatesSorted && templatesSorted.length > 0 ? templatesSorted.map(entry => entry.template.repo) : CI_CD_TEMPLATES.map(template => template.repo)).map(repo => `- ${templateUrl(repo)}`).join('\n');
|
|
314
|
+
|
|
315
|
+
return [
|
|
316
|
+
{
|
|
317
|
+
providedBy: [],
|
|
318
|
+
text: `Use all the best practices from CI/CD templates (check full file tree to compare for all GitHub workflow and CI/CD scripts file), if the same issue is found in template report issue also in templates:\n\n${templateLinks}`,
|
|
319
|
+
},
|
|
320
|
+
{
|
|
321
|
+
providedBy: [],
|
|
322
|
+
text: "We should compare all files, so we don't have more CI/CD errors in the future and reuse all the best practices from these templates.",
|
|
323
|
+
},
|
|
324
|
+
{
|
|
325
|
+
providedBy: [SOLVE_OPTION_DEEP_ANALYSIS],
|
|
326
|
+
text: DEBUG_OUTPUT_PARAGRAPH,
|
|
327
|
+
},
|
|
328
|
+
{
|
|
329
|
+
providedBy: [SOLVE_OPTION_DEEP_ANALYSIS],
|
|
330
|
+
text: REPORT_UPSTREAM_PARAGRAPH,
|
|
331
|
+
},
|
|
332
|
+
{
|
|
333
|
+
providedBy: [],
|
|
334
|
+
text: `Follow the CI/CD best practices collected in [${CI_CD_BEST_PRACTICES_URL}](${CI_CD_BEST_PRACTICES_URL}).`,
|
|
335
|
+
},
|
|
336
|
+
{
|
|
337
|
+
// Quoted verbatim from the template; identical to the reinforcement
|
|
338
|
+
// prompt /solve --keep-working-... reuses, so share the single constant.
|
|
339
|
+
providedBy: [],
|
|
340
|
+
text: KEEP_WORKING_PROMPT,
|
|
341
|
+
},
|
|
342
|
+
];
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* The standard remediation prompt, quoted from web-capture#139 with the
|
|
347
|
+
* paragraphs that `omittedOptions` already provide removed.
|
|
348
|
+
*/
|
|
349
|
+
export function buildStandardPrompt({ templatesSorted, omittedOptions = FIX_FORWARDED_SOLVE_OPTIONS } = {}) {
|
|
350
|
+
const omitted = new Set(omittedOptions || []);
|
|
351
|
+
return buildStandardPromptParagraphs({ templatesSorted })
|
|
352
|
+
.filter(paragraph => paragraph.providedBy.length === 0 || !paragraph.providedBy.every(option => omitted.has(option)))
|
|
353
|
+
.map(paragraph => paragraph.text)
|
|
354
|
+
.join('\n\n');
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Issue type and label of the generated issue.
|
|
359
|
+
*
|
|
360
|
+
* `/solve --deep-analysis` only emits the root-cause / debug-output /
|
|
361
|
+
* report-upstream instructions — the paragraphs this body omits — when the
|
|
362
|
+
* issue type is Bug (see `isBugIssueType` in development-log.lib.mjs). Creating
|
|
363
|
+
* the issue as a Bug is therefore what makes the omission lossless.
|
|
364
|
+
*/
|
|
365
|
+
export const CI_CD_ISSUE_TYPE = 'Bug';
|
|
366
|
+
export const CI_CD_ISSUE_LABELS = Object.freeze(['bug']);
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Build the full Markdown body of the auto-generated remediation issue.
|
|
370
|
+
*
|
|
371
|
+
* The body mirrors the template issue's own description: the CI/CD runs of the
|
|
372
|
+
* latest default-branch commit first, then the standard prompt. The data `/fix`
|
|
373
|
+
* collected to build it (commit, languages, template ranking) follows as a
|
|
374
|
+
* collapsed context block so it stays available without displacing the prompt.
|
|
375
|
+
*/
|
|
376
|
+
export function buildCiCdIssueBody({ repository, defaultBranch, commit, runs, languages, runsSource = 'commit', omittedOptions = FIX_FORWARDED_SOLVE_OPTIONS }) {
|
|
377
|
+
const { sortedTemplates } = mapLanguagesToTemplates(languages);
|
|
378
|
+
const { total, failing } = summarizeRunFailures(runs);
|
|
379
|
+
|
|
380
|
+
const commitLine = commit?.sha ? `\`${shortSha(commit.sha)}\`${commit.url ? ` ([commit](${commit.url}))` : ''}${commit.message ? ` — ${String(commit.message).split('\n')[0]}` : ''}` : 'unknown';
|
|
381
|
+
|
|
382
|
+
// When the exact latest commit produced no runs (common for release/tag
|
|
383
|
+
// commits), `/fix` falls back to the most recent runs on the default branch
|
|
384
|
+
// so the issue stays actionable. Label the source honestly.
|
|
385
|
+
const runsHeading = runsSource === 'branch' ? `Recent CI/CD runs on \`${defaultBranch || 'default branch'}\`` : 'Latest default-branch CI/CD runs';
|
|
386
|
+
const runsEmptyMessage = runsSource === 'branch' ? `No recent CI/CD runs were found on \`${defaultBranch || 'the default branch'}\`.` : 'No CI/CD runs were found for the latest default-branch commit.';
|
|
387
|
+
|
|
388
|
+
const sections = [`### ${runsHeading}`, '', buildRunsSection(runs, { emptyMessage: runsEmptyMessage }), '', buildStandardPrompt({ templatesSorted: sortedTemplates, omittedOptions }), '', '---', '', '<details>', '<summary>Context collected by <code>/fix --ci-cd</code></summary>', '', `- **Repository:** [${repository?.fullName}](${repository?.url})`, `- **Default branch:** \`${defaultBranch || 'unknown'}\``, `- **Latest commit:** ${commitLine}`, `- **CI/CD runs found:** ${total} (${failing} not passing)`, '', '**Detected languages**', '', buildLanguagesSection(languages), '', '**Recommended CI/CD templates**', '', buildTemplatesSection(languages), '', '</details>'];
|
|
389
|
+
|
|
390
|
+
return sections.join('\n');
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Flags that `/fix` consumes itself and must NOT be forwarded to `/solve`.
|
|
395
|
+
* Boolean flags only — they never take a value.
|
|
396
|
+
*/
|
|
397
|
+
export const FIX_OWNED_BOOLEAN_FLAGS = Object.freeze(['--ci-cd', '--dry-run', '--no-solve', '--solve', '--no-auto-solve', '--help', '-h', '--version']);
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Partition raw CLI args into the options `/fix` consumes and the passthrough
|
|
401
|
+
* args forwarded to `/solve`. Unknown flags (and their values) are preserved in
|
|
402
|
+
* order so that `--tool`, `--model`, `--think`, etc. reach `/solve` untouched.
|
|
403
|
+
*/
|
|
404
|
+
export function partitionFixArgs(rawArgs) {
|
|
405
|
+
const args = Array.isArray(rawArgs) ? rawArgs : [];
|
|
406
|
+
const result = {
|
|
407
|
+
repository: null,
|
|
408
|
+
repositoryRaw: null,
|
|
409
|
+
ciCd: false,
|
|
410
|
+
dryRun: false,
|
|
411
|
+
runSolve: true,
|
|
412
|
+
help: false,
|
|
413
|
+
version: false,
|
|
414
|
+
passthrough: [],
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
for (const arg of args) {
|
|
418
|
+
if (arg === '--ci-cd') {
|
|
419
|
+
result.ciCd = true;
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
422
|
+
if (arg === '--dry-run') {
|
|
423
|
+
result.dryRun = true;
|
|
424
|
+
continue;
|
|
425
|
+
}
|
|
426
|
+
if (arg === '--no-solve' || arg === '--no-auto-solve') {
|
|
427
|
+
result.runSolve = false;
|
|
428
|
+
continue;
|
|
429
|
+
}
|
|
430
|
+
if (arg === '--solve') {
|
|
431
|
+
result.runSolve = true;
|
|
432
|
+
continue;
|
|
433
|
+
}
|
|
434
|
+
if (arg === '--help' || arg === '-h') {
|
|
435
|
+
result.help = true;
|
|
436
|
+
continue;
|
|
437
|
+
}
|
|
438
|
+
if (arg === '--version') {
|
|
439
|
+
result.version = true;
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
442
|
+
// First bare GitHub repository argument becomes the target.
|
|
443
|
+
if (!result.repository && !arg.startsWith('-')) {
|
|
444
|
+
const repository = parseFixRepository(arg);
|
|
445
|
+
if (repository) {
|
|
446
|
+
result.repository = repository;
|
|
447
|
+
result.repositoryRaw = arg;
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
result.passthrough.push(arg);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
return result;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* The options `/fix` always turns on when handing the issue to `/solve`
|
|
459
|
+
* (issue #1733: "do similar to what `/solve --development-log --deep-analysis
|
|
460
|
+
* --auto-merge`"). `--development-log` replaces the superseded case-study
|
|
461
|
+
* workflow, while `--deep-analysis` re-injects the prompt paragraphs
|
|
462
|
+
* `buildCiCdIssueBody` conditionally omits from the issue text.
|
|
463
|
+
*/
|
|
464
|
+
export const FIX_SOLVE_OPTIONS = Object.freeze([SOLVE_OPTION_DEVELOPMENT_LOG, SOLVE_OPTION_DEEP_ANALYSIS, '--auto-merge']);
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* Build the argv passed to `solve.mjs`: the created issue URL, the options
|
|
468
|
+
* `/fix` always enables, and every forwarded option. An option the caller
|
|
469
|
+
* already passed through is not duplicated.
|
|
470
|
+
*/
|
|
471
|
+
export function buildSolveArgs({ issueUrl, passthrough = [] }) {
|
|
472
|
+
const args = [issueUrl];
|
|
473
|
+
for (const option of FIX_SOLVE_OPTIONS) {
|
|
474
|
+
if (!passthrough.includes(option)) {
|
|
475
|
+
args.push(option);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
args.push(...passthrough);
|
|
479
|
+
return args;
|
|
480
|
+
}
|