@link-assistant/hive-mind 1.35.11 → 1.36.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +25 -0
- package/package.json +1 -1
- package/src/github-error-reporter.lib.mjs +47 -3
- package/src/hive.mjs +10 -2
- package/src/solve.branch.lib.mjs +94 -0
- package/src/solve.config.lib.mjs +19 -0
- package/src/solve.error-handlers.lib.mjs +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
# @link-assistant/hive-mind
|
|
2
2
|
|
|
3
|
+
## 1.36.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 3adbf2b: feat: add --auto-report-issue and --disable-report-issue flags for non-interactive error reporting (Issue #1484)
|
|
8
|
+
- Add `--auto-report-issue` flag that automatically creates a GitHub issue on failure without prompting.
|
|
9
|
+
The auto-reported issue includes error details, logs, and case study analysis instructions in the body.
|
|
10
|
+
Issue is labeled as `bug`.
|
|
11
|
+
- Add `--disable-report-issue` flag that completely disables error issue creation (no prompt, no auto-creation).
|
|
12
|
+
Takes precedence over `--auto-report-issue` if both are specified.
|
|
13
|
+
- Default behavior (neither flag) preserves the existing interactive y/n prompt.
|
|
14
|
+
- Both flags are automatically available as passthrough options in hive and TELEGRAM_HIVE_OVERRIDES.
|
|
15
|
+
|
|
16
|
+
## 1.35.12
|
|
17
|
+
|
|
18
|
+
### Patch Changes
|
|
19
|
+
|
|
20
|
+
- 05a72c3: fix: reject URLs and invalid git branch names used as --base-branch (Issue #1482)
|
|
21
|
+
- Add `validateBranchName()` function to `solve.branch.lib.mjs` that validates branch names against git-check-ref-format rules
|
|
22
|
+
- Reject URLs (https://, http://, git@, ssh://) passed as --base-branch with clear error message
|
|
23
|
+
- Reject invalid git ref characters (spaces, ~, ^, :, ?, \*, [, ], \, control chars, .., @{)
|
|
24
|
+
- Add validation in `solve.config.lib.mjs` parseArguments (early catch), `solve.branch.lib.mjs` createOrCheckoutBranch (defense-in-depth), and `hive.mjs` (before forwarding to solve)
|
|
25
|
+
- Add 19 test cases in `tests/test-base-branch-validation.mjs`
|
|
26
|
+
- Add case study documentation in `docs/case-studies/issue-1482/`
|
|
27
|
+
|
|
3
28
|
## 1.35.11
|
|
4
29
|
|
|
5
30
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -149,7 +149,7 @@ export const formatLogForIssue = async (logContent, logFilePath) => {
|
|
|
149
149
|
* @returns {Promise<string|null>} Issue URL or null on failure
|
|
150
150
|
*/
|
|
151
151
|
export const createIssueForError = async options => {
|
|
152
|
-
const { error, errorType, logFile, context = {} } = options;
|
|
152
|
+
const { error, errorType, logFile, context = {}, autoReport = false } = options;
|
|
153
153
|
|
|
154
154
|
try {
|
|
155
155
|
const currentUser = await getCurrentGitHubUser();
|
|
@@ -159,7 +159,20 @@ export const createIssueForError = async options => {
|
|
|
159
159
|
}
|
|
160
160
|
|
|
161
161
|
const errorMessage = cleanErrorMessage(error);
|
|
162
|
-
|
|
162
|
+
|
|
163
|
+
let shouldCreateIssue;
|
|
164
|
+
if (autoReport) {
|
|
165
|
+
// Auto-report mode: skip prompt, automatically create issue
|
|
166
|
+
console.log('\n❌ An error occurred:');
|
|
167
|
+
console.log(` ${errorMessage}`);
|
|
168
|
+
if (isSentryEnabled()) {
|
|
169
|
+
console.log('\n✅ Error reported to Sentry successfully');
|
|
170
|
+
}
|
|
171
|
+
console.log('\nℹ️ --auto-report-issue enabled: automatically creating GitHub issue...');
|
|
172
|
+
shouldCreateIssue = true;
|
|
173
|
+
} else {
|
|
174
|
+
shouldCreateIssue = await promptUserForIssueCreation(errorMessage);
|
|
175
|
+
}
|
|
163
176
|
|
|
164
177
|
if (!shouldCreateIssue) {
|
|
165
178
|
await log('ℹ️ Issue creation cancelled by user');
|
|
@@ -199,6 +212,20 @@ export const createIssueForError = async options => {
|
|
|
199
212
|
}
|
|
200
213
|
}
|
|
201
214
|
|
|
215
|
+
if (autoReport) {
|
|
216
|
+
issueBody += '### Instructions\n\n';
|
|
217
|
+
issueBody += 'We need to download all logs and data related about the issue to this repository, ';
|
|
218
|
+
issueBody += 'make sure we compile that data to `./docs/case-studies/issue-{id}` folder, ';
|
|
219
|
+
issueBody += 'and use it to do deep case study analysis (also make sure to search online for additional facts and data), ';
|
|
220
|
+
issueBody += 'in which we will reconstruct timeline/sequence of events, find root causes of the problem, ';
|
|
221
|
+
issueBody += 'and propose possible solutions (including known existing components/libraries, ';
|
|
222
|
+
issueBody += 'that solve similar problem or can help in solutions).\n\n';
|
|
223
|
+
issueBody += 'If there is not enough data to find actual root cause, add debug output and verbose mode if not present, ';
|
|
224
|
+
issueBody += 'that will allow us to find root cause on next iteration.\n\n';
|
|
225
|
+
issueBody += 'If issue related to any other repository/project, where we can report issues on GitHub, please do so. ';
|
|
226
|
+
issueBody += 'Each issue must contain reproducible examples, workarounds and suggestions for fix the issue in code.\n\n';
|
|
227
|
+
}
|
|
228
|
+
|
|
202
229
|
issueBody += '---\n';
|
|
203
230
|
issueBody += `*This issue was automatically created by @${currentUser} using hive-mind error reporting*\n`;
|
|
204
231
|
|
|
@@ -239,7 +266,24 @@ export const createIssueForError = async options => {
|
|
|
239
266
|
* @returns {Promise<string|null>} Issue URL if created, null otherwise
|
|
240
267
|
*/
|
|
241
268
|
export const handleErrorWithIssueCreation = async options => {
|
|
242
|
-
const { error, errorType, logFile, context = {}, skipPrompt = false } = options;
|
|
269
|
+
const { error, errorType, logFile, context = {}, skipPrompt = false, autoReport = false, disableReport = false } = options;
|
|
270
|
+
|
|
271
|
+
// --disable-report-issue takes highest precedence
|
|
272
|
+
if (disableReport) {
|
|
273
|
+
await log('ℹ️ Issue reporting disabled via --disable-report-issue.');
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// --auto-report-issue: create issue automatically without prompting
|
|
278
|
+
if (autoReport) {
|
|
279
|
+
return await createIssueForError({
|
|
280
|
+
error,
|
|
281
|
+
errorType,
|
|
282
|
+
logFile: logFile || (await getAbsoluteLogPath()),
|
|
283
|
+
context,
|
|
284
|
+
autoReport: true,
|
|
285
|
+
});
|
|
286
|
+
}
|
|
243
287
|
|
|
244
288
|
if (skipPrompt) {
|
|
245
289
|
return null;
|
package/src/hive.mjs
CHANGED
|
@@ -766,8 +766,16 @@ if (isDirectExecution) {
|
|
|
766
766
|
const kebabToCamel = str => str.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
767
767
|
const args = [issueUrl, '--model', argv.model];
|
|
768
768
|
// Special handling for options with different semantics in hive vs solve
|
|
769
|
-
|
|
770
|
-
|
|
769
|
+
// Validate branch name before forwarding (issue #1482: reject URLs used as branch names)
|
|
770
|
+
const branchValue = argv.baseBranch || argv.targetBranch;
|
|
771
|
+
if (branchValue) {
|
|
772
|
+
const { validateBranchName } = await import('./solve.branch.lib.mjs');
|
|
773
|
+
const branchValidation = validateBranchName(branchValue);
|
|
774
|
+
if (!branchValidation.valid) {
|
|
775
|
+
throw new Error(`Invalid branch name for --base-branch/--target-branch: ${branchValidation.reason}`);
|
|
776
|
+
}
|
|
777
|
+
args.push('--base-branch', branchValue);
|
|
778
|
+
}
|
|
771
779
|
if (argv.skipToolConnectionCheck || argv.toolConnectionCheck === false) args.push('--skip-tool-connection-check');
|
|
772
780
|
if (argv.dryRun) args.push('--dry-run');
|
|
773
781
|
if (argv.autoCleanup) args.push('--auto-cleanup'); // hive default differs from solve's auto-detect default
|
package/src/solve.branch.lib.mjs
CHANGED
|
@@ -100,6 +100,93 @@ export function detectBranchFormat(branchName) {
|
|
|
100
100
|
return null;
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
+
/**
|
|
104
|
+
* Validates a branch name for use as --base-branch.
|
|
105
|
+
* Rejects URLs, invalid git ref characters, and enforces safe naming conventions.
|
|
106
|
+
* Based on git-check-ref-format rules: https://git-scm.com/docs/git-check-ref-format
|
|
107
|
+
*
|
|
108
|
+
* @param {string} branchName - The branch name to validate
|
|
109
|
+
* @returns {{ valid: boolean, reason?: string }} Validation result
|
|
110
|
+
*/
|
|
111
|
+
export function validateBranchName(branchName) {
|
|
112
|
+
if (!branchName || typeof branchName !== 'string') {
|
|
113
|
+
return { valid: false, reason: 'Branch name must be a non-empty string' };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const trimmed = branchName.trim();
|
|
117
|
+
if (trimmed !== branchName) {
|
|
118
|
+
return { valid: false, reason: 'Branch name must not have leading or trailing whitespace' };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Reject URLs (the primary use case from issue #1482)
|
|
122
|
+
if (/^https?:\/\//i.test(branchName) || /^git@/i.test(branchName) || /^ssh:\/\//i.test(branchName)) {
|
|
123
|
+
return { valid: false, reason: `"${branchName}" looks like a URL, not a branch name. Use just the branch name (e.g. "main", "develop")` };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Reject if it contains :// anywhere (catches other protocol-like URLs)
|
|
127
|
+
if (branchName.includes('://')) {
|
|
128
|
+
return { valid: false, reason: `"${branchName}" contains "://" which is not valid in a branch name` };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Git ref format rules:
|
|
132
|
+
// Cannot contain ASCII control characters (bytes < 0x20) or DEL (0x7F)
|
|
133
|
+
// eslint-disable-next-line no-control-regex
|
|
134
|
+
if (/[\x00-\x1f\x7f]/.test(branchName)) {
|
|
135
|
+
return { valid: false, reason: 'Branch name must not contain control characters' };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Cannot contain space, ~, ^, :, ?, *, [, or backslash
|
|
139
|
+
if (/[ ~^:?*[\]\\]/.test(branchName)) {
|
|
140
|
+
return { valid: false, reason: 'Branch name contains invalid characters (spaces, ~, ^, :, ?, *, [, ] or \\ are not allowed)' };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Cannot contain ..
|
|
144
|
+
if (branchName.includes('..')) {
|
|
145
|
+
return { valid: false, reason: 'Branch name must not contain ".."' };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Cannot start with . or -
|
|
149
|
+
if (branchName.startsWith('.') || branchName.startsWith('-')) {
|
|
150
|
+
return { valid: false, reason: 'Branch name must not start with "." or "-"' };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Cannot end with . or .lock
|
|
154
|
+
if (branchName.endsWith('.') || branchName.endsWith('.lock')) {
|
|
155
|
+
return { valid: false, reason: 'Branch name must not end with "." or ".lock"' };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Cannot contain @{
|
|
159
|
+
if (branchName.includes('@{')) {
|
|
160
|
+
return { valid: false, reason: 'Branch name must not contain "@{"' };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Cannot be exactly @
|
|
164
|
+
if (branchName === '@') {
|
|
165
|
+
return { valid: false, reason: 'Branch name must not be "@"' };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Component-level checks: no component can start with . or end with .lock
|
|
169
|
+
const components = branchName.split('/');
|
|
170
|
+
for (const component of components) {
|
|
171
|
+
if (component === '') {
|
|
172
|
+
return { valid: false, reason: 'Branch name must not contain consecutive slashes or start/end with "/"' };
|
|
173
|
+
}
|
|
174
|
+
if (component.startsWith('.')) {
|
|
175
|
+
return { valid: false, reason: `Branch name component "${component}" must not start with "."` };
|
|
176
|
+
}
|
|
177
|
+
if (component.endsWith('.lock')) {
|
|
178
|
+
return { valid: false, reason: `Branch name component "${component}" must not end with ".lock"` };
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Reasonable length limit
|
|
183
|
+
if (branchName.length > 255) {
|
|
184
|
+
return { valid: false, reason: 'Branch name must not exceed 255 characters' };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return { valid: true };
|
|
188
|
+
}
|
|
189
|
+
|
|
103
190
|
export async function createOrCheckoutBranch({ isContinueMode, prBranch, issueNumber, tempDir, defaultBranch, argv, log, formatAligned, $, crypto, owner, repo, prNumber }) {
|
|
104
191
|
// Create a branch for the issue or checkout existing PR branch
|
|
105
192
|
let branchName;
|
|
@@ -120,6 +207,13 @@ export async function createOrCheckoutBranch({ isContinueMode, prBranch, issueNu
|
|
|
120
207
|
// Use user-specified base branch if provided, otherwise use repository default
|
|
121
208
|
const baseBranch = argv.baseBranch || defaultBranch;
|
|
122
209
|
const branchSource = argv.baseBranch ? 'custom' : 'default';
|
|
210
|
+
|
|
211
|
+
// Defense-in-depth: validate base branch name even if already validated at CLI parsing (issue #1482)
|
|
212
|
+
const baseBranchValidation = validateBranchName(baseBranch);
|
|
213
|
+
if (!baseBranchValidation.valid) {
|
|
214
|
+
throw new Error(`Invalid base branch "${baseBranch}": ${baseBranchValidation.reason}`);
|
|
215
|
+
}
|
|
216
|
+
|
|
123
217
|
await log(`\n${formatAligned('🌿', 'Creating branch:', `${branchName} from ${baseBranch} (${branchSource})`)}`);
|
|
124
218
|
|
|
125
219
|
// IMPORTANT: Don't use 2>&1 here as it can interfere with exit codes
|
package/src/solve.config.lib.mjs
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import { enhanceErrorMessage, detectMalformedFlags } from './option-suggestions.lib.mjs';
|
|
11
11
|
import { defaultModels, buildModelOptionDescription } from './models/index.mjs';
|
|
12
|
+
import { validateBranchName } from './solve.branch.lib.mjs';
|
|
12
13
|
|
|
13
14
|
// Re-export for use by telegram-bot.mjs (avoids extra import lines there)
|
|
14
15
|
export { detectMalformedFlags };
|
|
@@ -368,6 +369,16 @@ export const SOLVE_OPTION_DEFINITIONS = {
|
|
|
368
369
|
description: 'Automatically initialize empty repositories by creating a simple README.md file. Only works when you have write access to the repository. This allows branch creation and pull request workflows to proceed on repositories that have no commits.',
|
|
369
370
|
default: false,
|
|
370
371
|
},
|
|
372
|
+
'auto-report-issue': {
|
|
373
|
+
type: 'boolean',
|
|
374
|
+
description: 'Automatically create a GitHub issue on failure without prompting (non-interactive mode). The issue includes error details, logs, and case study analysis instructions. Sets issue type and label to bug.',
|
|
375
|
+
default: false,
|
|
376
|
+
},
|
|
377
|
+
'disable-report-issue': {
|
|
378
|
+
type: 'boolean',
|
|
379
|
+
description: 'Disable error issue creation entirely (no prompt, no automatic creation). Overrides --auto-report-issue if both are specified.',
|
|
380
|
+
default: false,
|
|
381
|
+
},
|
|
371
382
|
'attach-solution-summary': {
|
|
372
383
|
type: 'boolean',
|
|
373
384
|
description: 'Attach the AI solution summary (from the result field) as a comment to the PR/issue after completion. The summary is extracted from the AI tool JSON output and posted under a "Solution summary" header.',
|
|
@@ -563,6 +574,14 @@ export const parseArguments = async (yargs, hideBin) => {
|
|
|
563
574
|
}
|
|
564
575
|
}
|
|
565
576
|
|
|
577
|
+
// Validate --base-branch value (issue #1482: reject URLs and invalid git branch names)
|
|
578
|
+
if (argv.baseBranch) {
|
|
579
|
+
const branchValidation = validateBranchName(argv.baseBranch);
|
|
580
|
+
if (!branchValidation.valid) {
|
|
581
|
+
throw new Error(`Invalid --base-branch value: ${branchValidation.reason}`);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
566
585
|
if (argv.tool && !modelExplicitlyProvided && defaultModels[argv.tool]) {
|
|
567
586
|
// User did not explicitly provide --model, so use the correct default for the tool
|
|
568
587
|
// (Issue #1473: centralized in models/index.mjs)
|
|
@@ -30,6 +30,8 @@ export const handleFailure = async options => {
|
|
|
30
30
|
errorType,
|
|
31
31
|
},
|
|
32
32
|
skipPrompt: !process.stdin.isTTY || argv.noIssueCreation,
|
|
33
|
+
autoReport: argv.autoReportIssue,
|
|
34
|
+
disableReport: argv.disableReportIssue,
|
|
33
35
|
});
|
|
34
36
|
} catch (issueError) {
|
|
35
37
|
reportError(issueError, {
|