@link-assistant/hive-mind 2.10.4 → 2.11.0
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 +12 -0
- package/README.hi.md +5 -0
- package/README.md +5 -0
- package/README.ru.md +5 -0
- package/README.zh.md +4 -0
- package/package.json +1 -1
- package/src/exit-handler.lib.mjs +61 -44
- package/src/fix.ci-cd-issue.lib.mjs +137 -0
- package/src/fix.mjs +6 -116
- package/src/github-pr-state.lib.mjs +56 -0
- package/src/locales/en.lino +6 -3
- package/src/locales/hi.lino +6 -3
- package/src/locales/ru.lino +6 -3
- package/src/locales/zh.lino +6 -3
- package/src/session-monitor.docker-terminal.lib.mjs +118 -0
- package/src/session-monitor.lib.mjs +71 -158
- package/src/session-monitor.stale-executing.lib.mjs +151 -0
- package/src/solve.finalize.lib.mjs +23 -8
- package/src/solve.mjs +1 -1
- package/src/telegram-task-command.lib.mjs +47 -1
- package/src/work-session-formatting.lib.mjs +12 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @link-assistant/hive-mind
|
|
2
2
|
|
|
3
|
+
## 2.11.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 3613953: Add `/task --ci-cd <repository>` as a CI/CD remediation issue-only fallback for `/fix`.
|
|
8
|
+
|
|
9
|
+
## 2.10.5
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- 9772498: Stop reporting successful docker work sessions as failed. start-command can fabricate a detached-docker exit code from any `Exit Code: N` text the command itself printed (link-foundation/start#150), so the session monitor now trusts its own anchored log footer over `$ --status` and defers an uncorroborated docker failure for up to 60 seconds until the real footer is written.
|
|
14
|
+
|
|
3
15
|
## 2.10.4
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
package/README.hi.md
CHANGED
|
@@ -566,6 +566,11 @@ Examples:
|
|
|
566
566
|
`--no-solve` का उपयोग करें। विवरण के लिए
|
|
567
567
|
[स्वचालित CI/CD सुधार](docs/CI-CD-BEST-PRACTICES.md#automatic-cicd-remediation) देखें।
|
|
568
568
|
|
|
569
|
+
यदि पूरा `/fix` workflow उपलब्ध न हो, तो `/task --ci-cd <repository>` केवल वही CI/CD
|
|
570
|
+
issue-generation चरण चलाता है। यह बनाए गए issue का URL लौटाता है; सामान्य solve workflow
|
|
571
|
+
से remediation जारी रखने के लिए `/solve --development-log --deep-analysis --auto-merge`
|
|
572
|
+
से reply करें।
|
|
573
|
+
|
|
569
574
|
#### `/limits` - उपयोग सीमाएँ दिखाएँ
|
|
570
575
|
|
|
571
576
|
```
|
package/README.md
CHANGED
|
@@ -585,6 +585,11 @@ does not consume itself (e.g. `--tool`, `--model`, `--think`) is forwarded to
|
|
|
585
585
|
[Automatic CI/CD Remediation](docs/CI-CD-BEST-PRACTICES.md#automatic-cicd-remediation)
|
|
586
586
|
for details.
|
|
587
587
|
|
|
588
|
+
If the full `/fix` workflow is unavailable, `/task --ci-cd <repository>` runs
|
|
589
|
+
only the same CI/CD issue-generation step. It returns the created issue URL;
|
|
590
|
+
reply with `/solve --development-log --deep-analysis --auto-merge` to continue
|
|
591
|
+
the remediation through the normal solve workflow.
|
|
592
|
+
|
|
588
593
|
#### `/limits` - Show Usage Limits
|
|
589
594
|
|
|
590
595
|
```
|
package/README.ru.md
CHANGED
|
@@ -566,6 +566,11 @@ Examples:
|
|
|
566
566
|
`/solve`. Подробнее см.
|
|
567
567
|
[Автоматическое исправление CI/CD](docs/CI-CD-BEST-PRACTICES.md#automatic-cicd-remediation).
|
|
568
568
|
|
|
569
|
+
Если полный процесс `/fix` недоступен, `/task --ci-cd <repository>` выполняет
|
|
570
|
+
только тот же этап создания issue для CI/CD. Команда возвращает URL созданного
|
|
571
|
+
issue; ответьте `/solve --development-log --deep-analysis --auto-merge`, чтобы
|
|
572
|
+
продолжить исправление обычным процессом solve.
|
|
573
|
+
|
|
569
574
|
#### `/limits` — Показать лимиты использования
|
|
570
575
|
|
|
571
576
|
```
|
package/README.zh.md
CHANGED
|
@@ -560,6 +560,10 @@ issue 交给 `/solve --development-log --deep-analysis --auto-merge` 处理。
|
|
|
560
560
|
issue 的情况下预览,使用 `--no-solve` 可只创建 issue 而不启动 `/solve`。详见
|
|
561
561
|
[自动 CI/CD 修复](docs/CI-CD-BEST-PRACTICES.md#automatic-cicd-remediation)。
|
|
562
562
|
|
|
563
|
+
如果完整的 `/fix` 流程不可用,`/task --ci-cd <repository>` 只执行相同的 CI/CD issue
|
|
564
|
+
生成步骤。该命令会返回新建 issue 的 URL;回复
|
|
565
|
+
`/solve --development-log --deep-analysis --auto-merge` 即可通过常规 solve 流程继续修复。
|
|
566
|
+
|
|
563
567
|
#### `/limits` - 显示用量限制
|
|
564
568
|
|
|
565
569
|
```
|
package/package.json
CHANGED
package/src/exit-handler.lib.mjs
CHANGED
|
@@ -244,11 +244,22 @@ export const logActiveHandles = async (log = null) => {
|
|
|
244
244
|
* guidance for the pre-exit notifier.
|
|
245
245
|
*/
|
|
246
246
|
export const safeExit = async (code = 0, reason = 'Process completed', { skipPreExit = false, failureActionSection = null } = {}) => {
|
|
247
|
-
|
|
247
|
+
// Issue #2117: every best-effort step below is diagnostic housekeeping. It may
|
|
248
|
+
// fail, but it must never change the exit code the caller asked for — neither
|
|
249
|
+
// by masking a failure nor by turning a success into an uncaught exception.
|
|
250
|
+
try {
|
|
251
|
+
await showExitMessage(reason, code);
|
|
252
|
+
} catch (error) {
|
|
253
|
+
console.warn(`⚠️ Could not show exit message: ${error?.message || error}`);
|
|
254
|
+
}
|
|
248
255
|
|
|
249
256
|
// Issue #2090: collect the working session that is still uncollected (and the
|
|
250
257
|
// log tail produced after it) before the process goes away.
|
|
251
|
-
|
|
258
|
+
try {
|
|
259
|
+
await finalizeActiveDevelopmentLog({ force: true });
|
|
260
|
+
} catch {
|
|
261
|
+
// Best-effort finalization must never change the selected process exit.
|
|
262
|
+
}
|
|
252
263
|
|
|
253
264
|
if (!skipPreExit && code !== 0 && preExitFunction && !preExitHandlerRan) {
|
|
254
265
|
preExitHandlerRan = true;
|
|
@@ -267,7 +278,11 @@ export const safeExit = async (code = 0, reason = 'Process completed', { skipPre
|
|
|
267
278
|
// Issue #1431: Drain/unref active handles so the event loop exits naturally.
|
|
268
279
|
// This resolves the root causes of dangling ReadStream (stdin), Socket (undici),
|
|
269
280
|
// ChildProcess (command-stream), and WriteStream (stdout/stderr) handles.
|
|
270
|
-
|
|
281
|
+
try {
|
|
282
|
+
await drainHandles();
|
|
283
|
+
} catch {
|
|
284
|
+
// Best-effort handle draining must never change the selected process exit.
|
|
285
|
+
}
|
|
271
286
|
|
|
272
287
|
// Close Sentry to flush any pending events and allow the process to exit cleanly.
|
|
273
288
|
// Use Promise.race with a hard timeout to guarantee sentry.close() never hangs
|
|
@@ -291,7 +306,7 @@ export const safeExit = async (code = 0, reason = 'Process completed', { skipPre
|
|
|
291
306
|
/**
|
|
292
307
|
* Install global exit handlers to ensure log path is always shown
|
|
293
308
|
*/
|
|
294
|
-
export const installGlobalExitHandlers = () => {
|
|
309
|
+
export const installGlobalExitHandlers = ({ handleProcessErrors = true } = {}) => {
|
|
295
310
|
// Handle normal exit
|
|
296
311
|
process.on('exit', code => {
|
|
297
312
|
// Synchronous fallback - can't use async here
|
|
@@ -427,53 +442,55 @@ export const installGlobalExitHandlers = () => {
|
|
|
427
442
|
process.exit(143);
|
|
428
443
|
});
|
|
429
444
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
445
|
+
if (handleProcessErrors) {
|
|
446
|
+
// Handle uncaught exceptions
|
|
447
|
+
process.on('uncaughtException', async error => {
|
|
448
|
+
if (cleanupFunction) {
|
|
449
|
+
try {
|
|
450
|
+
await cleanupFunction();
|
|
451
|
+
} catch {
|
|
452
|
+
// Ignore cleanup errors on exception
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
if (logFunction) {
|
|
456
|
+
await logFunction(`\n❌ Uncaught Exception: ${error.message}`, { level: 'error' });
|
|
457
|
+
}
|
|
458
|
+
await showExitMessage('Uncaught exception occurred', 1);
|
|
433
459
|
try {
|
|
434
|
-
await
|
|
460
|
+
const sentry = await getSentry();
|
|
461
|
+
if (sentry && sentry.close) {
|
|
462
|
+
await Promise.race([sentry.close(2000), new Promise(resolve => setTimeout(resolve, 3000))]);
|
|
463
|
+
}
|
|
435
464
|
} catch {
|
|
436
|
-
// Ignore
|
|
465
|
+
// Ignore Sentry.close() errors
|
|
437
466
|
}
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
await logFunction(`\n❌ Uncaught Exception: ${error.message}`, { level: 'error' });
|
|
441
|
-
}
|
|
442
|
-
await showExitMessage('Uncaught exception occurred', 1);
|
|
443
|
-
try {
|
|
444
|
-
const sentry = await getSentry();
|
|
445
|
-
if (sentry && sentry.close) {
|
|
446
|
-
await Promise.race([sentry.close(2000), new Promise(resolve => setTimeout(resolve, 3000))]);
|
|
447
|
-
}
|
|
448
|
-
} catch {
|
|
449
|
-
// Ignore Sentry.close() errors
|
|
450
|
-
}
|
|
451
|
-
process.exit(1);
|
|
452
|
-
});
|
|
467
|
+
process.exit(1);
|
|
468
|
+
});
|
|
453
469
|
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
470
|
+
// Handle unhandled rejections
|
|
471
|
+
process.on('unhandledRejection', async reason => {
|
|
472
|
+
if (cleanupFunction) {
|
|
473
|
+
try {
|
|
474
|
+
await cleanupFunction();
|
|
475
|
+
} catch {
|
|
476
|
+
// Ignore cleanup errors on rejection
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
if (logFunction) {
|
|
480
|
+
await logFunction(`\n❌ Unhandled Rejection: ${reason}`, { level: 'error' });
|
|
481
|
+
}
|
|
482
|
+
await showExitMessage('Unhandled rejection occurred', 1);
|
|
457
483
|
try {
|
|
458
|
-
await
|
|
484
|
+
const sentry = await getSentry();
|
|
485
|
+
if (sentry && sentry.close) {
|
|
486
|
+
await Promise.race([sentry.close(2000), new Promise(resolve => setTimeout(resolve, 3000))]);
|
|
487
|
+
}
|
|
459
488
|
} catch {
|
|
460
|
-
// Ignore
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
|
-
if (logFunction) {
|
|
464
|
-
await logFunction(`\n❌ Unhandled Rejection: ${reason}`, { level: 'error' });
|
|
465
|
-
}
|
|
466
|
-
await showExitMessage('Unhandled rejection occurred', 1);
|
|
467
|
-
try {
|
|
468
|
-
const sentry = await getSentry();
|
|
469
|
-
if (sentry && sentry.close) {
|
|
470
|
-
await Promise.race([sentry.close(2000), new Promise(resolve => setTimeout(resolve, 3000))]);
|
|
489
|
+
// Ignore Sentry.close() errors
|
|
471
490
|
}
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
process.exit(1);
|
|
476
|
-
});
|
|
491
|
+
process.exit(1);
|
|
492
|
+
});
|
|
493
|
+
}
|
|
477
494
|
};
|
|
478
495
|
|
|
479
496
|
/**
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared GitHub-backed CI/CD issue generation for `/fix --ci-cd` and
|
|
3
|
+
* `/task --ci-cd` (issues #1733 and #2121).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { spawn } from 'child_process';
|
|
7
|
+
import { CI_CD_ISSUE_LABELS, CI_CD_ISSUE_TYPE, buildCiCdIssueBody, buildCiCdIssueTitle } from './fix.ci-cd.lib.mjs';
|
|
8
|
+
import { createTaskIssue } from './task.issue-creation.lib.mjs';
|
|
9
|
+
|
|
10
|
+
function runCommand(command, args, options = {}) {
|
|
11
|
+
return new Promise(resolve => {
|
|
12
|
+
const child = spawn(command, args, {
|
|
13
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
14
|
+
env: process.env,
|
|
15
|
+
...options,
|
|
16
|
+
});
|
|
17
|
+
let stdout = '';
|
|
18
|
+
let stderr = '';
|
|
19
|
+
child.stdout.on('data', data => {
|
|
20
|
+
stdout += data.toString();
|
|
21
|
+
});
|
|
22
|
+
child.stderr.on('data', data => {
|
|
23
|
+
stderr += data.toString();
|
|
24
|
+
});
|
|
25
|
+
child.on('error', error => {
|
|
26
|
+
resolve({ code: 1, stdout, stderr: stderr || error.message });
|
|
27
|
+
});
|
|
28
|
+
child.on('close', code => {
|
|
29
|
+
resolve({ code, stdout, stderr });
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function commandOutput(run, command, args) {
|
|
35
|
+
const result = await run(command, args);
|
|
36
|
+
if (result.code !== 0) {
|
|
37
|
+
const output = `${result.stderr || ''}${result.stdout || ''}`.trim();
|
|
38
|
+
throw new Error(output || `${command} exited with code ${result.code}`);
|
|
39
|
+
}
|
|
40
|
+
return result.stdout.trim();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function detectLanguages(repository, run, warn) {
|
|
44
|
+
try {
|
|
45
|
+
const json = await commandOutput(run, 'gh', ['api', `repos/${repository.fullName}/languages`]);
|
|
46
|
+
return JSON.parse(json);
|
|
47
|
+
} catch (error) {
|
|
48
|
+
warn(`⚠️ Could not detect languages: ${error.message}`);
|
|
49
|
+
return {};
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function getDefaultBranch(repository, run, warn) {
|
|
54
|
+
try {
|
|
55
|
+
return await commandOutput(run, 'gh', ['api', `repos/${repository.fullName}`, '--jq', '.default_branch']);
|
|
56
|
+
} catch (error) {
|
|
57
|
+
warn(`⚠️ Could not determine default branch: ${error.message}`);
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function getLatestCommit(repository, branch, run, warn) {
|
|
63
|
+
if (!branch) return null;
|
|
64
|
+
try {
|
|
65
|
+
const json = await commandOutput(run, 'gh', ['api', `repos/${repository.fullName}/commits/${branch}`, '--jq', '{sha: .sha, message: .commit.message, url: .html_url}']);
|
|
66
|
+
return JSON.parse(json);
|
|
67
|
+
} catch (error) {
|
|
68
|
+
warn(`⚠️ Could not fetch latest commit: ${error.message}`);
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const RUNS_JQ = '[.workflow_runs[] | {name: .name, status: .status, conclusion: .conclusion, html_url: .html_url, head_sha: .head_sha}]';
|
|
74
|
+
|
|
75
|
+
async function getRunsForCommit(repository, sha, run, warn) {
|
|
76
|
+
if (!sha) return [];
|
|
77
|
+
try {
|
|
78
|
+
const json = await commandOutput(run, 'gh', ['api', `repos/${repository.fullName}/actions/runs?head_sha=${sha}&per_page=100`, '--jq', RUNS_JQ]);
|
|
79
|
+
const parsed = JSON.parse(json);
|
|
80
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
81
|
+
} catch (error) {
|
|
82
|
+
warn(`⚠️ Could not fetch CI/CD runs: ${error.message}`);
|
|
83
|
+
return [];
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function getRecentBranchRuns(repository, branch, run, warn) {
|
|
88
|
+
if (!branch) return [];
|
|
89
|
+
try {
|
|
90
|
+
const json = await commandOutput(run, 'gh', ['api', `repos/${repository.fullName}/actions/runs?branch=${encodeURIComponent(branch)}&per_page=20`, '--jq', RUNS_JQ]);
|
|
91
|
+
const parsed = JSON.parse(json);
|
|
92
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
93
|
+
} catch (error) {
|
|
94
|
+
warn(`⚠️ Could not fetch recent CI/CD runs for branch ${branch}: ${error.message}`);
|
|
95
|
+
return [];
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export async function prepareCiCdIssue({ repository, run = runCommand, warn = message => console.warn(message) }) {
|
|
100
|
+
const [languages, defaultBranch] = await Promise.all([detectLanguages(repository, run, warn), getDefaultBranch(repository, run, warn)]);
|
|
101
|
+
const commit = await getLatestCommit(repository, defaultBranch, run, warn);
|
|
102
|
+
let runs = await getRunsForCommit(repository, commit?.sha, run, warn);
|
|
103
|
+
let runsSource = 'commit';
|
|
104
|
+
|
|
105
|
+
if (runs.length === 0) {
|
|
106
|
+
const branchRuns = await getRecentBranchRuns(repository, defaultBranch, run, warn);
|
|
107
|
+
if (branchRuns.length > 0) {
|
|
108
|
+
runs = branchRuns;
|
|
109
|
+
runsSource = 'branch';
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
repository,
|
|
115
|
+
defaultBranch,
|
|
116
|
+
commit,
|
|
117
|
+
runs,
|
|
118
|
+
languages,
|
|
119
|
+
runsSource,
|
|
120
|
+
title: buildCiCdIssueTitle(),
|
|
121
|
+
body: buildCiCdIssueBody({ repository, defaultBranch, commit, runs, languages, runsSource }),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export async function createCiCdIssue({ repository, prepared = null, run = runCommand, log = null, warn = message => console.warn(message) }) {
|
|
126
|
+
const issueDraft = prepared || (await prepareCiCdIssue({ repository, run, warn }));
|
|
127
|
+
const issue = await createTaskIssue({
|
|
128
|
+
repository,
|
|
129
|
+
title: issueDraft.title,
|
|
130
|
+
body: issueDraft.body,
|
|
131
|
+
issueType: CI_CD_ISSUE_TYPE,
|
|
132
|
+
labels: [...CI_CD_ISSUE_LABELS],
|
|
133
|
+
run,
|
|
134
|
+
log,
|
|
135
|
+
});
|
|
136
|
+
return { ...issue, prepared: issueDraft };
|
|
137
|
+
}
|
package/src/fix.mjs
CHANGED
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
import path from 'path';
|
|
17
17
|
import { spawn } from 'child_process';
|
|
18
18
|
import { fileURLToPath } from 'url';
|
|
19
|
-
import {
|
|
19
|
+
import { buildSolveArgs, partitionFixArgs, summarizeRunFailures } from './fix.ci-cd.lib.mjs';
|
|
20
|
+
import { createCiCdIssue, prepareCiCdIssue } from './fix.ci-cd-issue.lib.mjs';
|
|
20
21
|
import { setupStdioLogInterceptor } from './lib.mjs';
|
|
21
22
|
|
|
22
23
|
setupStdioLogInterceptor();
|
|
@@ -44,95 +45,6 @@ Examples:
|
|
|
44
45
|
fix.mjs owner/repo --ci-cd --think max --no-solve`);
|
|
45
46
|
}
|
|
46
47
|
|
|
47
|
-
function runCommand(command, args, options = {}) {
|
|
48
|
-
return new Promise(resolve => {
|
|
49
|
-
const child = spawn(command, args, {
|
|
50
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
51
|
-
env: process.env,
|
|
52
|
-
...options,
|
|
53
|
-
});
|
|
54
|
-
let stdout = '';
|
|
55
|
-
let stderr = '';
|
|
56
|
-
child.stdout.on('data', data => {
|
|
57
|
-
stdout += data.toString();
|
|
58
|
-
});
|
|
59
|
-
child.stderr.on('data', data => {
|
|
60
|
-
stderr += data.toString();
|
|
61
|
-
});
|
|
62
|
-
child.on('error', error => {
|
|
63
|
-
resolve({ code: 1, stdout, stderr: stderr || error.message });
|
|
64
|
-
});
|
|
65
|
-
child.on('close', code => {
|
|
66
|
-
resolve({ code, stdout, stderr });
|
|
67
|
-
});
|
|
68
|
-
});
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
async function commandOutput(command, args) {
|
|
72
|
-
const result = await runCommand(command, args);
|
|
73
|
-
if (result.code !== 0) {
|
|
74
|
-
const output = `${result.stderr || ''}${result.stdout || ''}`.trim();
|
|
75
|
-
throw new Error(output || `${command} exited with code ${result.code}`);
|
|
76
|
-
}
|
|
77
|
-
return result.stdout.trim();
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
async function detectLanguages(repository) {
|
|
81
|
-
try {
|
|
82
|
-
const json = await commandOutput('gh', ['api', `repos/${repository.fullName}/languages`]);
|
|
83
|
-
return JSON.parse(json);
|
|
84
|
-
} catch (error) {
|
|
85
|
-
console.warn(`⚠️ Could not detect languages: ${error.message}`);
|
|
86
|
-
return {};
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
async function getDefaultBranch(repository) {
|
|
91
|
-
try {
|
|
92
|
-
return await commandOutput('gh', ['api', `repos/${repository.fullName}`, '--jq', '.default_branch']);
|
|
93
|
-
} catch (error) {
|
|
94
|
-
console.warn(`⚠️ Could not determine default branch: ${error.message}`);
|
|
95
|
-
return null;
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
async function getLatestCommit(repository, branch) {
|
|
100
|
-
if (!branch) return null;
|
|
101
|
-
try {
|
|
102
|
-
const json = await commandOutput('gh', ['api', `repos/${repository.fullName}/commits/${branch}`, '--jq', '{sha: .sha, message: .commit.message, url: .html_url}']);
|
|
103
|
-
return JSON.parse(json);
|
|
104
|
-
} catch (error) {
|
|
105
|
-
console.warn(`⚠️ Could not fetch latest commit: ${error.message}`);
|
|
106
|
-
return null;
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
const RUNS_JQ = '[.workflow_runs[] | {name: .name, status: .status, conclusion: .conclusion, html_url: .html_url, head_sha: .head_sha}]';
|
|
111
|
-
|
|
112
|
-
async function getRunsForCommit(repository, sha) {
|
|
113
|
-
if (!sha) return [];
|
|
114
|
-
try {
|
|
115
|
-
const json = await commandOutput('gh', ['api', `repos/${repository.fullName}/actions/runs?head_sha=${sha}&per_page=100`, '--jq', RUNS_JQ]);
|
|
116
|
-
const parsed = JSON.parse(json);
|
|
117
|
-
return Array.isArray(parsed) ? parsed : [];
|
|
118
|
-
} catch (error) {
|
|
119
|
-
console.warn(`⚠️ Could not fetch CI/CD runs: ${error.message}`);
|
|
120
|
-
return [];
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
async function getRecentBranchRuns(repository, branch) {
|
|
125
|
-
if (!branch) return [];
|
|
126
|
-
try {
|
|
127
|
-
const json = await commandOutput('gh', ['api', `repos/${repository.fullName}/actions/runs?branch=${encodeURIComponent(branch)}&per_page=20`, '--jq', RUNS_JQ]);
|
|
128
|
-
const parsed = JSON.parse(json);
|
|
129
|
-
return Array.isArray(parsed) ? parsed : [];
|
|
130
|
-
} catch (error) {
|
|
131
|
-
console.warn(`⚠️ Could not fetch recent CI/CD runs for branch ${branch}: ${error.message}`);
|
|
132
|
-
return [];
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
48
|
function resolveSolveCommand() {
|
|
137
49
|
return path.join(__dirname, 'solve.mjs');
|
|
138
50
|
}
|
|
@@ -171,29 +83,14 @@ async function main() {
|
|
|
171
83
|
const repository = parsed.repository;
|
|
172
84
|
console.log(`🔧 /fix --ci-cd for ${repository.fullName}`);
|
|
173
85
|
|
|
174
|
-
const
|
|
175
|
-
const commit
|
|
176
|
-
let runs = await getRunsForCommit(repository, commit?.sha);
|
|
177
|
-
let runsSource = 'commit';
|
|
178
|
-
|
|
179
|
-
// Release/tag commits frequently produce no runs of their own. Fall back to
|
|
180
|
-
// the most recent runs on the default branch so the issue stays actionable.
|
|
181
|
-
if (runs.length === 0) {
|
|
182
|
-
const branchRuns = await getRecentBranchRuns(repository, defaultBranch);
|
|
183
|
-
if (branchRuns.length > 0) {
|
|
184
|
-
runs = branchRuns;
|
|
185
|
-
runsSource = 'branch';
|
|
186
|
-
}
|
|
187
|
-
}
|
|
86
|
+
const prepared = await prepareCiCdIssue({ repository });
|
|
87
|
+
const { defaultBranch, commit, runs, runsSource, title, body } = prepared;
|
|
188
88
|
|
|
189
89
|
const { total, failing } = summarizeRunFailures(runs);
|
|
190
90
|
console.log(` Default branch: ${defaultBranch || 'unknown'}`);
|
|
191
91
|
console.log(` Latest commit: ${commit?.sha ? commit.sha.slice(0, 7) : 'unknown'}`);
|
|
192
92
|
console.log(` CI/CD runs: ${total} (${failing} not passing)${runsSource === 'branch' ? ' [recent branch runs]' : ''}`);
|
|
193
93
|
|
|
194
|
-
const title = buildCiCdIssueTitle();
|
|
195
|
-
const body = buildCiCdIssueBody({ repository, defaultBranch, commit, runs, languages, runsSource });
|
|
196
|
-
|
|
197
94
|
if (parsed.dryRun) {
|
|
198
95
|
console.log('\n--- DRY RUN: issue that would be created ---\n');
|
|
199
96
|
console.log(`Title: ${title}\n`);
|
|
@@ -202,16 +99,9 @@ async function main() {
|
|
|
202
99
|
}
|
|
203
100
|
|
|
204
101
|
console.log('\n📝 Creating remediation issue...');
|
|
205
|
-
const
|
|
206
|
-
const issue = await createTaskIssue({
|
|
102
|
+
const issue = await createCiCdIssue({
|
|
207
103
|
repository,
|
|
208
|
-
|
|
209
|
-
body,
|
|
210
|
-
// The Bug type is what makes /solve --deep-analysis emit the root-cause
|
|
211
|
-
// instructions this body omits (issue #1733).
|
|
212
|
-
issueType: CI_CD_ISSUE_TYPE,
|
|
213
|
-
labels: [...CI_CD_ISSUE_LABELS],
|
|
214
|
-
run: runCommand,
|
|
104
|
+
prepared,
|
|
215
105
|
log: message => console.log(message),
|
|
216
106
|
});
|
|
217
107
|
console.log(`✅ Created issue: ${issue.url}`);
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { execGhWithRetry } from './github-rate-limit.lib.mjs';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Resolve the externally visible state of a GitHub pull request.
|
|
5
|
+
*
|
|
6
|
+
* A strict URL parser keeps the command limited to GitHub owner/repo/PR
|
|
7
|
+
* identifiers. The REST response exposes `merged` and `merged_at`, which are
|
|
8
|
+
* stronger evidence of goal completion than the detached runner's exit code.
|
|
9
|
+
*
|
|
10
|
+
* @param {string|null} pullRequestUrl
|
|
11
|
+
* @param {Object} [options]
|
|
12
|
+
* @param {Function} [options.lookupPullRequestState] - Test/application override
|
|
13
|
+
* @param {boolean} [options.verbose]
|
|
14
|
+
* @returns {Promise<{merged:boolean, mergedAt:string|null, state:string|null}|null>}
|
|
15
|
+
*/
|
|
16
|
+
export async function resolvePullRequestState(pullRequestUrl, { lookupPullRequestState = null, verbose = false } = {}) {
|
|
17
|
+
if (!pullRequestUrl) return null;
|
|
18
|
+
if (typeof lookupPullRequestState === 'function') {
|
|
19
|
+
return (await lookupPullRequestState(pullRequestUrl)) || null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const match = String(pullRequestUrl).match(/^https:\/\/github\.com\/([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)\/pull\/(\d+)(?:[/?#].*)?$/i);
|
|
23
|
+
if (!match) {
|
|
24
|
+
if (verbose) console.log(`[VERBOSE] Cannot resolve PR state for unrecognized URL: ${pullRequestUrl}`);
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const [, owner, repo, number] = match;
|
|
29
|
+
try {
|
|
30
|
+
const { stdout } = await execGhWithRetry(`gh api repos/${owner}/${repo}/pulls/${number} --jq '{merged: .merged, mergedAt: .merged_at, state: .state}'`, {
|
|
31
|
+
execOptions: {
|
|
32
|
+
encoding: 'utf8',
|
|
33
|
+
maxBuffer: 1024 * 1024,
|
|
34
|
+
},
|
|
35
|
+
label: `gh api pull request state (${owner}/${repo}#${number})`,
|
|
36
|
+
});
|
|
37
|
+
const state = JSON.parse(stdout);
|
|
38
|
+
return {
|
|
39
|
+
merged: state?.merged === true,
|
|
40
|
+
mergedAt: state?.mergedAt || null,
|
|
41
|
+
state: state?.state || null,
|
|
42
|
+
};
|
|
43
|
+
} catch (error) {
|
|
44
|
+
if (verbose) console.log(`[VERBOSE] Pull request state lookup failed for ${pullRequestUrl}: ${error?.message || error}`);
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function resolveFailedSessionPullRequestState({ pullRequestUrl, outcome, lookupPullRequestState = null, verbose = false, sessionName = 'unknown', exitCode = null, status = null, logPath = null } = {}) {
|
|
50
|
+
if (!outcome?.failed || outcome.killed || !pullRequestUrl) return null;
|
|
51
|
+
const state = await resolvePullRequestState(pullRequestUrl, { lookupPullRequestState, verbose });
|
|
52
|
+
if (verbose && state) {
|
|
53
|
+
console.log(`[VERBOSE] Completion evidence for ${sessionName}: exitCode=${exitCode}, status=${status || 'unknown'}, pullRequest=${pullRequestUrl}, merged=${state.merged}, mergedAt=${state.mergedAt || 'unknown'}, logPath=${logPath || 'unknown'}`);
|
|
54
|
+
}
|
|
55
|
+
return state;
|
|
56
|
+
}
|
package/src/locales/en.lino
CHANGED
|
@@ -556,9 +556,9 @@ en
|
|
|
556
556
|
locked
|
|
557
557
|
options "🔒 Locked options: `{{options}}`"
|
|
558
558
|
task
|
|
559
|
-
enabled "*/task* - Create a GitHub issue from
|
|
560
|
-
usage "Usage: `/task <github-repository-url>` followed by issue text, or
|
|
561
|
-
example "Example: `/task https://github.com/owner/repo`
|
|
559
|
+
enabled "*/task* - Create a GitHub issue from text or CI/CD context"
|
|
560
|
+
usage "Usage: `/task <github-repository-url>` followed by issue text, or `/task --ci-cd <github-repository-url>`"
|
|
561
|
+
example "Example: `/task --ci-cd https://github.com/owner/repo` creates only the CI/CD remediation issue"
|
|
562
562
|
disabled "*/task* / */split* - ❌ Disabled"
|
|
563
563
|
split
|
|
564
564
|
enabled "*/split* - Split a GitHub issue into smaller issues"
|
|
@@ -649,6 +649,9 @@ en
|
|
|
649
649
|
executing "⏳ Executing..."
|
|
650
650
|
finished "Work session finished successfully"
|
|
651
651
|
failed "Work session failed (exit code: {{exitCode}})"
|
|
652
|
+
merged_but_failed "Pull request merged, but the work session exited with code: {{exitCode}}"
|
|
653
|
+
merged_success "The requested pull request was merged successfully."
|
|
654
|
+
runner_also_failed "The runner also failed; its exit code is preserved for investigation."
|
|
652
655
|
killed "Work session {{reason}}{{exitSuffix}}"
|
|
653
656
|
stopped "Work session stopped by user{{requestedBy}}{{exitSuffix}}"
|
|
654
657
|
duration
|
package/src/locales/hi.lino
CHANGED
|
@@ -556,9 +556,9 @@ hi
|
|
|
556
556
|
locked
|
|
557
557
|
options "🔒 लॉक किए गए विकल्प: `{{options}}`"
|
|
558
558
|
task
|
|
559
|
-
enabled "*/task* -
|
|
560
|
-
usage "उपयोग: `/task <github-repository-url>` के बाद issue text, या `/task
|
|
561
|
-
example "उदाहरण: `/task https://github.com/owner/repo
|
|
559
|
+
enabled "*/task* - text या CI/CD context से GitHub issue बनाएँ"
|
|
560
|
+
usage "उपयोग: `/task <github-repository-url>` के बाद issue text, या `/task --ci-cd <github-repository-url>`"
|
|
561
|
+
example "उदाहरण: `/task --ci-cd https://github.com/owner/repo` केवल CI/CD remediation issue बनाता है"
|
|
562
562
|
disabled "*/task* / */split* - ❌ अक्षम"
|
|
563
563
|
split
|
|
564
564
|
enabled "*/split* - GitHub issue को छोटे issues में बाँटें"
|
|
@@ -649,6 +649,9 @@ hi
|
|
|
649
649
|
executing "⏳ चल रहा है..."
|
|
650
650
|
finished "कार्य सत्र सफलतापूर्वक पूरा हुआ"
|
|
651
651
|
failed "कार्य सत्र विफल हुआ (exit code: {{exitCode}})"
|
|
652
|
+
merged_but_failed "पुल अनुरोध मर्ज हो गया, लेकिन कार्य सत्र कोड {{exitCode}} के साथ समाप्त हुआ"
|
|
653
|
+
merged_success "अनुरोधित पुल अनुरोध सफलतापूर्वक मर्ज हो गया।"
|
|
654
|
+
runner_also_failed "रनर भी विफल हुआ; जाँच के लिए उसका exit code सुरक्षित रखा गया है।"
|
|
652
655
|
killed "कार्य सत्र रोका गया: {{reason}}{{exitSuffix}}"
|
|
653
656
|
stopped "कार्य सत्र उपयोगकर्ता द्वारा रोका गया{{requestedBy}}{{exitSuffix}}"
|
|
654
657
|
duration
|
package/src/locales/ru.lino
CHANGED
|
@@ -556,9 +556,9 @@ ru
|
|
|
556
556
|
locked
|
|
557
557
|
options "🔒 Заблокированные опции: `{{options}}`"
|
|
558
558
|
task
|
|
559
|
-
enabled "*/task* - Создать
|
|
560
|
-
usage "Использование: `/task <github-repository-url>` с текстом
|
|
561
|
-
example "Пример: `/task https://github.com/owner/repo
|
|
559
|
+
enabled "*/task* - Создать issue GitHub из текста или контекста CI/CD"
|
|
560
|
+
usage "Использование: `/task <github-repository-url>` с текстом или `/task --ci-cd <github-repository-url>`"
|
|
561
|
+
example "Пример: `/task --ci-cd https://github.com/owner/repo` создаёт только issue для исправления CI/CD"
|
|
562
562
|
disabled "*/task* / */split* - ❌ Отключено"
|
|
563
563
|
split
|
|
564
564
|
enabled "*/split* - Разделить задачу GitHub на меньшие задачи"
|
|
@@ -649,6 +649,9 @@ ru
|
|
|
649
649
|
executing "⏳ Выполняется..."
|
|
650
650
|
finished "Рабочий сеанс успешно завершен"
|
|
651
651
|
failed "Рабочий сеанс завершился с ошибкой (код выхода: {{exitCode}})"
|
|
652
|
+
merged_but_failed "Пул-реквест объединён, но рабочий сеанс завершился с кодом: {{exitCode}}"
|
|
653
|
+
merged_success "Запрошенный пул-реквест успешно объединён."
|
|
654
|
+
runner_also_failed "Средство запуска также завершилось с ошибкой; код выхода сохранён для расследования."
|
|
652
655
|
killed "Рабочий сеанс остановлен: {{reason}}{{exitSuffix}}"
|
|
653
656
|
stopped "Рабочий сеанс остановлен пользователем{{requestedBy}}{{exitSuffix}}"
|
|
654
657
|
duration
|