@link-assistant/hive-mind 2.11.4 → 2.11.6

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.
@@ -18,7 +18,7 @@ import { buildSolveResumeCommand } from './solve.resume-command.lib.mjs'; // Iss
18
18
  const __geminiBuildSolveResumeCmd = (argv, sessionId, tempDir) => (sessionId && argv?.url ? buildSolveResumeCommand({ issueUrl: argv.url, sessionId, tool: 'gemini', model: argv.model, fallbackModel: argv.fallbackModel, tempDir }) : null);
19
19
  import { sanitizeObjectStrings } from './unicode-sanitization.lib.mjs';
20
20
  import { defaultModels, geminiModels, isFormalAiModel } from './models/index.mjs';
21
- import { logPreparedToolCommand, resolveFormalAiToolInvocation } from './formal-ai.lib.mjs';
21
+ import { isPrepareOnly, logPreparedToolCommand, resolveFormalAiToolExecution } from './formal-ai.lib.mjs';
22
22
  import { buildFormalAiPricingInfo } from './formal-ai-pricing.lib.mjs'; // Issue #2119
23
23
  import { checkPlaywrightMcpPackageAvailability } from './playwright-mcp.lib.mjs';
24
24
  import { classifyRetryableError, prepareRetryAfterError, waitWithCountdown } from './tool-retry.lib.mjs';
@@ -412,11 +412,9 @@ export const executeGeminiCommand = async params => {
412
412
  await log(` Load: ${resourcesBefore.load}`, { verbose: true });
413
413
 
414
414
  const mappedModel = mapModelToId(argv.model || defaultModels.gemini);
415
- const toolInvocation = resolveFormalAiToolInvocation({
416
- tool: 'gemini',
417
- model: argv.model || defaultModels.gemini,
418
- toolPath: geminiPath,
419
- });
415
+ // Issue #2130: Formal AI runs the native CLI against a local Formal AI server (no argv wrapper).
416
+ const toolInvocation = await resolveFormalAiToolExecution({ tool: 'gemini', model: argv.model || defaultModels.gemini, toolPath: geminiPath, workdir: tempDir, log, verbose: argv.verbose, prepareOnly: isPrepareOnly(argv) });
417
+ const geminiEnv = { ...process.env, ...toolInvocation.env };
420
418
  const combinedPrompt = systemPrompt ? `${systemPrompt}\n\n${prompt}` : prompt;
421
419
 
422
420
  if (argv.resume) {
@@ -426,7 +424,6 @@ export const executeGeminiCommand = async params => {
426
424
  // Issue #1809: build args via shared helper so verbose/sandbox/include-dirs
427
425
  // toggles stay consistent between the logged command and the real invocation.
428
426
  const geminiArgList = buildGeminiArgs(argv, mappedModel, { tempDir, workspaceTmpDir });
429
- const fullGeminiArgList = [...toolInvocation.args, ...geminiArgList];
430
427
  const fullCommand = `(cd ${shellQuote(tempDir)} && ${toolInvocation.displayCommand} ${geminiArgList.map(shellQuote).join(' ')} <<< <prompt>)`;
431
428
 
432
429
  const preparedResult = await logPreparedToolCommand({ argv, fullCommand, log, formatAligned });
@@ -449,7 +446,8 @@ export const executeGeminiCommand = async params => {
449
446
  cwd: tempDir,
450
447
  stdin: combinedPrompt,
451
448
  mirror: false,
452
- })`${toolInvocation.command} ${fullGeminiArgList}`;
449
+ env: geminiEnv,
450
+ })`${toolInvocation.command} ${geminiArgList}`;
453
451
 
454
452
  await log(`${formatAligned('📋', 'Command details:', '')}`);
455
453
  await log(formatAligned('📂', 'Working directory:', tempDir, 2));
package/src/git.lib.mjs CHANGED
@@ -1,6 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { exec } from 'child_process';
4
+
5
+ import { quietProbe } from './quiet-probe.lib.mjs'; // issue #2130: keep read-only probe payloads out of the attached log
4
6
  import { promisify } from 'util';
5
7
 
6
8
  const execAsync = promisify(exec);
@@ -86,7 +88,14 @@ export const getGitVersion = async (execFunc = execAsync, currentVersion) => {
86
88
  };
87
89
 
88
90
  // Helper function for async git operations with zx
89
- export const getGitVersionAsync = async ($, currentVersion) => {
91
+ //
92
+ // Issue #2130: every probe below is quiet. They answer questions the caller
93
+ // asks about its own checkout ("is this a git repo?", "what is HEAD?"), and
94
+ // their bare answers - a `.git` path, a tag, a short SHA - were mirrored into
95
+ // the log attached to the pull request with nothing to explain them. The
96
+ // version this function computes is logged in words by the caller.
97
+ export const getGitVersionAsync = async (dollar, currentVersion) => {
98
+ const $ = quietProbe(dollar);
90
99
  // First check if we're in a git repository to avoid "fatal: not a git repository" errors
91
100
  // Redirect stderr to /dev/null at shell level to prevent error messages from appearing
92
101
  try {
@@ -17,6 +17,7 @@ if (typeof globalThis.use === 'undefined') {
17
17
  const fs = (await use('fs')).promises;
18
18
  const { $: __rawDollar$ } = await use('command-stream');
19
19
  const { wrapDollarWithGhRetry } = await import('./github-rate-limit.lib.mjs');
20
+ const { QUIET_PROBE } = await import('./quiet-probe.lib.mjs'); // issue #2130: keep read-only probe payloads out of the attached log
20
21
  const $ = wrapDollarWithGhRetry(__rawDollar$);
21
22
  const GITHUB_ISSUE_BODY_MAX_SIZE = 60000;
22
23
  const GITHUB_FILE_MAX_SIZE = 10 * 1024 * 1024;
@@ -53,7 +54,7 @@ export const promptUserForIssueCreation = async errorMessage => {
53
54
  */
54
55
  const getCurrentGitHubUser = async () => {
55
56
  try {
56
- const result = await $`gh api user --jq .login`;
57
+ const result = await $(QUIET_PROBE)`gh api user --jq .login`;
57
58
  if (result.exitCode === 0) {
58
59
  const user = result.stdout.toString().trim();
59
60
  if (user) return user;
@@ -449,6 +449,13 @@ export const execGhWithRetry = async (command, options = {}) => {
449
449
  * const { $: rawDollar } = await use('command-stream');
450
450
  * const $ = wrapDollarWithGhRetry(rawDollar);
451
451
  *
452
+ * The wrapper also forwards command-stream's options-call form, so the
453
+ * "do not mirror this probe" idiom (see src/quiet-probe.lib.mjs) keeps working
454
+ * on a wrapped `$` - issue #2130, where read-only probes dumped raw API
455
+ * payloads into the log that gets attached to the pull request:
456
+ *
457
+ * await $(QUIET_PROBE)`gh api repos/${owner}/${repo}`;
458
+ *
452
459
  * @template T
453
460
  * @param {(strings: TemplateStringsArray, ...values: unknown[]) => Promise<T>} dollar
454
461
  * @param {object} [options] - forwarded to ghWithRateLimitRetry per call.
@@ -456,6 +463,12 @@ export const execGhWithRetry = async (command, options = {}) => {
456
463
  */
457
464
  export const wrapDollarWithGhRetry = (dollar, options = {}) => {
458
465
  const wrapped = (strings, ...values) => {
466
+ // Options-call form: `$({ mirror: false })` returns a new tag bound to
467
+ // those options. Template literals always arrive as an array of quasis, so
468
+ // a plain object here is unambiguous.
469
+ if (strings && !Array.isArray(strings) && typeof strings === 'object') {
470
+ return wrapDollarWithGhRetry(dollar(strings), options);
471
+ }
459
472
  // Reconstruct the literal command for inspection (sufficient — leading
460
473
  // `gh ` is what we care about).
461
474
  let preview = '';
@@ -21,7 +21,13 @@ const getDefaultCommandRunner = async () => {
21
21
  const use = globalThis.use;
22
22
  const { $: rawDollar } = await use('command-stream');
23
23
  const { wrapDollarWithGhRetry } = await import('./github-rate-limit.lib.mjs');
24
- defaultCommandRunner = wrapDollarWithGhRetry(rawDollar);
24
+ const { QUIET_PROBE } = await import('./quiet-probe.lib.mjs');
25
+ // Issue #2130: these are existence probes. Their raw payloads (a ~33 KB pull
26
+ // request object, once per watch iteration) and their expected "gh: Not Found
27
+ // (HTTP 404)" answers were mirrored into the attached log, where a handled
28
+ // probe reads as an unexplained error. Every outcome is reported by the
29
+ // caller in words, so the raw output is captured but never mirrored.
30
+ defaultCommandRunner = wrapDollarWithGhRetry(rawDollar(QUIET_PROBE));
25
31
  return defaultCommandRunner;
26
32
  };
27
33
 
@@ -19,6 +19,7 @@ import { buildCostInfoString } from './github-cost-info.lib.mjs';
19
19
  export { buildCostInfoString };
20
20
  // #1756: route gh exec calls through transient + rate-limit retry wrapper
21
21
  import { execGhWithRetry } from './github-rate-limit.lib.mjs';
22
+ import { QUIET_PROBE } from './quiet-probe.lib.mjs'; // issue #2130: keep read-only probe payloads out of the attached log
22
23
  // Issue #1625: Named marker constants (single source of truth) + in-memory
23
24
  // tracking for tool-posted comments. See tool-comments.lib.mjs for design.
24
25
  import { SOLUTION_DRAFT_LOG_MARKER, SOLUTION_DRAFT_FAILED_MARKER, SOLUTION_DRAFT_FINISHED_WITH_ERRORS_MARKER, USAGE_LIMIT_REACHED_MARKER, NOW_WORKING_SESSION_IS_ENDED_MARKER, postTrackedComment, postTrackedCommentFromFile } from './tool-comments.lib.mjs';
@@ -43,8 +44,11 @@ export const checkFileInBranch = async (owner, repo, fileName, branchName) => {
43
44
  const { $ } = await use('command-stream');
44
45
 
45
46
  try {
46
- // Use GitHub CLI to check if file exists in the branch
47
- const result = await $`gh api repos/${owner}/${repo}/contents/${fileName}?ref=${branchName}`;
47
+ // Issue #2130: this is an existence probe, and "absent" is the answer the
48
+ // caller is usually looking for. Mirroring the command would print the whole
49
+ // contents payload on a hit and `gh: Not Found (HTTP 404)` on a miss, which
50
+ // reads as a failure in the log even though nothing went wrong.
51
+ const result = await $(QUIET_PROBE)`gh api repos/${owner}/${repo}/contents/${fileName}?ref=${branchName}`;
48
52
  return result.code === 0;
49
53
  } catch (error) {
50
54
  // File doesn't exist or access error - this is expected behavior
@@ -66,8 +70,10 @@ export const checkGitHubPermissions = async () => {
66
70
  const { $ } = await use('command-stream');
67
71
  try {
68
72
  await log('\n🔐 Checking GitHub authentication and permissions...');
69
- // Get auth status including token scopes
70
- const authStatusResult = await $`gh auth status 2>&1`;
73
+ // Get auth status including token scopes.
74
+ // Issue #2130: capture without mirroring - the parsed summary below is what
75
+ // belongs in the log, not gh's raw account/token/scope block.
76
+ const authStatusResult = await $(QUIET_PROBE)`gh auth status 2>&1`;
71
77
  const authOutput = authStatusResult.stdout.toString() + authStatusResult.stderr.toString();
72
78
  if (authStatusResult.code !== 0 || authOutput.includes('not logged into any GitHub hosts')) {
73
79
  await log('❌ GitHub authentication error: Not logged in', { level: 'error' });
@@ -183,7 +189,7 @@ export const checkRepositoryWritePermission = async (owner, repo, options = {})
183
189
  await log('');
184
190
  // Get current user to suggest their fork
185
191
  try {
186
- const userResult = await $`gh api user --jq .login`;
192
+ const userResult = await $(QUIET_PROBE)`gh api user --jq .login`;
187
193
  if (userResult.code === 0) {
188
194
  const currentUser = userResult.stdout.toString().trim();
189
195
  await log(' Run this command:', { level: 'error' });
@@ -614,7 +620,7 @@ ${logContent}
614
620
  // Issue #1173: Use public upload for public repos, private for private repos
615
621
  let isPublicRepo = true;
616
622
  try {
617
- const repoVisibilityResult = await $`gh api repos/${owner}/${repo} --jq .visibility`;
623
+ const repoVisibilityResult = await $(QUIET_PROBE)`gh api repos/${owner}/${repo} --jq .visibility`;
618
624
  if (repoVisibilityResult.code === 0) {
619
625
  const visibility = repoVisibilityResult.stdout.toString().trim();
620
626
  isPublicRepo = visibility === 'public';
@@ -972,7 +978,9 @@ export async function fetchProjectIssues(projectNumber, owner, statusFilter) {
972
978
  await log(`🔍 Fetching issues from GitHub Project #${projectNumber} (owner: ${owner}, status: ${statusFilter})`);
973
979
  // Check for project scope in GitHub CLI authentication
974
980
  try {
975
- const authStatus = await $`gh auth status --show-token`;
981
+ // Issue #2130: --show-token prints the token in clear text; mirroring it
982
+ // would put a live credential on stdout and in the log file.
983
+ const authStatus = await $(QUIET_PROBE)`gh auth status --show-token`;
976
984
  if (!authStatus.stdout.includes('project')) {
977
985
  throw new Error('Missing project scope. Run: gh auth refresh -s project');
978
986
  }
@@ -1431,7 +1439,7 @@ export async function handlePRNotFoundError({ prNumber, owner, repo, argv, shoul
1431
1439
  export async function detectRepositoryVisibility(owner, repo) {
1432
1440
  try {
1433
1441
  // Issue #1536: retry on transient network errors
1434
- const visibilityResult = await ghCmdRetry(() => $`gh api repos/${owner}/${repo} --jq .visibility`, { label: `visibility ${owner}/${repo}` });
1442
+ const visibilityResult = await ghCmdRetry(() => $(QUIET_PROBE)`gh api repos/${owner}/${repo} --jq .visibility`, { label: `visibility ${owner}/${repo}` });
1435
1443
  if (visibilityResult.code === 0) {
1436
1444
  const visibility = visibilityResult.stdout.toString().trim();
1437
1445
  const isPublic = visibility === 'public';
@@ -20,7 +20,7 @@ import { timeouts, retryLimits } from './config.lib.mjs';
20
20
  import { detectUsageLimit, formatUsageLimitMessage } from './usage-limit.lib.mjs';
21
21
  import { sanitizeObjectStrings } from './unicode-sanitization.lib.mjs';
22
22
  import { opencodeModels, defaultModels } from './models/index.mjs';
23
- import { logPreparedToolCommand, resolveFormalAiToolInvocation } from './formal-ai.lib.mjs';
23
+ import { isPrepareOnly, logPreparedToolCommand, resolveFormalAiToolExecution } from './formal-ai.lib.mjs';
24
24
  import { checkPlaywrightMcpPackageAvailability, getOpenCodePlaywrightMcpDisableEnv } from './playwright-mcp.lib.mjs';
25
25
  import { createAgentTokenUsage, accumulateAgentStepFinishUsage, parseAgentTokenUsage as parseOpenCodeTokenUsage } from './agent-token-usage.lib.mjs';
26
26
  import { createJsonStreamScanner } from './json-stream.lib.mjs';
@@ -234,11 +234,9 @@ export const executeOpenCodeCommand = async params => {
234
234
 
235
235
  // Map model alias to full ID
236
236
  const mappedModel = mapModelToId(argv.model);
237
- const toolInvocation = resolveFormalAiToolInvocation({
238
- tool: 'opencode',
239
- model: argv.model,
240
- toolPath: opencodePath,
241
- });
237
+ // Issue #2130: Formal AI runs the native CLI against a local Formal AI server (no argv wrapper).
238
+ const toolInvocation = await resolveFormalAiToolExecution({ tool: 'opencode', model: argv.model, toolPath: opencodePath, workdir: tempDir, log, verbose: argv.verbose, prepareOnly: isPrepareOnly(argv), env: opencodeEnv });
239
+ Object.assign(opencodeEnv, toolInvocation.env);
242
240
  const streamingTokenUsage = createAgentTokenUsage();
243
241
 
244
242
  // Build opencode command arguments
@@ -310,14 +308,14 @@ export const executeOpenCodeCommand = async params => {
310
308
  mirror: false,
311
309
  env: opencodeEnv,
312
310
  });
313
- execCommand = toolInvocation.formalAi ? commandRunner`cat ${promptFile} | ${toolInvocation.command} ${toolInvocation.args} run --format json --session ${argv.resume} --model ${mappedModel}` : commandRunner`cat ${promptFile} | ${toolInvocation.command} run --format json --session ${argv.resume} --model ${mappedModel}`;
311
+ execCommand = commandRunner`cat ${promptFile} | ${toolInvocation.command} run --format json --session ${argv.resume} --model ${mappedModel}`;
314
312
  } else {
315
313
  const commandRunner = $({
316
314
  cwd: tempDir,
317
315
  mirror: false,
318
316
  env: opencodeEnv,
319
317
  });
320
- execCommand = toolInvocation.formalAi ? commandRunner`cat ${promptFile} | ${toolInvocation.command} ${toolInvocation.args} run --format json --model ${mappedModel}` : commandRunner`cat ${promptFile} | ${toolInvocation.command} run --format json --model ${mappedModel}`;
318
+ execCommand = commandRunner`cat ${promptFile} | ${toolInvocation.command} run --format json --model ${mappedModel}`;
321
319
  }
322
320
 
323
321
  await log(`${formatAligned('📋', 'Command details:', '')}`);
@@ -355,8 +353,11 @@ export const executeOpenCodeCommand = async params => {
355
353
  accumulateAgentStepFinishUsage(streamingTokenUsage, data);
356
354
  // Track text content for result summary
357
355
  // OpenCode outputs text via 'text', 'assistant', 'message', or 'result' type events
358
- if (data.type === 'text' && data.text) {
359
- lastTextContent = data.text;
356
+ // Issue #2130: OpenCode-derived CLIs nest assistant text under `part`
357
+ // (`{"type":"text","part":{"type":"text","text":"…"}}`) with no
358
+ // top-level `data.text`, which left `resultSummary` null.
359
+ if (data.type === 'text' && (data.text || data.part?.text)) {
360
+ lastTextContent = data.text || data.part.text;
360
361
  } else if (data.type === 'assistant' && data.message?.content) {
361
362
  const content = Array.isArray(data.message.content) ? data.message.content : [data.message.content];
362
363
  for (const item of content) {
@@ -23,6 +23,7 @@
23
23
 
24
24
  import { sanitizeForPublication, getSanitizationStats } from './token-sanitization.lib.mjs';
25
25
  import { wrapDollarWithGhRetry as _wrapDollarWithGhRetry } from './github-rate-limit.lib.mjs'; // rate-limit marker (#1726): caller passes $ already wrapped through wrapDollarWithGhRetry
26
+ import { quietProbe } from './quiet-probe.lib.mjs'; // issue #2130: keep read-only probe payloads out of the attached log
26
27
 
27
28
  /**
28
29
  * Determine the bot's gh login name. The function returns null on any error
@@ -33,7 +34,7 @@ import { wrapDollarWithGhRetry as _wrapDollarWithGhRetry } from './github-rate-l
33
34
  */
34
35
  const detectBotLogin = async $ => {
35
36
  try {
36
- const result = await $`gh api user --jq .login`;
37
+ const result = await quietProbe($)`gh api user --jq .login`;
37
38
  if (result && result.code === 0 && result.stdout) {
38
39
  const login = result.stdout.toString().trim();
39
40
  return login || null;
@@ -61,7 +62,7 @@ export const sweepPrConversationComments = async ({ $, owner, repo, prNumber, bo
61
62
  const stats = { scanned: 0, edited: 0, errors: 0 };
62
63
  let response;
63
64
  try {
64
- response = await $`gh api repos/${owner}/${repo}/issues/${prNumber}/comments --paginate`;
65
+ response = await quietProbe($)`gh api repos/${owner}/${repo}/issues/${prNumber}/comments --paginate`;
65
66
  } catch (err) {
66
67
  await log(`⚠️ post-finish sweep: failed to list comments: ${err.message || err}`);
67
68
  stats.errors++;
@@ -120,7 +121,7 @@ export const sweepPrDescription = async ({ $, owner, repo, prNumber, log = async
120
121
  const stats = { scanned: 0, edited: 0, errors: 0 };
121
122
  let response;
122
123
  try {
123
- response = await $`gh api repos/${owner}/${repo}/pulls/${prNumber}`;
124
+ response = await quietProbe($)`gh api repos/${owner}/${repo}/pulls/${prNumber}`;
124
125
  } catch (err) {
125
126
  await log(`⚠️ post-finish sweep: failed to fetch PR ${prNumber}: ${err.message || err}`);
126
127
  stats.errors++;
@@ -0,0 +1,72 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Read-only probes that must not be mirrored into the attached log.
4
+ *
5
+ * Issue #2130 — the solver mirrors every child process's output to stdout, and
6
+ * `src/lib.mjs` copies stdout into the log file that is later attached to the
7
+ * pull request. That is the right default for the AI tool's own output, but it
8
+ * also dumped the raw answers of the solver's internal probes between its own
9
+ * sentences, where they read as unexplained output or as errors:
10
+ *
11
+ * - `gh api repos/OWNER/REPO/pulls/N` — a ~33 KB JSON object, once per watch
12
+ * iteration (12 copies in the log captured for this issue);
13
+ * - `gh api .../comments --paginate` — up to 46 KB per restart;
14
+ * - `gh auth status --show-token` — a live credential in clear text;
15
+ * - bare one-word answers such as a login, `public`, `I_kwDO...`, or a commit
16
+ * SHA, with nothing around them to say what asked the question.
17
+ *
18
+ * Every one of those call sites already reports what it learned in words, so
19
+ * the raw output is captured for the caller but never mirrored.
20
+ *
21
+ * @module quiet-probe
22
+ */
23
+
24
+ /**
25
+ * command-stream options for a probe whose raw output stays out of the log.
26
+ * `capture: true` keeps `result.stdout` available to the caller.
27
+ *
28
+ * const result = await $(QUIET_PROBE)`gh api user --jq .login`;
29
+ */
30
+ export const QUIET_PROBE = Object.freeze({ mirror: false, capture: true });
31
+
32
+ const quietProbeCache = new WeakMap();
33
+
34
+ /**
35
+ * Bind {@link QUIET_PROBE} to a `$` that arrived as a function argument,
36
+ * degrading to the tag itself when that `$` does not implement command-stream's
37
+ * options-call form.
38
+ *
39
+ * Many helpers take `$` as an injected parameter, and the doubles supplied by
40
+ * callers (tests, in particular) are plain tagged templates that throw on
41
+ * `$({ ... })`. Suppressing mirrored output is a readability improvement, never
42
+ * a correctness requirement, so a `$` that cannot be configured is used as-is
43
+ * rather than turned into a crash.
44
+ *
45
+ * const result = await quietProbe($)`gh api user --jq .login`;
46
+ *
47
+ * @param {Function} dollar - a command-stream `$` or a tagged-template double.
48
+ * @returns {Function} `dollar` bound to the quiet options, or `dollar` itself.
49
+ */
50
+ export const quietProbe = dollar => {
51
+ if (typeof dollar !== 'function') return dollar;
52
+ const cached = quietProbeCache.get(dollar);
53
+ if (cached) return cached;
54
+ let bound = dollar;
55
+ try {
56
+ const candidate = dollar(QUIET_PROBE);
57
+ // A tag that ignores the options call may return a promise for a command it
58
+ // never should have run; swallow it so it cannot surface as an unhandled
59
+ // rejection, and keep the original tag.
60
+ if (typeof candidate?.catch === 'function') candidate.catch(() => {});
61
+ if (typeof candidate === 'function') bound = candidate;
62
+ } catch {
63
+ /* not option-callable - use the tag unchanged */
64
+ }
65
+ quietProbeCache.set(dollar, bound);
66
+ return bound;
67
+ };
68
+
69
+ export default {
70
+ QUIET_PROBE,
71
+ quietProbe,
72
+ };
package/src/qwen.lib.mjs CHANGED
@@ -19,7 +19,7 @@ import { timeouts, retryLimits } from './config.lib.mjs';
19
19
  import { detectUsageLimit, formatUsageLimitMessage } from './usage-limit.lib.mjs';
20
20
  import { sanitizeObjectStrings } from './unicode-sanitization.lib.mjs';
21
21
  import { qwenModels, defaultModels, isFormalAiModel } from './models/index.mjs';
22
- import { logPreparedToolCommand, resolveFormalAiToolInvocation } from './formal-ai.lib.mjs';
22
+ import { buildFormalAiEnvExports, isPrepareOnly, logPreparedToolCommand, resolveFormalAiToolExecution } from './formal-ai.lib.mjs';
23
23
  import { buildFormalAiPricingInfo } from './formal-ai-pricing.lib.mjs'; // Issue #2119
24
24
  import { checkPlaywrightMcpPackageAvailability } from './playwright-mcp.lib.mjs';
25
25
  import { classifyRetryableError, prepareRetryAfterError, waitWithCountdown } from './tool-retry.lib.mjs';
@@ -503,15 +503,15 @@ export const executeQwenCommand = async params => {
503
503
  await log(` Load: ${resourcesBefore.load}`, { verbose: true });
504
504
 
505
505
  const mappedModel = mapModelToId(argv.model || defaultModels.qwen);
506
- const toolInvocation = resolveFormalAiToolInvocation({
507
- tool: 'qwen',
508
- model: argv.model || defaultModels.qwen,
509
- toolPath: qwenPath,
510
- });
506
+ // Issue #2130: Formal AI runs the native CLI against a local Formal AI server (no argv wrapper).
507
+ const toolInvocation = await resolveFormalAiToolExecution({ tool: 'qwen', model: argv.model || defaultModels.qwen, toolPath: qwenPath, workdir: tempDir, log, verbose: argv.verbose, prepareOnly: isPrepareOnly(argv) });
508
+ const qwenEnv = { ...process.env, ...toolInvocation.env };
511
509
  const resumeSession = argv.resume || null;
512
510
  const resumeArgs = resumeSession ? ` --resume ${shellQuote(resumeSession)}` : '';
513
511
  const appendSystemPromptArg = systemPrompt ? ` --append-system-prompt "$(cat ${shellQuote(systemPromptFile)})"` : '';
514
- const commandScript = `cd ${shellQuote(tempDir)} && ${toolInvocation.displayCommand} --model ${shellQuote(mappedModel)} --output-format stream-json --yolo${resumeArgs}${appendSystemPromptArg} --prompt "$(cat ${shellQuote(promptFile)})"`;
512
+ // Issue #2130: re-export the Formal AI environment inside the `sh -lc` script so a
513
+ // stale `formal-ai with --global` block in the operator profile cannot override it.
514
+ const commandScript = `${buildFormalAiEnvExports(toolInvocation.env)}cd ${shellQuote(tempDir)} && ${toolInvocation.displayCommand} --model ${shellQuote(mappedModel)} --output-format stream-json --yolo${resumeArgs}${appendSystemPromptArg} --prompt "$(cat ${shellQuote(promptFile)})"`;
515
515
  const fullCommand = `(cd "${tempDir}" && ${toolInvocation.displayCommand} --model "${mappedModel}" --output-format stream-json --yolo${resumeSession ? ` --resume "${resumeSession}"` : ''}${systemPrompt ? ` --append-system-prompt "$(cat "${systemPromptFile}")"` : ''} --prompt "$(cat "${promptFile}")")`;
516
516
 
517
517
  const preparedResult = await logPreparedToolCommand({ argv, fullCommand, log, formatAligned });
@@ -521,6 +521,7 @@ export const executeQwenCommand = async params => {
521
521
  const execCommand = dollar({
522
522
  cwd: tempDir,
523
523
  mirror: false,
524
+ env: qwenEnv,
524
525
  })`sh -lc ${commandScript}`;
525
526
 
526
527
  await log(`${formatAligned('📋', 'Command details:', '')}`);
@@ -16,6 +16,7 @@ const use = globalThis.use;
16
16
  // Use command-stream for consistent $ behavior across runtimes
17
17
  const { $: __rawDollar$ } = await use('command-stream');
18
18
  const { wrapDollarWithGhRetry } = await import('./github-rate-limit.lib.mjs');
19
+ const { QUIET_PROBE } = await import('./quiet-probe.lib.mjs'); // issue #2130: keep read-only probe payloads out of the attached log
19
20
  const $ = wrapDollarWithGhRetry(__rawDollar$);
20
21
  // Import shared library functions
21
22
  const lib = await import('./lib.mjs');
@@ -522,7 +523,7 @@ export const processAutoContinueForIssue = async (argv, isIssueUrl, urlNumber, o
522
523
  // When in fork mode, check for existing branches in the fork
523
524
  try {
524
525
  // Get current user to determine fork name
525
- const userResult = await $`gh api user --jq .login`;
526
+ const userResult = await $(QUIET_PROBE)`gh api user --jq .login`;
526
527
  if (userResult.code === 0) {
527
528
  const currentUser = userResult.stdout.toString().trim();
528
529
  // Determine fork name based on --prefix-fork-name-with-owner-name option
@@ -10,6 +10,7 @@ import { emitForkAwareDiagnostic } from './solve.auto-pr-fork-diagnostic.lib.mjs
10
10
  import { handleCompareApiNotReady } from './solve.auto-pr-compare-readiness.lib.mjs'; // Issue #1829: decides whether a failed compare-API readiness poll is fatal (fork mismatch / 0 commits) or a transient diff-render failure to degrade past.
11
11
 
12
12
  import { wrapDollarWithGhRetry as _wrapDollarWithGhRetry, execGhWithRetry, isTransientCompareApiError } from './github-rate-limit.lib.mjs'; // rate-limit marker (#1726): gh API calls flow through $ wrapped by caller. Issue #1756: execGhWithRetry retries on transient 5xx (504) too. Issue #1829: isTransientCompareApiError lets the compare-API readiness gate degrade gracefully on transient diff-render failures.
13
+ import { quietProbe } from './quiet-probe.lib.mjs'; // issue #2130: keep read-only probe payloads out of the attached log
13
14
  import { stagePlaceholderFileOrExplain, explainNothingStagedAndThrow } from './solve.auto-pr-placeholder.lib.mjs'; // Issue #1825: handles the seed placeholder when the target repo gitignores it.
14
15
  import { sanitizeForPublication, writeSanitizedPublicationFile } from './token-sanitization.lib.mjs';
15
16
 
@@ -432,7 +433,7 @@ Proceed.
432
433
  // Determine fork name based on --prefix-fork-name-with-owner-name option
433
434
  const forkRepoName = argv.prefixForkNameWithOwnerName ? `${owner}-${repo}` : repo;
434
435
  try {
435
- const userResult = await $`gh api user --jq .login`;
436
+ const userResult = await quietProbe($)`gh api user --jq .login`;
436
437
  if (userResult.code === 0) {
437
438
  currentUser = userResult.stdout.toString().trim();
438
439
  const userForkName = `${currentUser}/${forkRepoName}`;
@@ -1131,9 +1132,14 @@ ${prBody}`,
1131
1132
  // the queries are assembled in JS and passed as one argument.
1132
1133
  const repositorySelector = `repository(owner: ${JSON.stringify(owner)}, name: ${JSON.stringify(repo)})`;
1133
1134
 
1134
- // First, get the node IDs for both the issue and the PR
1135
+ // First, get the node IDs for both the issue and the PR.
1136
+ // Issue #2130: these three reads are quiet. Their bare answers
1137
+ // ("I_kwDO...", "PR_kwDO...", "1") were mirrored into the
1138
+ // attached log with nothing around them; the node IDs are
1139
+ // re-logged below under --verbose and the link result is
1140
+ // reported in words either way.
1135
1141
  const issueNodeQuery = `query { ${repositorySelector} { issue(number: ${issueNumber}) { id } } }`;
1136
- const issueNodeResult = await $`gh api graphql -f query=${issueNodeQuery} --jq .data.repository.issue.id`;
1142
+ const issueNodeResult = await quietProbe($)`gh api graphql -f query=${issueNodeQuery} --jq .data.repository.issue.id`;
1137
1143
 
1138
1144
  if (issueNodeResult.code !== 0) {
1139
1145
  throw new Error(`Failed to get issue node ID: ${issueNodeResult.stderr}`);
@@ -1143,7 +1149,7 @@ ${prBody}`,
1143
1149
  await log(` Issue node ID: ${issueNodeId}`, { verbose: true });
1144
1150
 
1145
1151
  const prNodeQuery = `query { ${repositorySelector} { pullRequest(number: ${localPrNumber}) { id } } }`;
1146
- const prNodeResult = await $`gh api graphql -f query=${prNodeQuery} --jq .data.repository.pullRequest.id`;
1152
+ const prNodeResult = await quietProbe($)`gh api graphql -f query=${prNodeQuery} --jq .data.repository.pullRequest.id`;
1147
1153
 
1148
1154
  if (prNodeResult.code !== 0) {
1149
1155
  throw new Error(`Failed to get PR node ID: ${prNodeResult.stderr}`);
@@ -1160,7 +1166,7 @@ ${prBody}`,
1160
1166
 
1161
1167
  // Let's verify the link was created
1162
1168
  const linkCheckQuery = `query { ${repositorySelector} { pullRequest(number: ${localPrNumber}) { closingIssuesReferences(first: 10) { nodes { number } } } } }`;
1163
- const linkCheckResult = await $`gh api graphql -f query=${linkCheckQuery} --jq '.data.repository.pullRequest.closingIssuesReferences.nodes[].number'`;
1169
+ const linkCheckResult = await quietProbe($)`gh api graphql -f query=${linkCheckQuery} --jq '.data.repository.pullRequest.closingIssuesReferences.nodes[].number'`;
1164
1170
 
1165
1171
  if (linkCheckResult.code === 0) {
1166
1172
  const linkedIssues = parseClosingIssueNumbers(linkCheckResult.stdout);
@@ -10,6 +10,7 @@
10
10
  import { reportError } from './sentry.lib.mjs';
11
11
 
12
12
  import { wrapDollarWithGhRetry as _wrapDollarWithGhRetry } from './github-rate-limit.lib.mjs'; // rate-limit marker (#1726): gh API calls flow through $ wrapped by caller
13
+ import { quietProbe } from './quiet-probe.lib.mjs'; // issue #2130: keep read-only probe payloads out of the attached log
13
14
  export async function handleBranchCheckoutError({ branchName, prNumber, errorOutput, issueUrl, owner, repo, tempDir, argv, formatAligned, log, $ }) {
14
15
  // Check if this is a PR from a fork
15
16
  let isForkPR = false;
@@ -67,7 +68,7 @@ export async function handleBranchCheckoutError({ branchName, prNumber, errorOut
67
68
 
68
69
  // Check if the current user has a fork of this repository
69
70
  try {
70
- const userResult = await $`gh api user --jq .login`;
71
+ const userResult = await quietProbe($)`gh api user --jq .login`;
71
72
  if (userResult.code === 0) {
72
73
  const currentUser = userResult.stdout.toString().trim();
73
74
  // Determine fork name based on --prefix-fork-name-with-owner-name option
@@ -15,6 +15,7 @@ const use = globalThis.use;
15
15
  // Use command-stream for consistent $ behavior across runtimes
16
16
  const { $: __rawDollar$ } = await use('command-stream');
17
17
  const { wrapDollarWithGhRetry } = await import('./github-rate-limit.lib.mjs');
18
+ const { QUIET_PROBE } = await import('./quiet-probe.lib.mjs'); // issue #2130: keep read-only probe payloads out of the attached log
18
19
  const $ = wrapDollarWithGhRetry(__rawDollar$);
19
20
  const os = (await use('os')).default;
20
21
  const path = (await use('path')).default;
@@ -86,7 +87,7 @@ export const setupRepository = async (argv, owner, repo) => {
86
87
  await log(`${formatAligned('', 'Checking fork status...', '')}\n`);
87
88
 
88
89
  // Get current user
89
- const userResult = await $`gh api user --jq .login`;
90
+ const userResult = await $(QUIET_PROBE)`gh api user --jq .login`;
90
91
  if (userResult.code !== 0) {
91
92
  await log(`${formatAligned('❌', 'Error:', 'Failed to get current user')}`);
92
93
  process.exit(1);
@@ -7,6 +7,7 @@
7
7
  import { reportError } from './sentry.lib.mjs';
8
8
 
9
9
  import { wrapDollarWithGhRetry as _wrapDollarWithGhRetry } from './github-rate-limit.lib.mjs'; // rate-limit marker (#1726): gh API calls flow through $ wrapped by caller
10
+ import { QUIET_PROBE, quietProbe } from './quiet-probe.lib.mjs'; // issue #2130: keep read-only probe payloads out of the attached log
10
11
  // Issue #1827: tool-generated comments (markers + in-memory tracked IDs) must
11
12
  // not count as feedback in watch/continue mode, mirroring checkForNonBotComments.
12
13
  import { isToolGeneratedComment, isToolTrackedCommentId } from './tool-comments.lib.mjs';
@@ -22,7 +23,7 @@ export const detectAndCountFeedback = async params => {
22
23
 
23
24
  // Get current GitHub user to filter out own comments
24
25
  try {
25
- const userResult = await $`gh api user --jq .login`;
26
+ const userResult = await quietProbe($)`gh api user --jq .login`;
26
27
  if (userResult.code === 0) {
27
28
  currentUser = userResult.stdout.toString().trim();
28
29
  await log(formatAligned('👤', 'Current user:', currentUser, 2));
@@ -63,7 +64,11 @@ export const detectAndCountFeedback = async params => {
63
64
 
64
65
  // Get the last commit timestamp from the PR branch
65
66
  let lastCommitTime = null;
66
- const git$ = repositoryPath ? $({ cwd: repositoryPath }) : $;
67
+ // Issue #2130: quiet. These probes answer "when was the last commit?", and
68
+ // the answer is logged in words below; mirroring them put bare ISO dates
69
+ // and `git log`'s "unknown revision" complaint - the expected outcome for
70
+ // a branch that has never been pushed - into the attached log.
71
+ const git$ = repositoryPath ? $({ cwd: repositoryPath, ...QUIET_PROBE }) : quietProbe($);
67
72
  let lastCommitResult = await git$`git log -1 --format="%aI" origin/${branchName}`;
68
73
  if (lastCommitResult.code !== 0) {
69
74
  // Fallback to local branch if remote doesn't exist
@@ -76,7 +81,7 @@ export const detectAndCountFeedback = async params => {
76
81
  } else {
77
82
  // Fallback: Get last commit time from GitHub API
78
83
  try {
79
- const prCommitsResult = await $`gh api repos/${owner}/${repo}/pulls/${prNumber}/commits --paginate --jq 'last.commit.author.date'`;
84
+ const prCommitsResult = await quietProbe($)`gh api repos/${owner}/${repo}/pulls/${prNumber}/commits --paginate --jq 'last.commit.author.date'`;
80
85
  if (prCommitsResult.code === 0 && prCommitsResult.stdout) {
81
86
  lastCommitTime = new Date(prCommitsResult.stdout.toString().trim());
82
87
  await log(formatAligned('📅', 'Last commit time (from API):', lastCommitTime.toISOString(), 2));
@@ -109,14 +114,14 @@ export const detectAndCountFeedback = async params => {
109
114
  let prConversationComments = [];
110
115
 
111
116
  // Get PR code review comments (use --paginate to get all comments, not just first page)
112
- const prReviewCommentsResult = await $`gh api repos/${owner}/${repo}/pulls/${prNumber}/comments --paginate`;
117
+ const prReviewCommentsResult = await quietProbe($)`gh api repos/${owner}/${repo}/pulls/${prNumber}/comments --paginate`;
113
118
  if (prReviewCommentsResult.code === 0) {
114
119
  prReviewComments = JSON.parse(prReviewCommentsResult.stdout.toString());
115
120
  }
116
121
 
117
122
  // Get PR conversation comments (PR is also an issue)
118
123
  // Use --paginate to get all comments - GitHub API returns max 30 per page by default
119
- const prConversationCommentsResult = await $`gh api repos/${owner}/${repo}/issues/${prNumber}/comments --paginate`;
124
+ const prConversationCommentsResult = await quietProbe($)`gh api repos/${owner}/${repo}/issues/${prNumber}/comments --paginate`;
120
125
  if (prConversationCommentsResult.code === 0) {
121
126
  prConversationComments = JSON.parse(prConversationCommentsResult.stdout.toString());
122
127
  }
@@ -156,7 +161,7 @@ export const detectAndCountFeedback = async params => {
156
161
 
157
162
  // Count new issue comments after last commit
158
163
  // Use --paginate to get all comments - GitHub API returns max 30 per page by default
159
- const issueCommentsResult = await $`gh api repos/${owner}/${repo}/issues/${issueNumber}/comments --paginate`;
164
+ const issueCommentsResult = await quietProbe($)`gh api repos/${owner}/${repo}/issues/${issueNumber}/comments --paginate`;
160
165
  if (issueCommentsResult.code === 0) {
161
166
  const issueComments = JSON.parse(issueCommentsResult.stdout.toString());
162
167
  const filteredIssueComments = issueComments.filter(comment => {
@@ -242,7 +247,7 @@ export const detectAndCountFeedback = async params => {
242
247
  // started) should be considered feedback.
243
248
  try {
244
249
  // Check PR description edit time
245
- const prDetailsResult = await $`gh api repos/${owner}/${repo}/pulls/${prNumber}`;
250
+ const prDetailsResult = await quietProbe($)`gh api repos/${owner}/${repo}/pulls/${prNumber}`;
246
251
  if (prDetailsResult.code === 0) {
247
252
  const prDetails = JSON.parse(prDetailsResult.stdout.toString());
248
253
  const prUpdatedAt = new Date(prDetails.updated_at);
@@ -265,7 +270,7 @@ export const detectAndCountFeedback = async params => {
265
270
 
266
271
  // Check issue description edit time if we have an issue
267
272
  if (issueNumber) {
268
- const issueDetailsResult = await $`gh api repos/${owner}/${repo}/issues/${issueNumber}`;
273
+ const issueDetailsResult = await quietProbe($)`gh api repos/${owner}/${repo}/issues/${issueNumber}`;
269
274
  if (issueDetailsResult.code === 0) {
270
275
  const issueDetails = JSON.parse(issueDetailsResult.stdout.toString());
271
276
  const issueUpdatedAt = new Date(issueDetails.updated_at);
@@ -300,12 +305,12 @@ export const detectAndCountFeedback = async params => {
300
305
 
301
306
  // 3. Check for new commits on default branch
302
307
  try {
303
- const defaultBranchResult = await $`gh api repos/${owner}/${repo}`;
308
+ const defaultBranchResult = await quietProbe($)`gh api repos/${owner}/${repo}`;
304
309
  if (defaultBranchResult.code === 0) {
305
310
  const repoData = JSON.parse(defaultBranchResult.stdout.toString());
306
311
  const defaultBranch = repoData.default_branch;
307
312
 
308
- const commitsResult = await $`gh api repos/${owner}/${repo}/commits --paginate --field sha=${defaultBranch} --field since=${lastCommitTime.toISOString()}`;
313
+ const commitsResult = await quietProbe($)`gh api repos/${owner}/${repo}/commits --paginate --field sha=${defaultBranch} --field since=${lastCommitTime.toISOString()}`;
309
314
  if (commitsResult.code === 0) {
310
315
  const commits = JSON.parse(commitsResult.stdout.toString());
311
316
  if (commits.length > 0) {
@@ -353,10 +358,10 @@ export const detectAndCountFeedback = async params => {
353
358
 
354
359
  // 6. Check for failed PR checks
355
360
  try {
356
- const prHeadResult = await $`gh api repos/${owner}/${repo}/pulls/${prNumber} --jq '.head.sha'`;
361
+ const prHeadResult = await quietProbe($)`gh api repos/${owner}/${repo}/pulls/${prNumber} --jq '.head.sha'`;
357
362
  if (prHeadResult.code === 0) {
358
363
  const prHeadSha = prHeadResult.stdout.toString().trim();
359
- const checksResult = await $`gh api repos/${owner}/${repo}/commits/${prHeadSha}/check-runs --paginate --slurp`;
364
+ const checksResult = await quietProbe($)`gh api repos/${owner}/${repo}/commits/${prHeadSha}/check-runs --paginate --slurp`;
360
365
  const checkRuns = checksResult.code === 0 ? JSON.parse(checksResult.stdout.toString() || '[]').flatMap(page => page.check_runs || []) : [];
361
366
  const failedChecks = checkRuns.filter(check => check.conclusion === 'failure' && new Date(check.completed_at) > lastCommitTime);
362
367
 
@@ -379,7 +384,7 @@ export const detectAndCountFeedback = async params => {
379
384
 
380
385
  // 7. Check for review requests with changes requested
381
386
  try {
382
- const reviewsResult = await $`gh api repos/${owner}/${repo}/pulls/${prNumber}/reviews --paginate`;
387
+ const reviewsResult = await quietProbe($)`gh api repos/${owner}/${repo}/pulls/${prNumber}/reviews --paginate`;
383
388
  if (reviewsResult.code === 0) {
384
389
  const reviews = JSON.parse(reviewsResult.stdout.toString());
385
390
  const changesRequestedReviews = reviews.filter(review => review.state === 'CHANGES_REQUESTED' && new Date(review.submitted_at) > lastCommitTime);