acdev 1.0.9 → 1.0.10

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "acdev",
3
- "version": "1.0.9",
3
+ "version": "1.0.10",
4
4
  "description": "Local CLI + web UI for running AI agents on GitHub issues via git worktrees",
5
5
  "type": "module",
6
6
  "bin": {
package/public/app.js CHANGED
@@ -228,6 +228,7 @@ const els = {
228
228
  enqueueJiraSettingsLink: document.getElementById('enqueue-jira-settings-link'),
229
229
  overviewTicketSource: document.getElementById('overview-ticket-source'),
230
230
  enqueueFeedback: document.getElementById('enqueue-feedback'),
231
+ preferredBranch: document.getElementById('preferred-branch'),
231
232
  addBtn: document.getElementById('add-btn'),
232
233
  statsGrid: document.getElementById('stats-grid'),
233
234
  overviewRunning: document.getElementById('overview-running'),
@@ -900,16 +901,22 @@ function jobTitle(job) {
900
901
  return job.issueTitle || truncate(job.issueUrl, 48);
901
902
  }
902
903
 
904
+ function jobDisplayBranch(job) {
905
+ return job.branchName || job.preferredBranchName || '';
906
+ }
907
+
903
908
  function jobRepoLine(job) {
904
909
  if (isJiraJob(job)) {
905
910
  const key = parseJiraKeyFromJob(job) || '—';
906
- const branch = job.branchName ? ` · ${job.branchName}` : '';
911
+ const name = jobDisplayBranch(job);
912
+ const branch = name ? ` · ${name}` : '';
907
913
  return { repo: 'Jira', number: key, branch, text: `Jira ${key}${branch}`, isJira: true };
908
914
  }
909
915
  const ref = parseIssueRef(job.issueUrl);
910
916
  const repo = ref?.full || '—';
911
917
  const num = ref?.number || job.issueNumber || '—';
912
- const branch = job.branchName ? ` · ${job.branchName}` : '';
918
+ const name = jobDisplayBranch(job);
919
+ const branch = name ? ` · ${name}` : '';
913
920
  return { repo, number: num, branch, text: `${repo} #${num}${branch}`, isJira: false };
914
921
  }
915
922
 
@@ -2696,6 +2703,7 @@ function jobMatchesReviewSearch(job, q) {
2696
2703
  job.issueTitle,
2697
2704
  job.issueUrl,
2698
2705
  job.branchName,
2706
+ job.preferredBranchName,
2699
2707
  job.prTitle,
2700
2708
  job.prUrl,
2701
2709
  ref?.number,
@@ -4445,18 +4453,32 @@ els.issueUrls?.addEventListener('keydown', (ev) => {
4445
4453
  els.addBtn?.click();
4446
4454
  });
4447
4455
 
4456
+ els.preferredBranch?.addEventListener('keydown', (ev) => {
4457
+ if (ev.key !== 'Enter' || ev.shiftKey || ev.metaKey || ev.ctrlKey || ev.altKey) return;
4458
+ ev.preventDefault();
4459
+ els.addBtn?.click();
4460
+ });
4461
+
4448
4462
  els.addBtn.addEventListener('click', async () => {
4449
4463
  els.enqueueFeedback.classList.add('hidden');
4450
4464
  const text = els.issueUrls.value.trim();
4451
4465
  if (!text) return;
4452
4466
 
4453
4467
  const urls = splitIssueUrls(text);
4468
+ const branchName = els.preferredBranch?.value?.trim() || '';
4469
+ // #region agent log
4470
+ fetch('http://127.0.0.1:7258/ingest/377aa5e2-15ea-4447-a68b-7ce215882bc3',{method:'POST',headers:{'Content-Type':'application/json','X-Debug-Session-Id':'473a78'},body:JSON.stringify({sessionId:'473a78',runId:'pre-fix',hypothesisId:'B',location:'public/app.js:addBtn',message:'enqueue submit',data:{hasCustom:Boolean(branchName),branchName:branchName||null,urlCount:urls.length},timestamp:Date.now()})}).catch(()=>{});
4471
+ // #endregion
4454
4472
 
4455
4473
  try {
4456
4474
  const res = await fetch('/api/issues', {
4457
4475
  method: 'POST',
4458
4476
  headers: { 'Content-Type': 'application/json' },
4459
- body: JSON.stringify({ urls, ticketSource }),
4477
+ body: JSON.stringify({
4478
+ urls,
4479
+ ticketSource,
4480
+ ...(branchName ? { branchName } : {}),
4481
+ }),
4460
4482
  });
4461
4483
  const data = await res.json();
4462
4484
  if (!res.ok) {
@@ -4467,6 +4489,7 @@ els.addBtn.addEventListener('click', async () => {
4467
4489
  return;
4468
4490
  }
4469
4491
  els.issueUrls.value = '';
4492
+ if (els.preferredBranch) els.preferredBranch.value = '';
4470
4493
  const parts = [];
4471
4494
  if (data.jobs?.length) parts.push(`Added ${data.jobs.length}`);
4472
4495
  if (data.skipped?.length) {
package/public/index.html CHANGED
@@ -138,6 +138,24 @@
138
138
  or a browse link. Press <kbd class="enqueue-kbd">Enter</kbd> to add.
139
139
  </p>
140
140
 
141
+ <div class="enqueue-branch-field">
142
+ <div class="enqueue-branch-label-row">
143
+ <label class="field-label" for="preferred-branch">Branch name</label>
144
+ <span class="enqueue-branch-optional">Optional</span>
145
+ </div>
146
+ <input
147
+ id="preferred-branch"
148
+ class="input enqueue-branch-input"
149
+ type="text"
150
+ spellcheck="false"
151
+ autocomplete="off"
152
+ placeholder="your-branch-name"
153
+ >
154
+ <p class="enqueue-input-hint">
155
+ Leave empty to keep feat/fix + ticket title. If set, that name is used instead.
156
+ </p>
157
+ </div>
158
+
141
159
  <div class="enqueue-info-banner" id="enqueue-info-banner" role="note">
142
160
  <svg class="enqueue-info-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
143
161
  <circle cx="12" cy="12" r="10"/>
package/public/styles.css CHANGED
@@ -459,6 +459,38 @@ a { color: var(--primary); text-underline-offset: 3px; }
459
459
  color: var(--text-muted);
460
460
  }
461
461
 
462
+ .enqueue-branch-field {
463
+ display: flex;
464
+ flex-direction: column;
465
+ gap: 6px;
466
+ }
467
+
468
+ .enqueue-branch-field .field-label {
469
+ margin-bottom: 0;
470
+ }
471
+
472
+ .enqueue-branch-field .enqueue-input-hint {
473
+ margin: 0;
474
+ }
475
+
476
+ .enqueue-branch-label-row {
477
+ display: flex;
478
+ align-items: baseline;
479
+ justify-content: space-between;
480
+ gap: 8px;
481
+ }
482
+
483
+ .enqueue-branch-optional {
484
+ font-size: 11px;
485
+ font-weight: 500;
486
+ color: var(--text-muted);
487
+ }
488
+
489
+ .enqueue-branch-input {
490
+ font-family: 'JetBrains Mono', ui-monospace, monospace;
491
+ font-size: 13px;
492
+ }
493
+
462
494
  .enqueue-kbd {
463
495
  display: inline-block;
464
496
  padding: 1px 6px;
package/src/git.js CHANGED
@@ -103,6 +103,68 @@ export function buildBranchName(type, title) {
103
103
  return `${type}/${slugifyTitle(title)}`;
104
104
  }
105
105
 
106
+ const PREFERRED_BRANCH_MAX = 100;
107
+
108
+ /**
109
+ * Sanitize a user-supplied git branch name. Preserves case (e.g. NC-2133).
110
+ * Empty / whitespace-only input returns ''.
111
+ * @param {unknown} raw
112
+ */
113
+ export function normalizePreferredBranchName(raw) {
114
+ if (raw == null) return '';
115
+ let name = String(raw).trim().replace(/\s+/g, '-');
116
+ if (!name) return '';
117
+
118
+ name = name
119
+ .replace(/[~^:?*\[\\]+/g, '-')
120
+ .replace(/@{/g, '-at-')
121
+ .replace(/[^A-Za-z0-9._/-]+/g, '-')
122
+ .replace(/\/+/g, '/')
123
+ .replace(/\.{2,}/g, '.')
124
+ .replace(/-{2,}/g, '-')
125
+ .replace(/^[-./]+|[-./]+$/g, '');
126
+
127
+ if (/\.lock$/i.test(name)) {
128
+ name = name.replace(/\.lock$/i, '').replace(/[-./]+$/g, '');
129
+ }
130
+
131
+ name = name.slice(0, PREFERRED_BRANCH_MAX).replace(/[-./]+$/g, '');
132
+ if (!name || name === '@' || /^HEAD$/i.test(name)) return '';
133
+ return name;
134
+ }
135
+
136
+ /**
137
+ * Parse optional branchName from an API body.
138
+ * Omitted / blank → no preference (caller uses feat/fix slug).
139
+ * Provided but invalid after sanitize → error.
140
+ * @param {unknown} raw
141
+ * @returns {{ ok: true, value?: string } | { ok: false, error: string }}
142
+ */
143
+ export function parsePreferredBranchName(raw) {
144
+ if (raw == null) return { ok: true };
145
+ if (typeof raw !== 'string') {
146
+ return { ok: false, error: 'branchName must be a string' };
147
+ }
148
+ if (!raw.trim()) return { ok: true };
149
+ const value = normalizePreferredBranchName(raw);
150
+ if (!value) {
151
+ return { ok: false, error: 'branchName is not a valid git branch name' };
152
+ }
153
+ return { ok: true, value };
154
+ }
155
+
156
+ /**
157
+ * Prefer a user-supplied branch name; otherwise feat/fix slug from the ticket.
158
+ * @param {string | undefined | null} preferred
159
+ * @param {'feat' | 'fix'} issueType
160
+ * @param {string} issueTitle
161
+ */
162
+ export function resolveDesiredBranchName(preferred, issueType, issueTitle) {
163
+ const custom = normalizePreferredBranchName(preferred);
164
+ if (custom) return custom;
165
+ return buildBranchName(issueType, issueTitle);
166
+ }
167
+
106
168
  /**
107
169
  * @param {string} repoRoot
108
170
  * @param {string | number} worktreeId GitHub issue number or Jira key
package/src/server.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import express from 'express';
2
+ import fs from 'node:fs';
2
3
  import path from 'node:path';
3
4
  import { fileURLToPath } from 'node:url';
4
5
  import { Store } from './store.js';
@@ -18,7 +19,8 @@ import {
18
19
  getDiff,
19
20
  pushBranch,
20
21
  detectIssueType,
21
- buildBranchName,
22
+ parsePreferredBranchName,
23
+ resolveDesiredBranchName,
22
24
  sanitizeBranchCommits,
23
25
  listChangedFiles,
24
26
  applyFileExclusions,
@@ -341,7 +343,34 @@ export function createServer({ repoRoot, config, store, useStubAgent = false, de
341
343
  issueType = detectIssueType(issueDetails);
342
344
  }
343
345
 
344
- const desiredBranchName = buildBranchName(issueType, issueTitle);
346
+ const desiredBranchName = resolveDesiredBranchName(
347
+ job.preferredBranchName,
348
+ issueType,
349
+ issueTitle
350
+ );
351
+ // #region agent log
352
+ try {
353
+ fs.appendFileSync(
354
+ '/Users/giancarlogarcia/Documents/Personal/Projects-2026/agent-mcp/.cursor/debug-473a78.log',
355
+ `${JSON.stringify({
356
+ sessionId: '473a78',
357
+ runId: 'pre-fix',
358
+ hypothesisId: 'C',
359
+ location: 'src/server.js:resolveDesiredBranchName',
360
+ message: 'resolved worktree branch name',
361
+ data: {
362
+ preferred: job.preferredBranchName ?? null,
363
+ issueType,
364
+ usedCustom: Boolean(job.preferredBranchName),
365
+ desiredBranchName,
366
+ },
367
+ timestamp: Date.now(),
368
+ })}\n`
369
+ );
370
+ } catch {
371
+ // ignore debug log failures
372
+ }
373
+ // #endregion
345
374
  const worktreeId = worktreeIdForJob(job);
346
375
  if (worktreeId == null) {
347
376
  throw new Error('Job is missing issueNumber / jiraKey for worktree path');
@@ -657,6 +686,35 @@ export function createServer({ repoRoot, config, store, useStubAgent = false, de
657
686
  });
658
687
  }
659
688
 
689
+ const preferredParsed = parsePreferredBranchName(req.body?.branchName);
690
+ if (!preferredParsed.ok) {
691
+ return res.status(400).json({ error: preferredParsed.error });
692
+ }
693
+ const preferredBranchName = preferredParsed.value;
694
+ // #region agent log
695
+ try {
696
+ fs.appendFileSync(
697
+ '/Users/giancarlogarcia/Documents/Personal/Projects-2026/agent-mcp/.cursor/debug-473a78.log',
698
+ `${JSON.stringify({
699
+ sessionId: '473a78',
700
+ runId: 'pre-fix',
701
+ hypothesisId: 'A',
702
+ location: 'src/server.js:POST /api/issues',
703
+ message: 'enqueue preferred branch parse',
704
+ data: {
705
+ rawProvided: req.body?.branchName != null,
706
+ rawType: typeof req.body?.branchName,
707
+ preferredBranchName: preferredBranchName ?? null,
708
+ urlCount: urls.length,
709
+ },
710
+ timestamp: Date.now(),
711
+ })}\n`
712
+ );
713
+ } catch {
714
+ // ignore debug log failures
715
+ }
716
+ // #endregion
717
+
660
718
  const ticketSource =
661
719
  req.body?.ticketSource === 'jira' || req.body?.ticketSource === 'github'
662
720
  ? req.body.ticketSource
@@ -751,6 +809,7 @@ export function createServer({ repoRoot, config, store, useStubAgent = false, de
751
809
  issueNumber: item.number,
752
810
  ticketSource: item.ticketSource,
753
811
  jiraKey: item.jiraKey,
812
+ ...(preferredBranchName ? { preferredBranchName } : {}),
754
813
  });
755
814
  created.push(job);
756
815
  }
package/src/store.js CHANGED
@@ -55,6 +55,7 @@ export class Store {
55
55
  * issueNumber?: number,
56
56
  * ticketSource?: 'github' | 'jira',
57
57
  * jiraKey?: string,
58
+ * preferredBranchName?: string,
58
59
  * }} data
59
60
  * @returns {Job}
60
61
  */
@@ -68,6 +69,7 @@ export class Store {
68
69
  issueNumber: data.issueNumber,
69
70
  ticketSource,
70
71
  ...(data.jiraKey ? { jiraKey: data.jiraKey } : {}),
72
+ ...(data.preferredBranchName ? { preferredBranchName: data.preferredBranchName } : {}),
71
73
  status: 'queued',
72
74
  createdAt: now,
73
75
  updatedAt: now,