@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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/hive-mind",
3
- "version": "2.10.4",
3
+ "version": "2.11.0",
4
4
  "description": "AI-powered issue solver and hive mind for collaborative problem solving",
5
5
  "main": "src/hive.mjs",
6
6
  "type": "module",
@@ -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
- await showExitMessage(reason, code);
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
- await finalizeActiveDevelopmentLog({ force: true });
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
- await drainHandles();
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
- // Handle uncaught exceptions
431
- process.on('uncaughtException', async error => {
432
- if (cleanupFunction) {
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 cleanupFunction();
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 cleanup errors on exception
465
+ // Ignore Sentry.close() errors
437
466
  }
438
- }
439
- if (logFunction) {
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
- // Handle unhandled rejections
455
- process.on('unhandledRejection', async reason => {
456
- if (cleanupFunction) {
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 cleanupFunction();
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 cleanup errors on rejection
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
- } catch {
473
- // Ignore Sentry.close() errors
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 { CI_CD_ISSUE_LABELS, CI_CD_ISSUE_TYPE, buildCiCdIssueBody, buildCiCdIssueTitle, buildSolveArgs, partitionFixArgs, summarizeRunFailures } from './fix.ci-cd.lib.mjs';
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 [languages, defaultBranch] = await Promise.all([detectLanguages(repository), getDefaultBranch(repository)]);
175
- const commit = await getLatestCommit(repository, defaultBranch);
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 { createTaskIssue } = await import('./task.issue-creation.lib.mjs');
206
- const issue = await createTaskIssue({
102
+ const issue = await createCiCdIssue({
207
103
  repository,
208
- title,
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
+ }
@@ -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 a repository link and issue text"
560
- usage "Usage: `/task <github-repository-url>` followed by issue text, or reply with `/task`"
561
- example "Example: `/task https://github.com/owner/repo` then the issue text on following lines"
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
@@ -556,9 +556,9 @@ hi
556
556
  locked
557
557
  options "🔒 लॉक किए गए विकल्प: `{{options}}`"
558
558
  task
559
- enabled "*/task* - repository लिंक और issue text से GitHub issue बनाएँ"
560
- usage "उपयोग: `/task <github-repository-url>` के बाद issue text, या `/task` से reply करें"
561
- example "उदाहरण: `/task https://github.com/owner/repo`, फिर अगली पंक्तियों में issue text"
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
@@ -556,9 +556,9 @@ ru
556
556
  locked
557
557
  options "🔒 Заблокированные опции: `{{options}}`"
558
558
  task
559
- enabled "*/task* - Создать задачу GitHub из ссылки на репозиторий и текста задачи"
560
- usage "Использование: `/task <github-repository-url>` с текстом задачи после команды или ответом `/task`"
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