acdev 1.0.14 → 1.0.15
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/README.md +2 -2
- package/package.json +1 -1
- package/public/app.js +130 -20
- package/public/index.html +8 -1
- package/public/styles.css +37 -0
- package/src/github.js +116 -1
- package/src/server.js +50 -6
package/README.md
CHANGED
|
@@ -148,7 +148,7 @@ The tool starts a local server (default port `4848`), opens the browser, and sho
|
|
|
148
148
|
1. Paste GitHub issue URLs or Jira keys/URLs on **Overview** and click **Add to queue**
|
|
149
149
|
2. Jobs process sequentially: sync base branch → create worktree → run agent → await review
|
|
150
150
|
3. Monitor live logs on **Runs**; review diffs on **Review**
|
|
151
|
-
4. **Approve as draft** (draft PR) or **Approve as ready for review** (non-draft PR)
|
|
151
|
+
4. **Approve as draft** (draft PR) or **Approve as ready for review** (non-draft PR); approval now opens PR and removes local worktree. **Reject** still cleanup worktree, keep history as discarded
|
|
152
152
|
5. **Clear** removes a job from history entirely (so you can re-queue the same issue). Allowed for queued / awaiting review / PR opened / failed / discarded; refused while a job is in-flight
|
|
153
153
|
|
|
154
154
|
### Web UI views
|
|
@@ -160,7 +160,7 @@ The tool starts a local server (default port `4848`), opens the browser, and sho
|
|
|
160
160
|
| **Review** | Edit PR title/body, colored diff, approve draft or ready, reject, or Clear |
|
|
161
161
|
| **Logs** | Cross-job activity log with job filter |
|
|
162
162
|
| **Alerts** | Failures / warnings with Retry, Clear, and dismiss |
|
|
163
|
-
| **Settings** | Ticket source (GitHub / Jira), GitHub + Claude authentication, Jira connection,
|
|
163
|
+
| **Settings** | Ticket source (GitHub / Jira), GitHub + Claude authentication, Jira connection, and `.acdev/config.json` fields |
|
|
164
164
|
|
|
165
165
|
Model selection lives in **Settings → Configuration**. The sidebar shows the current model as a read-only label. Default is **Sonnet 5** (`claude-sonnet-5`).
|
|
166
166
|
|
package/package.json
CHANGED
package/public/app.js
CHANGED
|
@@ -125,7 +125,7 @@ let reviewFileLists = {};
|
|
|
125
125
|
/** @type {Record<string, Set<string>>} excluded paths per job while reviewing */
|
|
126
126
|
let reviewExcludedByJob = {};
|
|
127
127
|
let settingsSaving = false;
|
|
128
|
-
/** @type {'ticket' | 'auth' | '
|
|
128
|
+
/** @type {'ticket' | 'auth' | 'config'} */
|
|
129
129
|
let settingsTab = 'ticket';
|
|
130
130
|
/** Sentinel: no model selected (jobs blocked until user picks one). */
|
|
131
131
|
const NO_MODEL = '-';
|
|
@@ -301,12 +301,12 @@ const els = {
|
|
|
301
301
|
settingsModelValue: document.getElementById('settings-model-value'),
|
|
302
302
|
settingsModelPanel: document.getElementById('settings-model-panel'),
|
|
303
303
|
settingsModelList: document.getElementById('settings-model-list'),
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
304
|
+
reviewPrLink: document.getElementById('review-pr-link'),
|
|
305
|
+
reviewPrStatusLine: document.getElementById('review-pr-status-line'),
|
|
306
|
+
reviewPrStatusNotes: document.getElementById('review-pr-status-notes'),
|
|
307
|
+
reviewTerminalSection: document.getElementById('review-terminal-section'),
|
|
308
|
+
reviewTerminalMessage: document.getElementById('review-terminal-message'),
|
|
309
|
+
reviewRetryBtn: document.getElementById('review-retry-btn'),
|
|
310
310
|
settingsTools: document.getElementById('settings-tools'),
|
|
311
311
|
settingsFeedback: document.getElementById('settings-feedback'),
|
|
312
312
|
settingsSaveBtn: document.getElementById('settings-save-btn'),
|
|
@@ -941,14 +941,64 @@ function jobRepoLine(job) {
|
|
|
941
941
|
const branch = name ? ` · ${name}` : '';
|
|
942
942
|
return { repo, number: num, branch, text: `${repo} #${num}${branch}`, isJira: false };
|
|
943
943
|
}
|
|
944
|
+
function prDecisionLabel(decision) {
|
|
945
|
+
switch (String(decision || '').trim().toUpperCase()) {
|
|
946
|
+
case 'APPROVED':
|
|
947
|
+
return 'approved';
|
|
948
|
+
case 'CHANGES_REQUESTED':
|
|
949
|
+
return 'changes requested';
|
|
950
|
+
case 'REVIEW_REQUIRED':
|
|
951
|
+
return 'review required';
|
|
952
|
+
default:
|
|
953
|
+
return '';
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
function jobPrStatusSummary(job) {
|
|
958
|
+
const pr = job?.prStatus;
|
|
959
|
+
if (!pr) return '';
|
|
960
|
+
if (!pr.ok) {
|
|
961
|
+
return `GitHub status unavailable${pr.error ? ` · ${truncate(pr.error, 72)}` : ''}`;
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
const parts = [];
|
|
965
|
+
if (pr.state === 'MERGED') {
|
|
966
|
+
parts.push('Merged');
|
|
967
|
+
} else if (pr.state === 'CLOSED') {
|
|
968
|
+
parts.push('Closed');
|
|
969
|
+
} else if (pr.isDraft) {
|
|
970
|
+
parts.push('Draft');
|
|
971
|
+
} else {
|
|
972
|
+
parts.push('Open');
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
const decision = prDecisionLabel(pr.reviewDecision);
|
|
976
|
+
if (decision) parts.push(decision);
|
|
977
|
+
if (pr.mergeStateStatus) {
|
|
978
|
+
parts.push(`merge ${pr.mergeStateStatus.toLowerCase().replace(/_/g, ' ')}`);
|
|
979
|
+
}
|
|
980
|
+
if (Array.isArray(pr.requestedReviewers) && pr.requestedReviewers.length) {
|
|
981
|
+
parts.push(`requested ${pr.requestedReviewers.join(', ')}`);
|
|
982
|
+
}
|
|
983
|
+
const changeRequest = Array.isArray(pr.latestReviews)
|
|
984
|
+
? pr.latestReviews.find((review) => review.state === 'CHANGES_REQUESTED' && review.body)
|
|
985
|
+
: null;
|
|
986
|
+
if (changeRequest?.body) {
|
|
987
|
+
const who = changeRequest.author ? `${changeRequest.author}: ` : '';
|
|
988
|
+
parts.push(`CR ${who}${truncate(changeRequest.body, 72)}`);
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
return `GitHub · ${parts.join(' · ')}`;
|
|
992
|
+
}
|
|
944
993
|
|
|
945
994
|
function jobSubLine(job) {
|
|
946
995
|
const meta = jobRepoLine(job);
|
|
947
996
|
const agent = jobLlmLine(job);
|
|
997
|
+
const pr = jobPrStatusSummary(job);
|
|
948
998
|
const base = meta.isJira
|
|
949
999
|
? `${meta.number}${meta.branch}`
|
|
950
1000
|
: `${meta.repo} #${meta.number}${meta.branch}`;
|
|
951
|
-
return agent
|
|
1001
|
+
return [base, agent, pr].filter(Boolean).join(' · ');
|
|
952
1002
|
}
|
|
953
1003
|
|
|
954
1004
|
/**
|
|
@@ -1001,6 +1051,62 @@ function sortedJobs() {
|
|
|
1001
1051
|
);
|
|
1002
1052
|
}
|
|
1003
1053
|
|
|
1054
|
+
/**
|
|
1055
|
+
* @param {HTMLElement | null} container
|
|
1056
|
+
* @param {object | null | undefined} pr
|
|
1057
|
+
*/
|
|
1058
|
+
function renderPrStatusNotes(container, pr) {
|
|
1059
|
+
if (!container) return;
|
|
1060
|
+
container.innerHTML = '';
|
|
1061
|
+
if (!pr || !pr.ok) return;
|
|
1062
|
+
|
|
1063
|
+
const reviews = Array.isArray(pr.latestReviews) ? [...pr.latestReviews] : [];
|
|
1064
|
+
reviews.sort((a, b) => {
|
|
1065
|
+
const ta = Date.parse(a.submittedAt || '') || 0;
|
|
1066
|
+
const tb = Date.parse(b.submittedAt || '') || 0;
|
|
1067
|
+
return tb - ta;
|
|
1068
|
+
});
|
|
1069
|
+
|
|
1070
|
+
if (reviews.length > 0) {
|
|
1071
|
+
const heading = document.createElement('div');
|
|
1072
|
+
heading.className = 'pr-status-notes-title';
|
|
1073
|
+
heading.textContent = 'Latest reviews';
|
|
1074
|
+
container.appendChild(heading);
|
|
1075
|
+
|
|
1076
|
+
const list = document.createElement('div');
|
|
1077
|
+
list.className = 'pr-status-note-list';
|
|
1078
|
+
|
|
1079
|
+
for (const review of reviews.slice(0, 4)) {
|
|
1080
|
+
const item = document.createElement('div');
|
|
1081
|
+
item.className = 'pr-status-note';
|
|
1082
|
+
const meta = document.createElement('div');
|
|
1083
|
+
meta.className = 'pr-status-note-meta';
|
|
1084
|
+
const parts = [review.state.replace(/_/g, ' ').toLowerCase()];
|
|
1085
|
+
if (review.author) parts.push(review.author);
|
|
1086
|
+
if (review.submittedAt) parts.push(formatTime(review.submittedAt));
|
|
1087
|
+
meta.textContent = parts.join(' · ');
|
|
1088
|
+
item.appendChild(meta);
|
|
1089
|
+
|
|
1090
|
+
if (review.body) {
|
|
1091
|
+
const body = document.createElement('div');
|
|
1092
|
+
body.className = 'pr-status-note-body';
|
|
1093
|
+
body.textContent = review.body;
|
|
1094
|
+
item.appendChild(body);
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
list.appendChild(item);
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
container.appendChild(list);
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
if (Array.isArray(pr.requestedReviewers) && pr.requestedReviewers.length > 0) {
|
|
1104
|
+
const requested = document.createElement('div');
|
|
1105
|
+
requested.className = 'pr-status-note-meta';
|
|
1106
|
+
requested.textContent = `Requested reviewers: ${pr.requestedReviewers.join(', ')}`;
|
|
1107
|
+
container.appendChild(requested);
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1004
1110
|
function formatTime(ts) {
|
|
1005
1111
|
try {
|
|
1006
1112
|
return new Date(ts).toLocaleTimeString([], {
|
|
@@ -2579,9 +2685,18 @@ function renderReview(jobs) {
|
|
|
2579
2685
|
els.reviewGeneralComment.disabled = !editable;
|
|
2580
2686
|
}
|
|
2581
2687
|
els.reviewPrSection.classList.toggle('hidden', !isOpened);
|
|
2688
|
+
if (isOpened) {
|
|
2689
|
+
if (els.reviewPrStatusLine) {
|
|
2690
|
+
els.reviewPrStatusLine.textContent = job.prStatus
|
|
2691
|
+
? jobPrStatusSummary(job)
|
|
2692
|
+
: 'GitHub status loading…';
|
|
2693
|
+
}
|
|
2694
|
+
renderPrStatusNotes(els.reviewPrStatusNotes, job.prStatus);
|
|
2695
|
+
} else {
|
|
2696
|
+
if (els.reviewPrStatusLine) els.reviewPrStatusLine.textContent = '';
|
|
2697
|
+
if (els.reviewPrStatusNotes) els.reviewPrStatusNotes.innerHTML = '';
|
|
2698
|
+
}
|
|
2582
2699
|
els.reviewTerminalSection.classList.toggle('hidden', !isTerminal);
|
|
2583
|
-
|
|
2584
|
-
const prMeta = resolvePrMeta(job);
|
|
2585
2700
|
if (document.activeElement !== els.prTitle && document.activeElement !== els.prBody) {
|
|
2586
2701
|
els.prTitle.value = prMeta.prTitle;
|
|
2587
2702
|
els.prBody.value = prMeta.prBody;
|
|
@@ -3799,12 +3914,11 @@ function knownToolsList(cfg = appConfig) {
|
|
|
3799
3914
|
}
|
|
3800
3915
|
|
|
3801
3916
|
/**
|
|
3802
|
-
* Switch Settings top-level tab (Ticket source / Authentication /
|
|
3803
|
-
* @param {'ticket' | 'auth' | '
|
|
3917
|
+
* Switch Settings top-level tab (Ticket source / Authentication / Configuration).
|
|
3918
|
+
* @param {'ticket' | 'auth' | 'config'} tab
|
|
3804
3919
|
*/
|
|
3805
3920
|
function setSettingsTab(tab) {
|
|
3806
|
-
const next =
|
|
3807
|
-
tab === 'auth' || tab === 'rules' || tab === 'config' ? tab : 'ticket';
|
|
3921
|
+
const next = tab === 'auth' || tab === 'config' ? tab : 'ticket';
|
|
3808
3922
|
settingsTab = next;
|
|
3809
3923
|
|
|
3810
3924
|
if (els.settingsTabs) {
|
|
@@ -3823,9 +3937,6 @@ function setSettingsTab(tab) {
|
|
|
3823
3937
|
if (next === 'config') {
|
|
3824
3938
|
void fetchModels();
|
|
3825
3939
|
}
|
|
3826
|
-
if (next === 'rules') {
|
|
3827
|
-
void fetchRuleStatuses();
|
|
3828
|
-
}
|
|
3829
3940
|
}
|
|
3830
3941
|
|
|
3831
3942
|
/**
|
|
@@ -4714,7 +4825,7 @@ els.settingsTabs?.addEventListener('click', (e) => {
|
|
|
4714
4825
|
const btn = e.target.closest('[data-settings-tab]');
|
|
4715
4826
|
if (!btn?.dataset.settingsTab) return;
|
|
4716
4827
|
setSettingsTab(
|
|
4717
|
-
/** @type {'ticket' | 'auth' | '
|
|
4828
|
+
/** @type {'ticket' | 'auth' | 'config'} */ (btn.dataset.settingsTab)
|
|
4718
4829
|
);
|
|
4719
4830
|
});
|
|
4720
4831
|
|
|
@@ -4738,7 +4849,7 @@ els.settingsTabs?.addEventListener('keydown', (e) => {
|
|
|
4738
4849
|
e.preventDefault();
|
|
4739
4850
|
const nextTab = tabs[nextIdx];
|
|
4740
4851
|
setSettingsTab(
|
|
4741
|
-
/** @type {'ticket' | 'auth' | '
|
|
4852
|
+
/** @type {'ticket' | 'auth' | 'config'} */ (nextTab.dataset.settingsTab)
|
|
4742
4853
|
);
|
|
4743
4854
|
nextTab.focus();
|
|
4744
4855
|
});
|
|
@@ -4990,7 +5101,6 @@ els.jiraTestBtn?.addEventListener('click', async () => {
|
|
|
4990
5101
|
}
|
|
4991
5102
|
els.jiraStatus.textContent = `Connected as ${data.displayName || 'OK'}`;
|
|
4992
5103
|
els.jiraStatus.className = 'jira-status ok';
|
|
4993
|
-
if (settingsTab === 'rules') void fetchJiraRuleStatuses();
|
|
4994
5104
|
} catch (err) {
|
|
4995
5105
|
els.jiraStatus.textContent = err.message || 'Test failed';
|
|
4996
5106
|
els.jiraStatus.className = 'jira-status err';
|
package/public/index.html
CHANGED
|
@@ -352,6 +352,8 @@
|
|
|
352
352
|
|
|
353
353
|
<div id="review-pr-section" class="card card-success hidden">
|
|
354
354
|
<p class="pr-opened-line">PR opened: <a id="review-pr-link" href="#" target="_blank" rel="noopener"></a></p>
|
|
355
|
+
<p id="review-pr-status-line" class="field-hint"></p>
|
|
356
|
+
<div id="review-pr-status-notes" class="pr-status-notes"></div>
|
|
355
357
|
<div class="actions">
|
|
356
358
|
<button id="review-pr-clear-btn" type="button" class="btn btn-secondary btn-sm">Clear from history</button>
|
|
357
359
|
</div>
|
|
@@ -431,6 +433,7 @@
|
|
|
431
433
|
aria-selected="false"
|
|
432
434
|
tabindex="-1"
|
|
433
435
|
>Authentication</button>
|
|
436
|
+
<!--
|
|
434
437
|
<button
|
|
435
438
|
type="button"
|
|
436
439
|
class="settings-tab"
|
|
@@ -441,6 +444,7 @@
|
|
|
441
444
|
aria-selected="false"
|
|
442
445
|
tabindex="-1"
|
|
443
446
|
>Rules</button>
|
|
447
|
+
-->
|
|
444
448
|
<button
|
|
445
449
|
type="button"
|
|
446
450
|
class="settings-tab"
|
|
@@ -571,6 +575,7 @@
|
|
|
571
575
|
</div>
|
|
572
576
|
</div>
|
|
573
577
|
|
|
578
|
+
<!--
|
|
574
579
|
<div
|
|
575
580
|
class="settings-panel hidden"
|
|
576
581
|
role="tabpanel"
|
|
@@ -640,7 +645,7 @@
|
|
|
640
645
|
<div class="settings-field" id="settings-github-rule-status-field">
|
|
641
646
|
<label class="field-label" for="settings-github-rule-status">Target status</label>
|
|
642
647
|
<select id="settings-github-rule-status" class="input" name="githubRuleTargetStatus"></select>
|
|
643
|
-
<p class="field-hint" id="settings-github-rule-status-hint">Live GitHub Project
|
|
648
|
+
<p class="field-hint" id="settings-github-rule-status-hint">Live statuses from GitHub Project v2 or labels. Must match a reachable board status / label.</p>
|
|
644
649
|
</div>
|
|
645
650
|
<div class="settings-field" id="settings-github-rule-label-field">
|
|
646
651
|
<label class="field-label" for="settings-github-rule-label">Label</label>
|
|
@@ -651,6 +656,8 @@
|
|
|
651
656
|
</div>
|
|
652
657
|
</div>
|
|
653
658
|
</div>
|
|
659
|
+
-->
|
|
660
|
+
</div>
|
|
654
661
|
|
|
655
662
|
<div
|
|
656
663
|
class="settings-panel hidden"
|
package/public/styles.css
CHANGED
|
@@ -1594,6 +1594,43 @@ a { color: var(--primary); text-underline-offset: 3px; }
|
|
|
1594
1594
|
margin-bottom: 12px;
|
|
1595
1595
|
}
|
|
1596
1596
|
|
|
1597
|
+
.pr-status-notes {
|
|
1598
|
+
display: grid;
|
|
1599
|
+
gap: 10px;
|
|
1600
|
+
margin: 0 0 12px;
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
.pr-status-notes-title {
|
|
1604
|
+
font-size: 12px;
|
|
1605
|
+
font-weight: 700;
|
|
1606
|
+
letter-spacing: 0.04em;
|
|
1607
|
+
text-transform: uppercase;
|
|
1608
|
+
color: var(--text-muted);
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
.pr-status-note-list {
|
|
1612
|
+
display: grid;
|
|
1613
|
+
gap: 8px;
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
.pr-status-note {
|
|
1617
|
+
padding: 10px 12px;
|
|
1618
|
+
border: 1px solid var(--border-soft);
|
|
1619
|
+
border-radius: 10px;
|
|
1620
|
+
background: var(--surface-2);
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
.pr-status-note-meta {
|
|
1624
|
+
font-size: 12px;
|
|
1625
|
+
color: var(--text-muted);
|
|
1626
|
+
margin-bottom: 4px;
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
.pr-status-note-body {
|
|
1630
|
+
white-space: pre-wrap;
|
|
1631
|
+
line-height: 1.45;
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1597
1634
|
/* —— Review file selection —— */
|
|
1598
1635
|
.review-files-header {
|
|
1599
1636
|
display: flex;
|
package/src/github.js
CHANGED
|
@@ -393,6 +393,122 @@ export function buildPrCreateArgs({ title, body, baseBranch, branchName, draft =
|
|
|
393
393
|
return args;
|
|
394
394
|
}
|
|
395
395
|
|
|
396
|
+
/**
|
|
397
|
+
* @typedef {{
|
|
398
|
+
* ok: true,
|
|
399
|
+
* url: string,
|
|
400
|
+
* title: string,
|
|
401
|
+
* number: number,
|
|
402
|
+
* state: string,
|
|
403
|
+
* isDraft: boolean,
|
|
404
|
+
* reviewDecision: string | null,
|
|
405
|
+
* mergeStateStatus: string | null,
|
|
406
|
+
* closedAt: string | null,
|
|
407
|
+
* mergedAt: string | null,
|
|
408
|
+
* author: string | null,
|
|
409
|
+
* requestedReviewers: string[],
|
|
410
|
+
* latestReviews: Array<{
|
|
411
|
+
* author: string | null,
|
|
412
|
+
* state: string,
|
|
413
|
+
* body: string,
|
|
414
|
+
* submittedAt: string | null,
|
|
415
|
+
* }>,
|
|
416
|
+
* }} GithubPrStatusOk
|
|
417
|
+
*
|
|
418
|
+
* @typedef {{
|
|
419
|
+
* ok: false,
|
|
420
|
+
* error: string,
|
|
421
|
+
* }} GithubPrStatusError
|
|
422
|
+
*
|
|
423
|
+
* @typedef {GithubPrStatusOk | GithubPrStatusError} GithubPrStatusResult
|
|
424
|
+
*/
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* @param {unknown} user
|
|
428
|
+
* @returns {string | null}
|
|
429
|
+
*/
|
|
430
|
+
function userName(user) {
|
|
431
|
+
if (!user || typeof user !== 'object') return null;
|
|
432
|
+
const name = String(user.login || user.name || user.displayName || '').trim();
|
|
433
|
+
return name || null;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* @param {unknown} review
|
|
438
|
+
* @returns {{ author: string | null, state: string, body: string, submittedAt: string | null } | null}
|
|
439
|
+
*/
|
|
440
|
+
function normalizeReview(review) {
|
|
441
|
+
if (!review || typeof review !== 'object') return null;
|
|
442
|
+
const state = String(review.state || '').trim().toUpperCase();
|
|
443
|
+
const body = String(review.body || '').trim();
|
|
444
|
+
const submittedAt = String(review.submittedAt || review.createdAt || review.updatedAt || '').trim();
|
|
445
|
+
return {
|
|
446
|
+
author: userName(review.author),
|
|
447
|
+
state: state || 'UNKNOWN',
|
|
448
|
+
body,
|
|
449
|
+
submittedAt: submittedAt || null,
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Current GitHub PR review state + summary via `gh pr view`.
|
|
455
|
+
* @param {{
|
|
456
|
+
* prUrl: string,
|
|
457
|
+
* cwd: string,
|
|
458
|
+
* runGh?: typeof execFileAsync,
|
|
459
|
+
* }} opts
|
|
460
|
+
* @returns {Promise<GithubPrStatusResult>}
|
|
461
|
+
*/
|
|
462
|
+
export async function fetchGithubPrStatus({ prUrl, cwd, runGh = execFileAsync }) {
|
|
463
|
+
const url = String(prUrl || '').trim();
|
|
464
|
+
if (!url) {
|
|
465
|
+
return { ok: false, error: 'PR URL is required' };
|
|
466
|
+
}
|
|
467
|
+
try {
|
|
468
|
+
const { stdout } = await runGh(
|
|
469
|
+
'gh',
|
|
470
|
+
[
|
|
471
|
+
'pr',
|
|
472
|
+
'view',
|
|
473
|
+
url,
|
|
474
|
+
'--json',
|
|
475
|
+
'url,title,number,state,isDraft,reviewDecision,mergeStateStatus,closedAt,mergedAt,author,reviewRequests,latestReviews',
|
|
476
|
+
],
|
|
477
|
+
{ cwd }
|
|
478
|
+
);
|
|
479
|
+
const raw = JSON.parse(stdout);
|
|
480
|
+
const latestReviews = Array.isArray(raw.latestReviews)
|
|
481
|
+
? raw.latestReviews.map(normalizeReview).filter(Boolean)
|
|
482
|
+
: [];
|
|
483
|
+
const requestedReviewers = Array.isArray(raw.reviewRequests || raw.requestedReviewers)
|
|
484
|
+
? (raw.reviewRequests || raw.requestedReviewers).map((item) => {
|
|
485
|
+
if (item && typeof item === 'object' && 'requestedReviewer' in item) {
|
|
486
|
+
return userName(item.requestedReviewer);
|
|
487
|
+
}
|
|
488
|
+
return userName(item);
|
|
489
|
+
}).filter(Boolean)
|
|
490
|
+
: [];
|
|
491
|
+
return {
|
|
492
|
+
ok: true,
|
|
493
|
+
url: String(raw.url || url),
|
|
494
|
+
title: String(raw.title || '').trim(),
|
|
495
|
+
number: Number(raw.number || 0),
|
|
496
|
+
state: String(raw.state || '').trim().toUpperCase() || 'UNKNOWN',
|
|
497
|
+
isDraft: Boolean(raw.isDraft),
|
|
498
|
+
reviewDecision: String(raw.reviewDecision || '').trim().toUpperCase() || null,
|
|
499
|
+
mergeStateStatus: String(raw.mergeStateStatus || '').trim().toUpperCase() || null,
|
|
500
|
+
closedAt: String(raw.closedAt || '').trim() || null,
|
|
501
|
+
mergedAt: String(raw.mergedAt || '').trim() || null,
|
|
502
|
+
author: userName(raw.author),
|
|
503
|
+
requestedReviewers,
|
|
504
|
+
latestReviews,
|
|
505
|
+
};
|
|
506
|
+
} catch (err) {
|
|
507
|
+
const message = err.stderr?.toString() || err.message || String(err);
|
|
508
|
+
return { ok: false, error: `Failed to fetch PR status for ${url}: ${message}` };
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
396
512
|
/**
|
|
397
513
|
* Create a GitHub PR (draft or ready for review).
|
|
398
514
|
* @param {{
|
|
@@ -442,7 +558,6 @@ export async function createPr({
|
|
|
442
558
|
throw err;
|
|
443
559
|
}
|
|
444
560
|
}
|
|
445
|
-
|
|
446
561
|
/**
|
|
447
562
|
* Add a label to a GitHub issue via `gh`.
|
|
448
563
|
* @param {number | string} issueNumber
|
package/src/server.js
CHANGED
|
@@ -3,7 +3,7 @@ import fs from 'node:fs';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
5
|
import { Store } from './store.js';
|
|
6
|
-
import { parseIssueUrl, createPr, fetchIssueDetails, listGithubIssueStatuses } from './github.js';
|
|
6
|
+
import { parseIssueUrl, createPr, fetchGithubPrStatus, fetchIssueDetails, listGithubIssueStatuses } from './github.js';
|
|
7
7
|
import {
|
|
8
8
|
parseJiraIssueRef,
|
|
9
9
|
resolveJiraCredentials,
|
|
@@ -172,6 +172,8 @@ export function normalizeReviewComments(body) {
|
|
|
172
172
|
* deps?: {
|
|
173
173
|
* pushBranch?: typeof pushBranch,
|
|
174
174
|
* createPr?: typeof createPr,
|
|
175
|
+
* fetchGithubPrStatus?: typeof fetchGithubPrStatus,
|
|
176
|
+
* removeWorktree?: typeof removeWorktree,
|
|
175
177
|
* getDiff?: typeof getDiff,
|
|
176
178
|
* listChangedFiles?: typeof listChangedFiles,
|
|
177
179
|
* applyFileExclusions?: typeof applyFileExclusions,
|
|
@@ -192,17 +194,37 @@ export function createServer({ repoRoot, config, store, useStubAgent = false, de
|
|
|
192
194
|
const app = express();
|
|
193
195
|
const publicDir = path.join(__dirname, '..', 'public');
|
|
194
196
|
app.use(express.json());
|
|
195
|
-
|
|
196
197
|
const doPushBranch = deps.pushBranch || pushBranch;
|
|
197
198
|
const doCreatePr = deps.createPr || createPr;
|
|
199
|
+
const doRemoveWorktree = deps.removeWorktree || removeWorktree;
|
|
198
200
|
const doGetDiff = deps.getDiff || getDiff;
|
|
199
201
|
const doListChangedFiles = deps.listChangedFiles || listChangedFiles;
|
|
200
202
|
const doApplyFileExclusions = deps.applyFileExclusions || applyFileExclusions;
|
|
203
|
+
const doFetchGithubPrStatus = deps.fetchGithubPrStatus || fetchGithubPrStatus;
|
|
201
204
|
const doCheckGhAuth = deps.checkGhAuth || checkGhAuth;
|
|
202
205
|
const doCheckClaudeAuth = deps.checkClaudeAuth || checkClaudeAuth;
|
|
203
206
|
const doListJiraBoardStatuses = deps.listJiraBoardStatuses || listJiraBoardStatuses;
|
|
204
207
|
const doListGithubIssueStatuses = deps.listGithubIssueStatuses || listGithubIssueStatuses;
|
|
205
208
|
|
|
209
|
+
/** @type {Map<string, { at: number, value: object }>} */
|
|
210
|
+
const prStatusCache = new Map();
|
|
211
|
+
|
|
212
|
+
async function getPrStatusForJob(job) {
|
|
213
|
+
const prUrl = String(job?.prUrl || '').trim();
|
|
214
|
+
if (!prUrl) return null;
|
|
215
|
+
|
|
216
|
+
const cached = prStatusCache.get(prUrl);
|
|
217
|
+
const now = Date.now();
|
|
218
|
+
if (cached && now - cached.at < 15_000) {
|
|
219
|
+
return cached.value;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const result = await doFetchGithubPrStatus({ prUrl, cwd: repoRoot });
|
|
223
|
+
const value = result.ok ? { ok: true, ...result } : result;
|
|
224
|
+
prStatusCache.set(prUrl, { at: now, value });
|
|
225
|
+
return value;
|
|
226
|
+
}
|
|
227
|
+
|
|
206
228
|
function formatAgentJobError(err) {
|
|
207
229
|
return err instanceof Error ? err.message : String(err);
|
|
208
230
|
}
|
|
@@ -613,10 +635,16 @@ export function createServer({ repoRoot, config, store, useStubAgent = false, de
|
|
|
613
635
|
}
|
|
614
636
|
}
|
|
615
637
|
|
|
616
|
-
app.get('/api/jobs', (_req, res) => {
|
|
638
|
+
app.get('/api/jobs', async (_req, res) => {
|
|
617
639
|
try {
|
|
618
|
-
|
|
619
|
-
|
|
640
|
+
const jobs = await Promise.all(
|
|
641
|
+
store.getJobs().map(async (job) => {
|
|
642
|
+
const withUsage = withJobUsage(job);
|
|
643
|
+
const prStatus = await getPrStatusForJob(withUsage);
|
|
644
|
+
return prStatus ? { ...withUsage, prStatus } : withUsage;
|
|
645
|
+
})
|
|
646
|
+
);
|
|
647
|
+
res.json(jobs);
|
|
620
648
|
} catch (err) {
|
|
621
649
|
res.status(500).json({ error: err.message });
|
|
622
650
|
}
|
|
@@ -1129,7 +1157,23 @@ export function createServer({ repoRoot, config, store, useStubAgent = false, de
|
|
|
1129
1157
|
);
|
|
1130
1158
|
}
|
|
1131
1159
|
|
|
1132
|
-
|
|
1160
|
+
const refreshed = store.getJob(updated.id) || updated;
|
|
1161
|
+
const wtId = worktreeIdForJob(refreshed);
|
|
1162
|
+
if (wtId != null && refreshed.worktreePath) {
|
|
1163
|
+
try {
|
|
1164
|
+
await doRemoveWorktree(repoRoot, wtId, refreshed.branchName);
|
|
1165
|
+
} catch (cleanupErr) {
|
|
1166
|
+
console.warn(
|
|
1167
|
+
`[acdev] removeWorktree after PR open failed for job ${refreshed.id}:`,
|
|
1168
|
+
cleanupErr instanceof Error ? cleanupErr.message : cleanupErr
|
|
1169
|
+
);
|
|
1170
|
+
}
|
|
1171
|
+
store.updateJob(refreshed.id, { worktreePath: undefined });
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
const finalJob = withJobUsage(store.getJob(updated.id) || updated);
|
|
1175
|
+
const finalPrStatus = await getPrStatusForJob(finalJob);
|
|
1176
|
+
res.json(finalPrStatus ? { ...finalJob, prStatus: finalPrStatus } : finalJob);
|
|
1133
1177
|
} catch (err) {
|
|
1134
1178
|
const message = err instanceof Error ? err.message : String(err);
|
|
1135
1179
|
store.updateJob(job.id, { status: 'failed', error: message });
|