@link-assistant/hive-mind 1.60.0 → 1.62.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.
@@ -0,0 +1,589 @@
1
+ #!/usr/bin/env node
2
+ // Qwen Code CLI-related utility functions
3
+
4
+ // Check if use is already defined (when imported from solve.mjs)
5
+ // If not, fetch it (when running standalone)
6
+ if (typeof globalThis.use === 'undefined') {
7
+ globalThis.use = (await eval(await (await fetch('https://unpkg.com/use-m/use.js')).text())).use;
8
+ }
9
+
10
+ const { $ } = await use('command-stream');
11
+ const fs = (await use('fs')).promises;
12
+ const path = (await use('path')).default;
13
+ const os = (await use('os')).default;
14
+
15
+ import { log } from './lib.mjs';
16
+ import { reportError } from './sentry.lib.mjs';
17
+ import { timeouts, retryLimits } from './config.lib.mjs';
18
+ import { detectUsageLimit, formatUsageLimitMessage } from './usage-limit.lib.mjs';
19
+ import { sanitizeObjectStrings } from './unicode-sanitization.lib.mjs';
20
+ import { qwenModels, defaultModels } from './models/index.mjs';
21
+ import { checkPlaywrightMcpPackageAvailability } from './playwright-mcp.lib.mjs';
22
+ import { classifyRetryableError, getRetryDelayMs, maybeSwitchToFallbackModel, waitWithCountdown } from './tool-retry.lib.mjs';
23
+
24
+ export const mapModelToId = model => qwenModels[model] || model;
25
+
26
+ export const checkPlaywrightMcpAvailability = checkPlaywrightMcpPackageAvailability;
27
+
28
+ const shellQuote = value => `'${String(value).replace(/'/g, "'\\''")}'`;
29
+
30
+ const getCommandResultOutput = result => `${result?.stdout?.toString() || ''}${result?.stderr?.toString() || ''}`;
31
+
32
+ const isQwenAuthError = output => {
33
+ const text = (output || '').toString().toLowerCase();
34
+ return text.includes('401') || text.includes('unauthorized') || text.includes('authentication') || text.includes('auth') || text.includes('login') || text.includes('api key') || text.includes('oauth free tier');
35
+ };
36
+
37
+ const stringifyErrorValue = value => {
38
+ if (value === null || value === undefined) return '';
39
+ if (typeof value === 'string') return value;
40
+ if (typeof value?.message === 'string') return value.message;
41
+ if (typeof value?.error?.message === 'string') return value.error.message;
42
+ try {
43
+ return JSON.stringify(value);
44
+ } catch {
45
+ return String(value);
46
+ }
47
+ };
48
+
49
+ const getNestedValue = (object, pathParts) => {
50
+ let cursor = object;
51
+ for (const part of pathParts) {
52
+ if (!cursor || typeof cursor !== 'object') return undefined;
53
+ cursor = cursor[part];
54
+ }
55
+ return cursor;
56
+ };
57
+
58
+ const findFirstValue = (object, paths) => {
59
+ for (const pathParts of paths) {
60
+ const value = getNestedValue(object, pathParts);
61
+ if (value !== undefined && value !== null && value !== '') return value;
62
+ }
63
+ return null;
64
+ };
65
+
66
+ const extractTextFragments = value => {
67
+ if (typeof value === 'string') return [value];
68
+ if (!value || typeof value !== 'object') return [];
69
+
70
+ if (Array.isArray(value)) {
71
+ return value.flatMap(item => extractTextFragments(item));
72
+ }
73
+
74
+ const fragments = [];
75
+ for (const key of ['text', 'result', 'response', 'content']) {
76
+ if (Object.hasOwn(value, key)) {
77
+ fragments.push(...extractTextFragments(value[key]));
78
+ }
79
+ }
80
+ if (value.message) fragments.push(...extractTextFragments(value.message));
81
+ return fragments.filter(Boolean);
82
+ };
83
+
84
+ const createQwenParserState = state => ({
85
+ buffer: state?.buffer || '',
86
+ plainText: state?.plainText || '',
87
+ parsedEvents: Array.isArray(state?.parsedEvents) ? [...state.parsedEvents] : [],
88
+ eventCounts: { ...(state?.eventCounts || {}) },
89
+ errors: Array.isArray(state?.errors) ? [...state.errors] : [],
90
+ sessionId: state?.sessionId || null,
91
+ lastTextContent: state?.lastTextContent || '',
92
+ });
93
+
94
+ const addQwenEventToState = (state, rawEvent) => {
95
+ const event = sanitizeObjectStrings(rawEvent);
96
+ state.parsedEvents.push(event);
97
+
98
+ const eventType = event?.type || event?.event || 'unknown';
99
+ state.eventCounts[eventType] = (state.eventCounts[eventType] || 0) + 1;
100
+
101
+ const sessionId = findFirstValue(event, [['session_id'], ['sessionId'], ['thread_id'], ['threadId'], ['conversation_id'], ['conversationId'], ['session', 'id'], ['message', 'session_id'], ['message', 'sessionId']]);
102
+ if (sessionId) state.sessionId = String(sessionId);
103
+
104
+ const isErrorEvent = eventType === 'error' || event?.subtype === 'error' || event?.is_error === true || event?.error;
105
+ if (!isErrorEvent) {
106
+ const textFragments = extractTextFragments(event);
107
+ if (textFragments.length > 0) {
108
+ state.lastTextContent = textFragments[textFragments.length - 1];
109
+ }
110
+ }
111
+
112
+ if (isErrorEvent) {
113
+ const errorMessage = stringifyErrorValue(event?.error || event?.message || event?.result || event);
114
+ state.errors.push({
115
+ type: eventType,
116
+ subtype: event?.subtype || null,
117
+ message: errorMessage || 'Qwen Code emitted an error event',
118
+ isAuthError: isQwenAuthError(errorMessage),
119
+ });
120
+ }
121
+ };
122
+
123
+ export const parseQwenStreamJsonOutput = (output, state = {}) => {
124
+ const nextState = createQwenParserState(state);
125
+ const text = output?.toString?.() ?? String(output || '');
126
+ nextState.plainText += text;
127
+
128
+ const parseCandidate = value => {
129
+ const trimmed = value.trim();
130
+ if (!trimmed) return true;
131
+
132
+ try {
133
+ const parsed = JSON.parse(trimmed);
134
+ if (Array.isArray(parsed)) {
135
+ for (const item of parsed) addQwenEventToState(nextState, item);
136
+ } else {
137
+ addQwenEventToState(nextState, parsed);
138
+ }
139
+ return true;
140
+ } catch {
141
+ return false;
142
+ }
143
+ };
144
+
145
+ const combined = `${nextState.buffer}${text}`;
146
+ nextState.buffer = '';
147
+
148
+ const lines = combined.split(/\r?\n/);
149
+ for (let index = 0; index < lines.length; index++) {
150
+ const line = lines[index];
151
+ const isLastLine = index === lines.length - 1;
152
+ if (!line.trim()) continue;
153
+
154
+ const parsed = parseCandidate(line);
155
+ if (!parsed && isLastLine) {
156
+ nextState.buffer = line;
157
+ }
158
+ }
159
+
160
+ return nextState;
161
+ };
162
+
163
+ // Function to validate Qwen connection
164
+ export const validateQwenConnection = async (model = defaultModels.qwen, qwenPath = process.env.QWEN_PATH || 'qwen') => {
165
+ const mappedModel = mapModelToId(model);
166
+
167
+ try {
168
+ await log('šŸ” Validating Qwen Code connection...');
169
+
170
+ try {
171
+ const versionResult = await $`timeout ${Math.floor(timeouts.qwenCli / 1000)} ${qwenPath} --version`;
172
+ if (versionResult.code === 0) {
173
+ const version = versionResult.stdout?.toString().trim();
174
+ await log(`šŸ“¦ Qwen Code CLI version: ${version}`);
175
+ }
176
+ } catch (versionError) {
177
+ await log(`āš ļø Qwen Code version check failed (${versionError.code}), proceeding with connection test...`, { level: 'warning' });
178
+ }
179
+
180
+ const testResult = await $`timeout ${Math.floor(timeouts.qwenCli / 1000)} ${qwenPath} --prompt ${'Respond with exactly: hi'} --model ${mappedModel} --output-format json --yolo`;
181
+ const output = getCommandResultOutput(testResult);
182
+
183
+ if (testResult.code !== 0) {
184
+ if (isQwenAuthError(output)) {
185
+ await log('āŒ Qwen Code authentication failed', { level: 'error' });
186
+ await log(' šŸ’” Run: qwen auth', { level: 'error' });
187
+ return false;
188
+ }
189
+
190
+ await log(`āŒ Qwen Code validation failed with exit code ${testResult.code}`, { level: 'error' });
191
+ if (output.trim()) await log(` Error: ${output.trim()}`, { level: 'error' });
192
+ return false;
193
+ }
194
+
195
+ const parsed = parseQwenStreamJsonOutput(output);
196
+ if (parsed.errors.some(error => error.isAuthError)) {
197
+ await log('āŒ Qwen Code authentication failed', { level: 'error' });
198
+ await log(' šŸ’” Run: qwen auth', { level: 'error' });
199
+ return false;
200
+ }
201
+
202
+ await log('āœ… Qwen Code connection validated successfully');
203
+ return true;
204
+ } catch (error) {
205
+ await log(`āŒ Failed to validate Qwen Code connection: ${error.message}`, { level: 'error' });
206
+ await log(' šŸ’” Make sure Qwen Code is installed and accessible as qwen', { level: 'error' });
207
+ return false;
208
+ }
209
+ };
210
+
211
+ // Main function to execute Qwen Code with prompts and settings
212
+ export const executeQwen = async params => {
213
+ const { issueUrl, issueNumber, prNumber, prUrl, branchName, tempDir, workspaceTmpDir, isContinueMode, mergeStateStatus, forkedRepo, feedbackLines, forkActionsUrl, owner, repo, argv, log, setLogFile, getLogFile, formatAligned, getResourceSnapshot, qwenPath = 'qwen', $ } = params;
214
+
215
+ const { buildUserPrompt, buildSystemPrompt } = await import('./qwen.prompts.lib.mjs');
216
+
217
+ const prompt = buildUserPrompt({
218
+ issueUrl,
219
+ issueNumber,
220
+ prNumber,
221
+ prUrl,
222
+ branchName,
223
+ tempDir,
224
+ workspaceTmpDir,
225
+ isContinueMode,
226
+ mergeStateStatus,
227
+ forkedRepo,
228
+ feedbackLines,
229
+ forkActionsUrl,
230
+ owner,
231
+ repo,
232
+ argv,
233
+ });
234
+
235
+ const systemPrompt = buildSystemPrompt({
236
+ owner,
237
+ repo,
238
+ issueNumber,
239
+ prNumber,
240
+ branchName,
241
+ tempDir,
242
+ workspaceTmpDir,
243
+ isContinueMode,
244
+ forkedRepo,
245
+ argv,
246
+ });
247
+
248
+ if (argv.verbose) {
249
+ await log('\nšŸ“ Final prompt structure:', { verbose: true });
250
+ await log(` Characters: ${prompt.length}`, { verbose: true });
251
+ await log(` System prompt characters: ${systemPrompt.length}`, { verbose: true });
252
+ if (feedbackLines && feedbackLines.length > 0) {
253
+ await log(' Feedback info: Included', { verbose: true });
254
+ }
255
+
256
+ if (argv.dryRun) {
257
+ await log('\nšŸ“‹ User prompt content:', { verbose: true });
258
+ await log('---BEGIN USER PROMPT---', { verbose: true });
259
+ await log(prompt, { verbose: true });
260
+ await log('---END USER PROMPT---', { verbose: true });
261
+ await log('\nšŸ“‹ System prompt content:', { verbose: true });
262
+ await log('---BEGIN SYSTEM PROMPT---', { verbose: true });
263
+ await log(systemPrompt, { verbose: true });
264
+ await log('---END SYSTEM PROMPT---', { verbose: true });
265
+ }
266
+ }
267
+
268
+ return await executeQwenCommand({
269
+ tempDir,
270
+ branchName,
271
+ prompt,
272
+ systemPrompt,
273
+ argv,
274
+ log,
275
+ setLogFile,
276
+ getLogFile,
277
+ formatAligned,
278
+ getResourceSnapshot,
279
+ forkedRepo,
280
+ feedbackLines,
281
+ qwenPath,
282
+ $,
283
+ });
284
+ };
285
+
286
+ export const executeQwenCommand = async params => {
287
+ const { tempDir, branchName, prompt, systemPrompt, argv, log, formatAligned = (_icon, label, value = '') => `${label} ${value}`.trim(), getResourceSnapshot = async () => ({ memory: '\nunknown', load: 'unknown' }), forkedRepo, feedbackLines, qwenPath = 'qwen', $: dollar = $, waitForRetryDelay = waitWithCountdown } = params;
288
+
289
+ let retryCount = 0;
290
+ const promptFile = path.join(os.tmpdir(), `qwen_prompt_${Date.now()}_${process.pid}.txt`);
291
+ const systemPromptFile = path.join(os.tmpdir(), `qwen_system_prompt_${Date.now()}_${process.pid}.txt`);
292
+
293
+ await fs.writeFile(promptFile, prompt);
294
+ await fs.writeFile(systemPromptFile, systemPrompt || '');
295
+
296
+ const executeWithRetry = async () => {
297
+ if (retryCount === 0) {
298
+ await log(`\n${formatAligned('šŸ¤–', 'Executing Qwen Code:', argv.model.toUpperCase())}`);
299
+ } else {
300
+ await log(`\n${formatAligned('šŸ”„', 'Retry attempt:', `${retryCount}/${retryLimits.maxTransientErrorRetries}`)}`);
301
+ }
302
+
303
+ if (argv.verbose) {
304
+ await log(` Model: ${argv.model}`, { verbose: true });
305
+ await log(` Working directory: ${tempDir}`, { verbose: true });
306
+ await log(` Branch: ${branchName}`, { verbose: true });
307
+ await log(` Prompt length: ${prompt.length} chars`, { verbose: true });
308
+ await log(` System prompt length: ${systemPrompt?.length || 0} chars`, { verbose: true });
309
+ await log(` Feedback info included: ${feedbackLines && feedbackLines.length > 0 ? `Yes (${feedbackLines.length} lines)` : 'No'}`, { verbose: true });
310
+ }
311
+
312
+ const resourcesBefore = await getResourceSnapshot();
313
+ await log('šŸ“ˆ System resources before execution:', { verbose: true });
314
+ await log(` Memory: ${resourcesBefore.memory.split('\n')[1] || resourcesBefore.memory}`, { verbose: true });
315
+ await log(` Load: ${resourcesBefore.load}`, { verbose: true });
316
+
317
+ const mappedModel = mapModelToId(argv.model || defaultModels.qwen);
318
+ const resumeSession = argv.resume || null;
319
+ const resumeArgs = resumeSession ? ` --resume ${shellQuote(resumeSession)}` : '';
320
+ const appendSystemPromptArg = systemPrompt ? ` --append-system-prompt "$(cat ${shellQuote(systemPromptFile)})"` : '';
321
+ const commandScript = `cd ${shellQuote(tempDir)} && ${shellQuote(qwenPath)} --model ${shellQuote(mappedModel)} --output-format stream-json --yolo${resumeArgs}${appendSystemPromptArg} --prompt "$(cat ${shellQuote(promptFile)})"`;
322
+ const fullCommand = `(cd "${tempDir}" && ${qwenPath} --model "${mappedModel}" --output-format stream-json --yolo${resumeSession ? ` --resume "${resumeSession}"` : ''}${systemPrompt ? ` --append-system-prompt "$(cat "${systemPromptFile}")"` : ''} --prompt "$(cat "${promptFile}")")`;
323
+
324
+ await log(`\n${formatAligned('šŸ“', 'Raw command:', '')}`);
325
+ await log(fullCommand);
326
+ await log('');
327
+
328
+ try {
329
+ const execCommand = dollar({
330
+ cwd: tempDir,
331
+ mirror: false,
332
+ })`sh -lc ${commandScript}`;
333
+
334
+ await log(`${formatAligned('šŸ“‹', 'Command details:', '')}`);
335
+ await log(formatAligned('šŸ“‚', 'Working directory:', tempDir, 2));
336
+ await log(formatAligned('🌿', 'Branch:', branchName, 2));
337
+ await log(formatAligned('šŸ¤–', 'Model:', `Qwen Code ${argv.model.toUpperCase()}`, 2));
338
+ if (argv.fork && forkedRepo) {
339
+ await log(formatAligned('šŸ“', 'Fork:', forkedRepo, 2));
340
+ }
341
+
342
+ await log(`\n${formatAligned('ā–¶ļø', 'Streaming output:', '')}\n`);
343
+
344
+ let exitCode = 0;
345
+ let qwenState = createQwenParserState();
346
+ let allOutput = '';
347
+
348
+ for await (const chunk of execCommand.stream()) {
349
+ if (chunk.type === 'stdout') {
350
+ const output = chunk.data.toString();
351
+ await log(output);
352
+ allOutput += output;
353
+ qwenState = parseQwenStreamJsonOutput(output, qwenState);
354
+ }
355
+
356
+ if (chunk.type === 'stderr') {
357
+ const errorOutput = chunk.data.toString();
358
+ if (errorOutput) {
359
+ await log(errorOutput, { stream: 'stderr' });
360
+ allOutput += errorOutput;
361
+ qwenState = parseQwenStreamJsonOutput(errorOutput, qwenState);
362
+ }
363
+ } else if (chunk.type === 'exit') {
364
+ exitCode = chunk.code;
365
+ }
366
+ }
367
+
368
+ if (qwenState.buffer.trim()) {
369
+ qwenState = parseQwenStreamJsonOutput(`${qwenState.buffer}\n`, { ...qwenState, buffer: '' });
370
+ }
371
+
372
+ const sessionId = qwenState.sessionId || null;
373
+ const resultSummary = qwenState.lastTextContent || null;
374
+ const errorMessage = qwenState.errors
375
+ .map(error => error.message)
376
+ .filter(Boolean)
377
+ .join('\n');
378
+ const combinedErrorText = `${allOutput}\n${errorMessage}`.trim();
379
+ const limitInfo = detectUsageLimit(combinedErrorText);
380
+
381
+ if (limitInfo.isUsageLimit) {
382
+ const messageLines = formatUsageLimitMessage({
383
+ tool: 'Qwen Code',
384
+ resetTime: limitInfo.resetTime,
385
+ sessionId,
386
+ resumeCommand: sessionId ? `${process.argv[0]} ${process.argv[1]} ${argv.url} --tool qwen --resume ${sessionId}` : null,
387
+ });
388
+ for (const line of messageLines) {
389
+ await log(line, { level: 'warning' });
390
+ }
391
+
392
+ return {
393
+ success: false,
394
+ sessionId,
395
+ limitReached: true,
396
+ limitResetTime: limitInfo.resetTime,
397
+ pricingInfo: null,
398
+ publicPricingEstimate: null,
399
+ tokenUsage: null,
400
+ resultSummary,
401
+ };
402
+ }
403
+
404
+ if (exitCode !== 0 || qwenState.errors.length > 0) {
405
+ if (isQwenAuthError(combinedErrorText) || qwenState.errors.some(error => error.isAuthError)) {
406
+ await log('\n\nāŒ Qwen Code authentication failed', { level: 'error' });
407
+ await log(' šŸ’” Run: qwen auth', { level: 'error' });
408
+ } else {
409
+ const retryableError = classifyRetryableError(combinedErrorText);
410
+ if (retryableError.isRetryable) {
411
+ const isRequestTimeoutRetry = retryableError.label === 'Request timeout';
412
+ const maxRetries = isRequestTimeoutRetry ? retryLimits.maxRequestTimeoutRetries : retryLimits.maxTransientErrorRetries;
413
+ if (retryCount < maxRetries) {
414
+ const delay = getRetryDelayMs({
415
+ retryCount,
416
+ initialDelayMs: isRequestTimeoutRetry ? retryLimits.initialRequestTimeoutDelayMs : retryLimits.initialTransientErrorDelayMs,
417
+ maxDelayMs: isRequestTimeoutRetry ? retryLimits.maxRequestTimeoutDelayMs : retryLimits.maxTransientErrorDelayMs,
418
+ });
419
+ const delayLabel = delay >= 60000 ? `${Math.round(delay / 60000)} min` : `${Math.round(delay / 1000)}s`;
420
+ await log(`\nāš ļø ${retryableError.label} detected. Retry ${retryCount + 1}/${maxRetries} in ${delayLabel}${sessionId ? ' (session preserved)' : ''}...`, { level: 'warning' });
421
+ if (sessionId && !argv.resume) argv.resume = sessionId;
422
+ await maybeSwitchToFallbackModel({ tool: 'qwen', argv, log, errorMessage: retryableError.message });
423
+ await waitForRetryDelay(delay, log);
424
+ await log('\nšŸ”„ Retrying now...');
425
+ retryCount++;
426
+ return await executeWithRetry();
427
+ }
428
+ await log(`\n\nāŒ ${retryableError.label} persisted after ${maxRetries} retries`, { level: 'error' });
429
+ } else if (exitCode === 130) {
430
+ await log('\n\nāš ļø Qwen Code command interrupted (CTRL+C)');
431
+ } else {
432
+ await log(`\n\nāŒ Qwen Code command failed${exitCode !== 0 ? ` with exit code ${exitCode}` : ''}`, { level: 'error' });
433
+ if (errorMessage) await log(errorMessage, { level: 'error' });
434
+ }
435
+ }
436
+
437
+ const resourcesAfter = await getResourceSnapshot();
438
+ await log('\nšŸ“ˆ System resources after execution:', { verbose: true });
439
+ await log(` Memory: ${resourcesAfter.memory.split('\n')[1] || resourcesAfter.memory}`, { verbose: true });
440
+ await log(` Load: ${resourcesAfter.load}`, { verbose: true });
441
+
442
+ return {
443
+ success: false,
444
+ sessionId,
445
+ limitReached: false,
446
+ limitResetTime: null,
447
+ pricingInfo: null,
448
+ publicPricingEstimate: null,
449
+ tokenUsage: null,
450
+ resultSummary,
451
+ };
452
+ }
453
+
454
+ await log('\n\nāœ… Qwen Code command completed');
455
+ if (resultSummary) {
456
+ await log('šŸ“ Captured result summary from Qwen Code output', { verbose: true });
457
+ }
458
+
459
+ return {
460
+ success: true,
461
+ sessionId,
462
+ limitReached: false,
463
+ limitResetTime: null,
464
+ pricingInfo: null,
465
+ publicPricingEstimate: null,
466
+ tokenUsage: null,
467
+ resultSummary,
468
+ };
469
+ } catch (error) {
470
+ reportError(error, {
471
+ context: 'execute_qwen',
472
+ qwenPath,
473
+ operation: 'run_qwen_command',
474
+ });
475
+
476
+ await log(`\n\nāŒ Error executing Qwen Code command: ${error.message}`, { level: 'error' });
477
+ return {
478
+ success: false,
479
+ sessionId: null,
480
+ limitReached: false,
481
+ limitResetTime: null,
482
+ pricingInfo: null,
483
+ publicPricingEstimate: null,
484
+ tokenUsage: null,
485
+ resultSummary: null,
486
+ };
487
+ }
488
+ };
489
+
490
+ try {
491
+ return await executeWithRetry();
492
+ } finally {
493
+ await fs.unlink(promptFile).catch(() => {});
494
+ await fs.unlink(systemPromptFile).catch(() => {});
495
+ }
496
+ };
497
+
498
+ export const checkForUncommittedChanges = async (tempDir, owner, repo, branchName, $, log, autoCommit = false, autoRestartEnabled = true) => {
499
+ await log('\nšŸ” Checking for uncommitted changes...');
500
+ try {
501
+ const gitStatusResult = await $({ cwd: tempDir })`git status --porcelain 2>&1`;
502
+
503
+ if (gitStatusResult.code === 0) {
504
+ const statusOutput = gitStatusResult.stdout.toString().trim();
505
+
506
+ if (statusOutput) {
507
+ await log('šŸ“ Found uncommitted changes');
508
+ await log('Changes:');
509
+ for (const line of statusOutput.split('\n')) {
510
+ await log(` ${line}`);
511
+ }
512
+
513
+ if (autoCommit) {
514
+ await log('šŸ’¾ Auto-committing changes (--auto-commit-uncommitted-changes is enabled)...');
515
+
516
+ const addResult = await $({ cwd: tempDir })`git add -A`;
517
+ if (addResult.code === 0) {
518
+ const commitMessage = 'Auto-commit: Changes made by Qwen Code during problem-solving session';
519
+ const commitResult = await $({ cwd: tempDir })`git commit -m ${commitMessage}`;
520
+
521
+ if (commitResult.code === 0) {
522
+ await log('āœ… Changes committed successfully');
523
+
524
+ const pushResult = await $({ cwd: tempDir })`git push origin ${branchName} 2>&1`;
525
+
526
+ if (pushResult.code === 0) {
527
+ await log('āœ… Changes pushed successfully');
528
+ } else {
529
+ await log(`āš ļø Warning: Could not push changes: ${pushResult.stderr?.toString().trim() || pushResult.stdout?.toString().trim()}`, {
530
+ level: 'warning',
531
+ });
532
+ }
533
+ } else {
534
+ await log(`āš ļø Warning: Could not commit changes: ${commitResult.stderr?.toString().trim()}`, {
535
+ level: 'warning',
536
+ });
537
+ }
538
+ } else {
539
+ await log(`āš ļø Warning: Could not stage changes: ${addResult.stderr?.toString().trim()}`, {
540
+ level: 'warning',
541
+ });
542
+ }
543
+ return false;
544
+ } else if (autoRestartEnabled) {
545
+ await log('');
546
+ await log('āš ļø IMPORTANT: Uncommitted changes detected!');
547
+ await log(' Qwen Code made changes that were not committed.');
548
+ await log('');
549
+ await log('šŸ”„ AUTO-RESTART: Restarting Qwen Code to handle uncommitted changes...');
550
+ await log(' Qwen Code will review the changes and decide what to commit.');
551
+ await log('');
552
+ return true;
553
+ } else {
554
+ await log('');
555
+ await log('āš ļø Uncommitted changes detected but auto-restart is disabled.');
556
+ await log(' Use --auto-restart-on-uncommitted-changes to enable or commit manually.');
557
+ await log('');
558
+ return false;
559
+ }
560
+ } else {
561
+ await log('āœ… No uncommitted changes found');
562
+ return false;
563
+ }
564
+ } else {
565
+ await log(`āš ļø Warning: Could not check git status: ${gitStatusResult.stderr?.toString().trim()}`, {
566
+ level: 'warning',
567
+ });
568
+ return false;
569
+ }
570
+ } catch (gitError) {
571
+ reportError(gitError, {
572
+ context: 'check_uncommitted_changes_qwen',
573
+ tempDir,
574
+ operation: 'git_status_check',
575
+ });
576
+ await log(`āš ļø Warning: Error checking for uncommitted changes: ${gitError.message}`, { level: 'warning' });
577
+ return false;
578
+ }
579
+ };
580
+
581
+ export default {
582
+ validateQwenConnection,
583
+ checkPlaywrightMcpAvailability,
584
+ executeQwen,
585
+ executeQwenCommand,
586
+ checkForUncommittedChanges,
587
+ parseQwenStreamJsonOutput,
588
+ mapModelToId,
589
+ };