@link-assistant/hive-mind 2.1.2 → 2.1.4

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.
@@ -14,7 +14,7 @@ import utc from 'dayjs/plugin/utc.js';
14
14
 
15
15
  import { wrapDollarWithGhRetry as _wrapDollarWithGhRetry, execGhWithRetry } from './github-rate-limit.lib.mjs'; // rate-limit marker (#1726): gh API calls flow through $ wrapped by caller. execGhWithRetry adds transient-network retry (#1756).
16
16
  import { formatLimitResetsAt, formatLimitResetsIn, formatLocalizedCurrentTime, formatLocalizedRelativeTime, formatLocalizedResetTime, localizeCompactDuration, lt, resolveLimitLocale } from './limits-i18n.lib.mjs';
17
- import { formatSubscriptionLines, getCachedClaudeSubscription, getCachedCodexSubscription, getClaudeSubscriptionInfo, getCodexSubscriptionInfo } from './limits-subscription.lib.mjs';
17
+ import { formatSubscriptionHeading, formatSubscriptionLines, getCachedClaudeSubscription, getCachedCodexSubscription, getClaudeSubscriptionInfo, getCodexSubscriptionInfo } from './limits-subscription.lib.mjs';
18
18
  export { getCachedClaudeSubscription, getCachedCodexSubscription, getClaudeSubscriptionInfo, getCodexSubscriptionInfo };
19
19
  // Initialize dayjs plugins
20
20
  dayjs.extend(utc);
@@ -280,6 +280,61 @@ function getLocalizedRelativeReset(window, options = {}, fallbackRelative = null
280
280
  return formatRelativeTime(window?.resetsAt, options) || localizeCompactDuration(fallbackRelative, options);
281
281
  }
282
282
 
283
+ function formatCodeBlock(content) {
284
+ const text = Array.isArray(content) ? content.join('\n') : String(content ?? '');
285
+ return '```\n' + (text.endsWith('\n') ? text : `${text}\n`) + '```';
286
+ }
287
+
288
+ function formatPlainTitledCodeSection(section) {
289
+ const text = String(section ?? '').trimEnd();
290
+ if (!text) return '';
291
+ if (text.includes('```')) return text;
292
+
293
+ const lines = text.split('\n');
294
+ const title = lines.shift();
295
+ const body = lines.join('\n');
296
+ if (!body) return formatCodeBlock(title);
297
+ return `${title}\n\n${formatCodeBlock(body)}`;
298
+ }
299
+
300
+ function formatLimitWindowSection(label, window, periodHours, threshold, options = {}) {
301
+ const locale = resolveLimitLocale(options);
302
+ let section = `${label}\n`;
303
+ if (hasLimitPercentage(window)) {
304
+ const timePassed = calculateTimePassedPercentage(window.resetsAt, periodHours);
305
+ if (timePassed !== null) {
306
+ section += `${getProgressBar(timePassed)} ${timePassed}% ${lt('passed', {}, { locale })}\n`;
307
+ }
308
+
309
+ const pct = Math.floor(window.percentage);
310
+ const bar = getProgressBar(pct, threshold);
311
+ const suffix = pct >= threshold ? ' ⚠️' : ` ${lt('used', {}, { locale })}`;
312
+ section += `${bar} ${pct}%${suffix}\n`;
313
+
314
+ const resetTime = getLocalizedResetTime(window, { locale });
315
+ if (resetTime) {
316
+ const relativeTime = getLocalizedRelativeReset(window, { locale });
317
+ section += relativeTime ? `${formatLimitResetsIn(relativeTime, resetTime, { locale })}\n` : `${formatLimitResetsAt(resetTime, { locale })}\n`;
318
+ }
319
+ } else {
320
+ section += `${lt('na', {}, { locale })}\n`;
321
+ }
322
+ return section;
323
+ }
324
+
325
+ function hasPositivePercentage(value) {
326
+ if (value === null || value === undefined) return false;
327
+ const numeric = Number(value);
328
+ return Number.isFinite(numeric) && numeric > 0;
329
+ }
330
+
331
+ function hasPositiveCreditBalance(credits) {
332
+ if (!credits) return false;
333
+ if (credits.unlimited) return true;
334
+ const numeric = Number.parseFloat(String(credits.balance ?? '0'));
335
+ return Number.isFinite(numeric) && numeric > 0;
336
+ }
337
+
283
338
  /**
284
339
  * Get GitHub API rate limits by calling gh api rate_limit
285
340
  * Returns rate limit info for core, search, graphql, and other resources
@@ -1061,118 +1116,51 @@ export function formatUsageMessage(usage, diskSpace = null, githubRateLimit = nu
1061
1116
  sections.push(section);
1062
1117
  }
1063
1118
 
1064
- // Claude limits section
1065
- // When there's an error (e.g., auth expired), show it once and skip empty subsections
1119
+ const claudeHeading = formatSubscriptionHeading('claude', subscription, { locale });
1120
+ const useShortClaudeLabels = Boolean(claudeHeading);
1121
+ const claudeSections = [];
1122
+
1123
+ // Claude limits section. When there's an error (e.g., auth expired), show it once and skip empty subsections.
1066
1124
  if (claudeError) {
1067
- sections.push(`${lt('claude_limits', {}, { locale })}\n${claudeError}\n`);
1125
+ claudeSections.push(useShortClaudeLabels ? `${claudeError}\n` : `${lt('claude_limits', {}, { locale })}\n${claudeError}\n`);
1068
1126
  } else {
1069
- // Claude 5 hour session (five_hour)
1070
- // Threshold: One-at-a-time mode when usage >= 65%
1071
- let sessionSection = `${lt('claude_5_hour_session', {}, { locale })}\n`;
1072
- if (hasLimitPercentage(usage?.currentSession)) {
1073
- const timePassed = calculateTimePassedPercentage(usage.currentSession.resetsAt, 5);
1074
- if (timePassed !== null) {
1075
- const timeBar = getProgressBar(timePassed);
1076
- sessionSection += `${timeBar} ${timePassed}% ${lt('passed', {}, { locale })}\n`;
1077
- }
1078
-
1079
- // Use Math.floor so 100% only appears when usage is exactly 100%
1080
- // See: https://github.com/link-assistant/hive-mind/issues/1133
1081
- const pct = Math.floor(usage.currentSession.percentage);
1082
- const bar = getProgressBar(pct, DISPLAY_THRESHOLDS.CLAUDE_5_HOUR_SESSION);
1083
- const suffix = pct >= DISPLAY_THRESHOLDS.CLAUDE_5_HOUR_SESSION ? ' ⚠️' : ` ${lt('used', {}, { locale })}`;
1084
- sessionSection += `${bar} ${pct}%${suffix}\n`;
1085
-
1086
- const sessionResetTime = getLocalizedResetTime(usage.currentSession, { locale });
1087
- if (sessionResetTime) {
1088
- const relativeTime = getLocalizedRelativeReset(usage.currentSession, { locale });
1089
- if (relativeTime) {
1090
- sessionSection += `${formatLimitResetsIn(relativeTime, sessionResetTime, { locale })}\n`;
1091
- } else {
1092
- sessionSection += `${formatLimitResetsAt(sessionResetTime, { locale })}\n`;
1093
- }
1094
- }
1095
- } else {
1096
- sessionSection += `${lt('na', {}, { locale })}\n`;
1127
+ const hasSonnetOnly = hasLimitPercentage(usage?.sonnetOnly);
1128
+ claudeSections.push(formatLimitWindowSection(useShortClaudeLabels ? lt('five_hour_limit_session', {}, { locale }) : lt('claude_5_hour_session', {}, { locale }), usage?.currentSession, 5, DISPLAY_THRESHOLDS.CLAUDE_5_HOUR_SESSION, { locale }));
1129
+ claudeSections.push(formatLimitWindowSection(useShortClaudeLabels && !hasSonnetOnly ? lt('current_week', {}, { locale }) : lt('current_week_all_models', {}, { locale }), usage?.allModels, 168, DISPLAY_THRESHOLDS.CLAUDE_WEEKLY, { locale }));
1130
+ if (hasSonnetOnly || !useShortClaudeLabels) {
1131
+ claudeSections.push(formatLimitWindowSection(lt('current_week_sonnet_only', {}, { locale }), usage?.sonnetOnly, 168, DISPLAY_THRESHOLDS.CLAUDE_WEEKLY, { locale }));
1097
1132
  }
1098
- sections.push(sessionSection);
1099
-
1100
- // Current week (all models / seven_day)
1101
- // Threshold: One-at-a-time mode when usage >= 97%
1102
- let allModelsSection = `${lt('current_week_all_models', {}, { locale })}\n`;
1103
- if (hasLimitPercentage(usage?.allModels)) {
1104
- const timePassed = calculateTimePassedPercentage(usage.allModels.resetsAt, 168);
1105
- if (timePassed !== null) {
1106
- const timeBar = getProgressBar(timePassed);
1107
- allModelsSection += `${timeBar} ${timePassed}% ${lt('passed', {}, { locale })}\n`;
1108
- }
1109
1133
 
1110
- // Use Math.floor so 100% only appears when usage is exactly 100%
1111
- // See: https://github.com/link-assistant/hive-mind/issues/1133
1112
- const pct = Math.floor(usage.allModels.percentage);
1113
- const bar = getProgressBar(pct, DISPLAY_THRESHOLDS.CLAUDE_WEEKLY);
1114
- const suffix = pct >= DISPLAY_THRESHOLDS.CLAUDE_WEEKLY ? ' ⚠️' : ` ${lt('used', {}, { locale })}`;
1115
- allModelsSection += `${bar} ${pct}%${suffix}\n`;
1116
-
1117
- const allModelsResetTime = getLocalizedResetTime(usage.allModels, { locale });
1118
- if (allModelsResetTime) {
1119
- const relativeTime = getLocalizedRelativeReset(usage.allModels, { locale });
1120
- if (relativeTime) {
1121
- allModelsSection += `${formatLimitResetsIn(relativeTime, allModelsResetTime, { locale })}\n`;
1122
- } else {
1123
- allModelsSection += `${formatLimitResetsAt(allModelsResetTime, { locale })}\n`;
1124
- }
1125
- }
1126
- } else {
1127
- allModelsSection += `${lt('na', {}, { locale })}\n`;
1134
+ if (!useShortClaudeLabels) {
1135
+ const subscriptionLines = formatSubscriptionLines(subscription, { locale });
1136
+ if (subscriptionLines) claudeSections.push(subscriptionLines);
1128
1137
  }
1129
- sections.push(allModelsSection);
1130
-
1131
- // Current week (Sonnet only / seven_day_sonnet)
1132
- // Threshold: One-at-a-time mode when usage >= 97% (same as all models)
1133
- let sonnetSection = `${lt('current_week_sonnet_only', {}, { locale })}\n`;
1134
- if (hasLimitPercentage(usage?.sonnetOnly)) {
1135
- // Add time passed progress bar first (no threshold marker for time)
1136
- const timePassed = calculateTimePassedPercentage(usage.sonnetOnly.resetsAt, 168);
1137
- if (timePassed !== null) {
1138
- const timeBar = getProgressBar(timePassed);
1139
- sonnetSection += `${timeBar} ${timePassed}% ${lt('passed', {}, { locale })}\n`;
1140
- }
1138
+ }
1141
1139
 
1142
- // Add usage progress bar second with threshold marker
1143
- // Use Math.floor so 100% only appears when usage is exactly 100%
1144
- // See: https://github.com/link-assistant/hive-mind/issues/1133
1145
- const pct = Math.floor(usage.sonnetOnly.percentage);
1146
- const bar = getProgressBar(pct, DISPLAY_THRESHOLDS.CLAUDE_WEEKLY);
1147
- const suffix = pct >= DISPLAY_THRESHOLDS.CLAUDE_WEEKLY ? ' ⚠️' : ` ${lt('used', {}, { locale })}`;
1148
- sonnetSection += `${bar} ${pct}%${suffix}\n`;
1149
-
1150
- const sonnetResetTime = getLocalizedResetTime(usage.sonnetOnly, { locale });
1151
- if (sonnetResetTime) {
1152
- const relativeTime = getLocalizedRelativeReset(usage.sonnetOnly, { locale });
1153
- if (relativeTime) {
1154
- sonnetSection += `${formatLimitResetsIn(relativeTime, sonnetResetTime, { locale })}\n`;
1155
- } else {
1156
- sonnetSection += `${formatLimitResetsAt(sonnetResetTime, { locale })}\n`;
1157
- }
1158
- }
1159
- } else {
1160
- sonnetSection += `${lt('na', {}, { locale })}\n`;
1140
+ const hasFencedExtraSection = extraSections.some(extra => String(extra ?? '').includes('```'));
1141
+ const useSplitLayout = Boolean(claudeHeading) || hasFencedExtraSection;
1142
+
1143
+ if (!useSplitLayout) {
1144
+ sections.push(...claudeSections);
1145
+ for (const extra of extraSections) {
1146
+ sections.push(extra);
1161
1147
  }
1162
- sections.push(sonnetSection);
1148
+ return formatCodeBlock(sections.join('\n'));
1149
+ }
1163
1150
 
1164
- const subscriptionLines = formatSubscriptionLines(subscription, { locale });
1165
- if (subscriptionLines) sections.push(subscriptionLines);
1151
+ const markdownSections = [];
1152
+ markdownSections.push(formatCodeBlock(claudeHeading ? sections.join('\n') : [...sections, ...claudeSections].join('\n')));
1153
+ if (claudeHeading) {
1154
+ markdownSections.push(claudeHeading);
1155
+ markdownSections.push(formatCodeBlock(claudeSections.join('\n')));
1166
1156
  }
1167
1157
 
1168
- // Append any caller-provided extra sections (e.g. queue status) inside the code block
1169
1158
  for (const extra of extraSections) {
1170
- sections.push(extra);
1159
+ const formatted = formatPlainTitledCodeSection(extra);
1160
+ if (formatted) markdownSections.push(formatted);
1171
1161
  }
1172
1162
 
1173
- // Wrap all sections in a single code block for monospace font / aligned progress bars.
1174
- // Sections are separated by blank lines; the trailing newline on each section provides spacing.
1175
- return '```\n' + sections.join('\n') + '```';
1163
+ return markdownSections.join('\n\n');
1176
1164
  }
1177
1165
 
1178
1166
  /**
@@ -1185,64 +1173,33 @@ export function formatUsageMessage(usage, diskSpace = null, githubRateLimit = nu
1185
1173
  */
1186
1174
  export function formatCodexLimitsSection(codexLimits, codexError = null, options = {}) {
1187
1175
  const locale = resolveLimitLocale(options);
1188
- if (codexError) {
1189
- return `${lt('codex_limits', {}, { locale })}\n${codexError}\n`;
1190
- }
1191
-
1176
+ const subscription = options?.subscription || null;
1192
1177
  const usage = codexLimits?.usage || null;
1193
1178
  const additionalRateLimits = codexLimits?.additionalRateLimits || [];
1194
1179
  const credits = codexLimits?.credits || null;
1195
- const planType = codexLimits?.planType || null;
1196
- const subscription = options?.subscription || null;
1180
+ const planType = subscription?.planType || codexLimits?.planType || null;
1181
+ const heading = formatSubscriptionHeading('codex', subscription, { locale, planType });
1182
+ const useTitledLayout = Boolean(heading);
1197
1183
 
1198
- let section = `${lt('codex_limits', {}, { locale })}\n`;
1199
- if (planType) {
1200
- section += `${lt('plan', {}, { locale })}: ${planType}\n`;
1184
+ if (codexError) {
1185
+ const errorSection = useTitledLayout ? `${codexError}\n` : `${lt('codex_limits', {}, { locale })}\n${codexError}\n`;
1186
+ return useTitledLayout ? `${heading}\n\n${formatCodeBlock(errorSection)}` : errorSection;
1201
1187
  }
1202
1188
 
1203
- let sessionSection = `${lt('codex_5_hour_session', {}, { locale })}\n`;
1204
- if (hasLimitPercentage(usage?.currentSession)) {
1205
- const timePassed = calculateTimePassedPercentage(usage.currentSession.resetsAt, 5);
1206
- if (timePassed !== null) {
1207
- sessionSection += `${getProgressBar(timePassed)} ${timePassed}% ${lt('passed', {}, { locale })}\n`;
1208
- }
1209
- const pct = Math.floor(usage.currentSession.percentage);
1210
- const bar = getProgressBar(pct, DISPLAY_THRESHOLDS.CODEX_5_HOUR_SESSION);
1211
- const suffix = pct >= DISPLAY_THRESHOLDS.CODEX_5_HOUR_SESSION ? ' ⚠️' : ` ${lt('used', {}, { locale })}`;
1212
- sessionSection += `${bar} ${pct}%${suffix}\n`;
1213
- const sessionResetTime = getLocalizedResetTime(usage.currentSession, { locale });
1214
- if (sessionResetTime) {
1215
- const relativeTime = getLocalizedRelativeReset(usage.currentSession, { locale });
1216
- sessionSection += relativeTime ? `${formatLimitResetsIn(relativeTime, sessionResetTime, { locale })}\n` : `${formatLimitResetsAt(sessionResetTime, { locale })}\n`;
1217
- }
1218
- } else {
1219
- sessionSection += `${lt('na', {}, { locale })}\n`;
1189
+ let section = useTitledLayout ? '' : `${lt('codex_limits', {}, { locale })}\n`;
1190
+ if (planType && !useTitledLayout) {
1191
+ section += `${lt('plan', {}, { locale })}: ${planType}\n`;
1220
1192
  }
1221
1193
 
1222
- let weeklySection = `${lt('current_week_all_models', {}, { locale })}\n`;
1223
- if (hasLimitPercentage(usage?.allModels)) {
1224
- const timePassed = calculateTimePassedPercentage(usage.allModels.resetsAt, 168);
1225
- if (timePassed !== null) {
1226
- weeklySection += `${getProgressBar(timePassed)} ${timePassed}% ${lt('passed', {}, { locale })}\n`;
1227
- }
1228
- const pct = Math.floor(usage.allModels.percentage);
1229
- const bar = getProgressBar(pct, DISPLAY_THRESHOLDS.CODEX_WEEKLY);
1230
- const suffix = pct >= DISPLAY_THRESHOLDS.CODEX_WEEKLY ? ' ⚠️' : ` ${lt('used', {}, { locale })}`;
1231
- weeklySection += `${bar} ${pct}%${suffix}\n`;
1232
- const weeklyResetTime = getLocalizedResetTime(usage.allModels, { locale });
1233
- if (weeklyResetTime) {
1234
- const relativeTime = getLocalizedRelativeReset(usage.allModels, { locale });
1235
- weeklySection += relativeTime ? `${formatLimitResetsIn(relativeTime, weeklyResetTime, { locale })}\n` : `${formatLimitResetsAt(weeklyResetTime, { locale })}\n`;
1236
- }
1237
- } else {
1238
- weeklySection += `${lt('na', {}, { locale })}\n`;
1239
- }
1194
+ const sessionSection = formatLimitWindowSection(useTitledLayout ? lt('five_hour_limit_session', {}, { locale }) : lt('codex_5_hour_session', {}, { locale }), usage?.currentSession, 5, DISPLAY_THRESHOLDS.CODEX_5_HOUR_SESSION, { locale });
1195
+ const weeklySection = formatLimitWindowSection(useTitledLayout ? lt('current_week', {}, { locale }) : lt('current_week_all_models', {}, { locale }), usage?.allModels, 168, DISPLAY_THRESHOLDS.CODEX_WEEKLY, { locale });
1240
1196
 
1241
1197
  section += `${sessionSection}\n${weeklySection}`;
1242
1198
 
1243
- if (additionalRateLimits.length > 0) {
1199
+ const visibleAdditionalRateLimits = additionalRateLimits.filter(limit => hasPositivePercentage(limit.allModels?.percentage));
1200
+ if (visibleAdditionalRateLimits.length > 0) {
1244
1201
  section += `\n${lt('additional_codex_limits', {}, { locale })}\n`;
1245
- for (const limit of additionalRateLimits) {
1202
+ for (const limit of visibleAdditionalRateLimits) {
1246
1203
  const sessionPct = limit.currentSession?.percentage;
1247
1204
  const weeklyPct = limit.allModels?.percentage;
1248
1205
  const sessionText = sessionPct === null || sessionPct === undefined ? `${lt('session', {}, { locale })} ${lt('na', {}, { locale })}` : `${lt('session', {}, { locale })} ${Math.floor(sessionPct)}%`;
@@ -1251,15 +1208,17 @@ export function formatCodexLimitsSection(codexLimits, codexError = null, options
1251
1208
  }
1252
1209
  }
1253
1210
 
1254
- if (credits) {
1211
+ if (hasPositiveCreditBalance(credits)) {
1255
1212
  const creditSummary = credits.unlimited ? lt('unlimited', {}, { locale }) : `${credits.balance ?? '0'} ${lt('balance', {}, { locale })}`;
1256
1213
  section += `\n${lt('codex_credits', {}, { locale })}\n${creditSummary}\n`;
1257
1214
  }
1258
1215
 
1259
- const subscriptionLines = formatSubscriptionLines(subscription, { locale });
1260
- if (subscriptionLines) section += subscriptionLines;
1216
+ if (!useTitledLayout) {
1217
+ const subscriptionLines = formatSubscriptionLines(subscription, { locale });
1218
+ if (subscriptionLines) section += subscriptionLines;
1219
+ }
1261
1220
 
1262
- return section;
1221
+ return useTitledLayout ? `${heading}\n\n${formatCodeBlock(section)}` : section;
1263
1222
  }
1264
1223
 
1265
1224
  // ============================================================================
@@ -0,0 +1,221 @@
1
+ /**
2
+ * Shared capability matrix for live issue/PR event input.
3
+ *
4
+ * Issue #2007 asks solve to feed issue/PR events into the running AI tool "in
5
+ * all ways possible", and to provide a universal fallback for every tool that
6
+ * does not have a mid-session live input channel: wait for the current turn to
7
+ * finish in the JSON output, stop the process, and resume the AI session with
8
+ * the new events.
9
+ *
10
+ * Because of that fallback, live event input is *available* for every tool.
11
+ * Tools differ only in the delivery `mode`:
12
+ *
13
+ * - `stream` : the tool exposes a live stdin/JSON channel, so new events are
14
+ * written into the running process without restarting it. Claude
15
+ * and Agent (`--input-format stream-json`) are wired for this today.
16
+ * - `fallback`: no verified mid-session input channel exists yet, so solve uses
17
+ * the restart/resume loop (`--auto-restart-until-mergeable` /
18
+ * `watchUntilMergeable`). It waits for the current session to end,
19
+ * then resumes/restarts the AI with the new issue/PR events as
20
+ * feedback. This works for every tool.
21
+ *
22
+ * Missing native live-input features for each tool are reported upstream in the
23
+ * https://github.com/link-assistant/agent repository so they can be implemented
24
+ * (see `agentIssue`), after which a tool can graduate from `fallback` to `stream`.
25
+ */
26
+
27
+ export const ISSUE_2007_REQUIRED_EVENT_IDS = Object.freeze(['issue-title', 'issue-body', 'issue-comments', 'pull-request-comments']);
28
+
29
+ export const LIVE_INPUT_EVENT_SOURCES = Object.freeze([
30
+ Object.freeze({
31
+ id: 'issue-title',
32
+ label: 'Issue title updates',
33
+ requiredByIssue2007: true,
34
+ note: 'User-facing issue metadata should be streamed when it changes during a run.',
35
+ }),
36
+ Object.freeze({
37
+ id: 'issue-body',
38
+ label: 'Issue description updates',
39
+ requiredByIssue2007: true,
40
+ note: 'The issue body is treated as user feedback because it can change after the agent starts.',
41
+ }),
42
+ Object.freeze({
43
+ id: 'issue-comments',
44
+ label: 'Issue comments',
45
+ requiredByIssue2007: true,
46
+ note: 'New non-system issue comments are user feedback.',
47
+ }),
48
+ Object.freeze({
49
+ id: 'pull-request-comments',
50
+ label: 'Pull request comments',
51
+ requiredByIssue2007: true,
52
+ note: 'New non-system PR conversation comments and review comments should reach the agent.',
53
+ }),
54
+ Object.freeze({
55
+ id: 'pull-request-description',
56
+ label: 'Pull request description updates',
57
+ requiredByIssue2007: false,
58
+ note: 'Issue #2007 explicitly treats the PR description as AI-owned, so it is not a required user-feedback source.',
59
+ }),
60
+ ]);
61
+
62
+ const REQUIRED_EVENTS = ISSUE_2007_REQUIRED_EVENT_IDS;
63
+
64
+ // Delivery modes for live issue/PR event input.
65
+ export const LIVE_INPUT_MODE_STREAM = 'stream';
66
+ export const LIVE_INPUT_MODE_FALLBACK = 'fallback';
67
+
68
+ // Shared description of the universal restart/resume fallback so every tool
69
+ // entry reports it identically.
70
+ const FALLBACK_DESCRIPTION = 'Universal fallback: wait for the current AI turn to finish in the JSON output, stop the process, then resume/restart the AI session with the new issue/PR events as feedback via --auto-restart-until-mergeable (watchUntilMergeable). Works for every tool even without a live stdin channel.';
71
+
72
+ const CAPABILITIES = Object.freeze({
73
+ claude: Object.freeze({
74
+ tool: 'claude',
75
+ label: 'Claude',
76
+ available: true,
77
+ mode: LIVE_INPUT_MODE_STREAM,
78
+ liveStreaming: true,
79
+ supported: true,
80
+ option: '--auto-input-until-mergeable',
81
+ protocol: 'claude --input-format stream-json stdin NDJSON',
82
+ currentRunner: 'src/claude.lib.mjs keeps stdin as a pipe and attaches bidirectional-interactive.lib.mjs',
83
+ futureProtocol: '',
84
+ fallback: FALLBACK_DESCRIPTION,
85
+ events: REQUIRED_EVENTS,
86
+ agentIssue: '',
87
+ unsupportedReason: '',
88
+ testing: 'Run solve with --tool claude --auto-input-until-mergeable, add an issue or PR comment while the Claude process is alive, and watch for the bidirectional handler to queue or stream a user frame into stdin.',
89
+ }),
90
+ codex: Object.freeze({
91
+ tool: 'codex',
92
+ label: 'Codex',
93
+ available: true,
94
+ mode: LIVE_INPUT_MODE_FALLBACK,
95
+ liveStreaming: false,
96
+ supported: false,
97
+ option: '--auto-input-until-mergeable',
98
+ protocol: 'Restart/resume fallback (no live stdin wired through solve for Codex yet).',
99
+ currentRunner: 'src/codex.lib.mjs uses codex exec with prompt/stdin context at process start',
100
+ futureProtocol: 'Codex app-server JSON-RPC turn/steer',
101
+ fallback: FALLBACK_DESCRIPTION,
102
+ events: REQUIRED_EVENTS,
103
+ agentIssue: 'https://github.com/link-assistant/agent/issues',
104
+ unsupportedReason: 'The current solve Codex runner uses codex exec, whose stdin is one-shot prompt/context at process start. It does not expose a live JSON input pipe for mid-session issue/PR events, so the restart/resume fallback is used. Codex app-server turn/steer is the candidate protocol for a future live-streaming Codex runner.',
105
+ testing: 'Passing --tool codex --auto-input-until-mergeable activates the restart/resume fallback: the run finishes the current session, then resumes with the new issue/PR events.',
106
+ }),
107
+ agent: Object.freeze({
108
+ tool: 'agent',
109
+ label: 'Agent',
110
+ available: true,
111
+ mode: LIVE_INPUT_MODE_STREAM,
112
+ liveStreaming: true,
113
+ supported: true,
114
+ option: '--auto-input-until-mergeable',
115
+ protocol: 'agent --input-format stream-json --output-format stream-json stdin/stdout NDJSON',
116
+ currentRunner: 'src/agent.lib.mjs keeps stdin as a pipe and attaches bidirectional-interactive.lib.mjs when live input is enabled',
117
+ futureProtocol: '',
118
+ fallback: FALLBACK_DESCRIPTION,
119
+ events: REQUIRED_EVENTS,
120
+ agentIssue: 'https://github.com/link-assistant/agent/pull/274',
121
+ unsupportedReason: '',
122
+ testing: 'Run solve with --tool agent --auto-input-until-mergeable, add an issue or PR comment while the Agent process is alive, and watch for the bidirectional handler to queue or stream a user frame into stdin.',
123
+ }),
124
+ opencode: Object.freeze({
125
+ tool: 'opencode',
126
+ label: 'OpenCode',
127
+ available: true,
128
+ mode: LIVE_INPUT_MODE_FALLBACK,
129
+ liveStreaming: false,
130
+ supported: false,
131
+ option: '--auto-input-until-mergeable',
132
+ protocol: 'Restart/resume fallback (no live JSON input channel wired through solve for OpenCode yet).',
133
+ currentRunner: 'src/opencode.lib.mjs uses a prompt-via-file/stdin pattern',
134
+ futureProtocol: '',
135
+ fallback: FALLBACK_DESCRIPTION,
136
+ events: REQUIRED_EVENTS,
137
+ agentIssue: 'https://github.com/link-assistant/agent/issues',
138
+ unsupportedReason: 'No verified live JSON input channel is wired through solve for OpenCode yet, so the restart/resume fallback is used.',
139
+ testing: 'Passing --tool opencode --auto-input-until-mergeable activates the restart/resume fallback.',
140
+ }),
141
+ gemini: Object.freeze({
142
+ tool: 'gemini',
143
+ label: 'Gemini',
144
+ available: true,
145
+ mode: LIVE_INPUT_MODE_FALLBACK,
146
+ liveStreaming: false,
147
+ supported: false,
148
+ option: '--auto-input-until-mergeable',
149
+ protocol: 'Restart/resume fallback (no live JSON input channel wired through solve for Gemini yet).',
150
+ currentRunner: 'src/gemini.lib.mjs uses a prompt-driven process invocation',
151
+ futureProtocol: '',
152
+ fallback: FALLBACK_DESCRIPTION,
153
+ events: REQUIRED_EVENTS,
154
+ agentIssue: 'https://github.com/link-assistant/agent/issues',
155
+ unsupportedReason: 'No verified live JSON input channel is wired through solve for Gemini yet, so the restart/resume fallback is used.',
156
+ testing: 'Passing --tool gemini --auto-input-until-mergeable activates the restart/resume fallback.',
157
+ }),
158
+ qwen: Object.freeze({
159
+ tool: 'qwen',
160
+ label: 'Qwen',
161
+ available: true,
162
+ mode: LIVE_INPUT_MODE_FALLBACK,
163
+ liveStreaming: false,
164
+ supported: false,
165
+ option: '--auto-input-until-mergeable',
166
+ protocol: 'Restart/resume fallback (no live JSON input channel wired through solve for Qwen yet).',
167
+ currentRunner: 'src/qwen.lib.mjs uses a prompt-driven process invocation',
168
+ futureProtocol: '',
169
+ fallback: FALLBACK_DESCRIPTION,
170
+ events: REQUIRED_EVENTS,
171
+ agentIssue: 'https://github.com/link-assistant/agent/issues',
172
+ unsupportedReason: 'No verified live JSON input channel is wired through solve for Qwen yet, so the restart/resume fallback is used.',
173
+ testing: 'Passing --tool qwen --auto-input-until-mergeable activates the restart/resume fallback.',
174
+ }),
175
+ });
176
+
177
+ const UNKNOWN_CAPABILITY = tool =>
178
+ Object.freeze({
179
+ tool,
180
+ label: tool,
181
+ available: true,
182
+ mode: LIVE_INPUT_MODE_FALLBACK,
183
+ liveStreaming: false,
184
+ supported: false,
185
+ option: '--auto-input-until-mergeable',
186
+ protocol: 'Restart/resume fallback (no live JSON input channel wired through solve for this tool yet).',
187
+ currentRunner: 'Unknown or custom solve tool runner',
188
+ futureProtocol: '',
189
+ fallback: FALLBACK_DESCRIPTION,
190
+ events: REQUIRED_EVENTS,
191
+ agentIssue: 'https://github.com/link-assistant/agent/issues',
192
+ unsupportedReason: `No verified live JSON input channel is wired through solve for ${tool}, so the restart/resume fallback is used. Add a live-input capability entry (and report the missing native API to link-assistant/agent) once the runner has a long-lived stdin, JSON-RPC, or SDK channel that accepts new user turns mid-session.`,
193
+ testing: 'The flag activates the restart/resume fallback until a live-streaming runner is implemented.',
194
+ });
195
+
196
+ export const getLiveInputCapability = tool => {
197
+ const normalizedTool = String(tool || '')
198
+ .trim()
199
+ .toLowerCase();
200
+ return CAPABILITIES[normalizedTool] || UNKNOWN_CAPABILITY(normalizedTool || 'unknown');
201
+ };
202
+
203
+ /**
204
+ * Whether the tool has a live *streaming* input channel (writes events into the
205
+ * running process). Kept as `isLiveInputSupported` for backward compatibility;
206
+ * it is the stream-mode predicate, not "is live input available at all".
207
+ */
208
+ export const isLiveInputSupported = tool => getLiveInputCapability(tool).mode === LIVE_INPUT_MODE_STREAM;
209
+
210
+ /**
211
+ * Whether live issue/PR event input is available for the tool in *any* mode
212
+ * (streaming or restart/resume fallback). This is true for every tool.
213
+ */
214
+ export const isLiveInputAvailable = tool => getLiveInputCapability(tool).available === true;
215
+
216
+ /**
217
+ * Resolve the delivery mode ('stream' or 'fallback') for a tool.
218
+ */
219
+ export const getLiveInputMode = tool => getLiveInputCapability(tool).mode;
220
+
221
+ export const getLiveInputCapabilityRows = () => Object.values(CAPABILITIES);
@@ -192,6 +192,11 @@ en
192
192
  hour
193
193
  session "Claude 5 hour session"
194
194
  limits "Claude limits"
195
+ subscription
196
+ title "Claude{{plan}} subscription"
197
+ chatgpt
198
+ subscription
199
+ title "ChatGPT{{plan}} subscription"
195
200
  codex
196
201
  5
197
202
  hour
@@ -205,6 +210,7 @@ en
205
210
  current
206
211
  time "Current time"
207
212
  week
213
+ label "Current week"
208
214
  all
209
215
  models "Current week (all models)"
210
216
  sonnet
@@ -226,6 +232,8 @@ en
226
232
  end "End"
227
233
  five
228
234
  hour
235
+ limit
236
+ session "5 hour session"
229
237
  session "5h session"
230
238
  min
231
239
  load
@@ -314,6 +322,14 @@ en
314
322
  session "session"
315
323
  start "Start"
316
324
  subscription
325
+ detail
326
+ ends
327
+ label "ends {{time}}"
328
+ in "ends in {{duration}}; {{time}}"
329
+ trial
330
+ ends
331
+ label "trial ends {{time}}"
332
+ in "trial ends in {{duration}}; {{time}}"
317
333
  ends
318
334
  label "Subscription ends {{time}}"
319
335
  in "Subscription ends in {{duration}} ({{time}})"
@@ -192,6 +192,11 @@ hi
192
192
  hour
193
193
  session "Claude 5 घंटे का सत्र"
194
194
  limits "Claude सीमाएँ"
195
+ subscription
196
+ title "Claude{{plan}} सदस्यता"
197
+ chatgpt
198
+ subscription
199
+ title "ChatGPT{{plan}} सदस्यता"
195
200
  codex
196
201
  5
197
202
  hour
@@ -205,6 +210,7 @@ hi
205
210
  current
206
211
  time "वर्तमान समय"
207
212
  week
213
+ label "वर्तमान सप्ताह"
208
214
  all
209
215
  models "वर्तमान सप्ताह (सभी मॉडल)"
210
216
  sonnet
@@ -226,6 +232,8 @@ hi
226
232
  end "समाप्ति"
227
233
  five
228
234
  hour
235
+ limit
236
+ session "5 घंटे का सत्र"
229
237
  session "5 घंटे का सत्र"
230
238
  min
231
239
  load
@@ -314,6 +322,14 @@ hi
314
322
  session "सत्र"
315
323
  start "शुरुआत"
316
324
  subscription
325
+ detail
326
+ ends
327
+ label "{{time}} को समाप्त होगी"
328
+ in "{{duration}} में समाप्त होगी; {{time}}"
329
+ trial
330
+ ends
331
+ label "ट्रायल {{time}} को समाप्त होगा"
332
+ in "ट्रायल {{duration}} में समाप्त होगा; {{time}}"
317
333
  ends
318
334
  label "सदस्यता समाप्त होगी {{time}}"
319
335
  in "सदस्यता {{duration}} में समाप्त होगी ({{time}})"