Skip to content

Commit a2f7cc5

Browse files
committed
improved prompt escaping and branch name generation
1 parent 8cffa99 commit a2f7cc5

File tree

4 files changed

+51
-26
lines changed

4 files changed

+51
-26
lines changed

bun.lock

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
"@opentui/react": "^0.1.74",
2424
"build-strap": "^5.2.0",
2525
"commander": "^14.0.2",
26+
"nanoid": "^5.1.6",
2627
"react": "^19.2.3"
2728
}
2829
}

src/services/docker.ts

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -431,14 +431,15 @@ cp /tmp/opencode-cfg/auth.json ~/.local/share/opencode/auth.json
431431
set -e
432432
${opencodeAuthSetup}
433433
cd /work
434-
gh auth setup-git
435-
gh repo clone ${repoInfo.fullName} app
436-
cd app
437-
git switch -c "hermes/${branchName}"
438-
exec ${agentCommand} \\
439-
"${prompt.replace(/"/g, '\\"')}
440-
441-
Use the \\\`gh\\\` command to create a PR when done."
434+
gh auth setup-git
435+
gh repo clone ${repoInfo.fullName} app
436+
cd app
437+
git switch -c "hermes/${branchName}"
438+
exec ${agentCommand} <<'HERMES_PROMPT_HEREDOC'
439+
${prompt}
440+
441+
Use the \`gh\` command to create a PR when done.
442+
HERMES_PROMPT_HEREDOC
442443
`.trim();
443444

444445
// Build label arguments for hermes metadata

src/services/git.ts

Lines changed: 38 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Git & Branch Name Services
33
// ============================================================================
44

5+
import { nanoid } from 'nanoid';
56
import { formatShellError, type ShellError } from '../utils';
67

78
export interface RepoInfo {
@@ -41,14 +42,22 @@ export async function getRepoInfo(): Promise<RepoInfo> {
4142
};
4243
}
4344

44-
function isValidBranchName(name: string): boolean {
45+
function isValidBranchName(name: string): [boolean, string] {
4546
// Must start with letter, contain only lowercase letters, numbers, hyphens
4647
// Must end with letter or number, max 50 chars
47-
if (name.length === 0 || name.length > 50) return false;
48-
if (!/^[a-z][a-z0-9-]*[a-z0-9]$/.test(name) && !/^[a-z]$/.test(name))
49-
return false;
50-
if (name.includes('--')) return false; // No double hyphens
51-
return true;
48+
if (!name || name.length < 5) {
49+
return [false, 'too short'];
50+
}
51+
if (name.length > 50) {
52+
return [false, 'too long'];
53+
}
54+
if (!/^[a-z][a-z0-9-]*[a-z0-9]$/.test(name) && !/^[a-z]$/.test(name)) {
55+
return [false, 'invalid characters'];
56+
}
57+
if (name.includes('--')) {
58+
return [false, 'double hyphens not allowed'];
59+
}
60+
return [true, ''];
5261
}
5362

5463
async function getExistingBranches(): Promise<string[]> {
@@ -112,14 +121,21 @@ export async function generateBranchName(
112121
let lastAttempt = '';
113122

114123
for (let attempt = 1; attempt <= maxRetries; attempt++) {
115-
let claudePrompt = `Generate a git branch name for the following task: ${prompt}
124+
let claudePrompt = `Generate a git branch name for the following task:
125+
126+
<task>
127+
\`\`\`markdown
128+
${prompt.replace(/```/g, '\\`\\`\\`')}
129+
\`\`\`
130+
</task>
116131
117132
Requirements:
118-
- Output ONLY the branch name, nothing else
119133
- Lowercase letters, numbers, and hyphens only
120134
- No special characters, spaces, or underscores
121135
- Keep it concise (2-4 words max)
122-
- Example format: add-user-auth, fix-login-bug`;
136+
- Example format: add-user-auth, fix-login-bug
137+
138+
CRITICAL: Output ONLY the branch name, nothing else`;
123139

124140
if (allExistingNames.size > 0) {
125141
claudePrompt += `\n\nIMPORTANT: Do NOT use any of these names (they already exist):
@@ -140,13 +156,12 @@ ${[...allExistingNames].join(', ')}`;
140156
const branchName = result.trim().toLowerCase();
141157

142158
// Clean up any quotes or extra whitespace
143-
const cleaned = branchName.replace(/['"]/g, '').trim();
159+
const cleaned = branchName.replace(/['"\n ]/g, '').trim();
144160

145-
if (!isValidBranchName(cleaned)) {
146-
console.log(
147-
` Attempt ${attempt}: '${cleaned}' is not a valid branch name`,
148-
);
149-
lastAttempt = cleaned;
161+
const [isValid, reason] = isValidBranchName(cleaned);
162+
if (!isValid) {
163+
console.log(` Attempt ${attempt} is invalid (${reason})`);
164+
lastAttempt = cleaned.slice(0, 100);
150165
continue;
151166
}
152167

@@ -160,7 +175,12 @@ ${[...allExistingNames].join(', ')}`;
160175
return cleaned;
161176
}
162177

163-
throw new Error(
164-
`Failed to generate valid branch name after ${maxRetries} attempts`,
165-
);
178+
console.log(' Failed to generate a valid branch name, using a random name.');
179+
// Fallback: use a generic name with random suffix
180+
let fallbackName: string;
181+
do {
182+
const randomSuffix = nanoid(12).toLowerCase();
183+
fallbackName = `hermes-branch-${randomSuffix}`;
184+
} while (allExistingNames.has(fallbackName));
185+
return fallbackName;
166186
}

0 commit comments

Comments
 (0)