acdev 1.0.3 → 1.0.5
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 +11 -7
- package/bin/acdev.js +7 -4
- package/package.json +1 -1
- package/public/app.js +31 -0
- package/public/index.html +6 -0
- package/public/styles.css +24 -0
- package/src/claude-auth.js +7 -13
- package/src/config.js +2 -0
- package/src/gh-auth.js +11 -10
- package/src/server.js +98 -2
package/README.md
CHANGED
|
@@ -5,11 +5,13 @@ Local CLI + web UI for running AI coding agents on GitHub Issues or Jira tickets
|
|
|
5
5
|
## Prerequisites
|
|
6
6
|
|
|
7
7
|
- Node.js 20+
|
|
8
|
-
- [GitHub CLI](https://cli.github.com/) (`gh`) — required
|
|
9
|
-
- Claude authentication (see [Authentication](#authentication) below)
|
|
8
|
+
- [GitHub CLI](https://cli.github.com/) (`gh`) on your `PATH` — required to fetch GitHub issues and open PRs. You can install `gh` first and authenticate later (see below).
|
|
10
9
|
- A git repository with a `develop` branch on `origin` (or configure another base branch)
|
|
10
|
+
- Optional: Claude authentication for real agent runs (or use `--stub-agent` for UI-only testing)
|
|
11
11
|
- Optional: Jira Cloud credentials when using Jira as the ticket source (see [Jira](#jira-ticket-source))
|
|
12
12
|
|
|
13
|
+
You do **not** need GitHub or Claude tokens before starting `acdev`. The server always starts (as long as you are inside a git repo); configure auth in **Settings → Authentication**, then enqueue jobs.
|
|
14
|
+
|
|
13
15
|
## Install
|
|
14
16
|
|
|
15
17
|
```bash
|
|
@@ -27,7 +29,9 @@ npm link # optional — makes `acdev` available globally
|
|
|
27
29
|
|
|
28
30
|
## Authentication
|
|
29
31
|
|
|
30
|
-
Configure GitHub and Claude from **Settings** in the web UI (tokens are written to `.acdev/.env`, gitignored) or by editing that file directly. Leave a Settings field blank to keep the current secret. Shell environment variables override `.env` on startup; saving in Settings updates the running process immediately.
|
|
32
|
+
`acdev` starts even when auth is missing and prints a warning. Configure GitHub and Claude from **Settings → Authentication** in the web UI (tokens are written to `.acdev/.env`, gitignored) or by editing that file directly. Leave a Settings field blank to keep the current secret. Shell environment variables override `.env` on startup; saving in Settings updates the running process immediately so status refreshes without a restart.
|
|
33
|
+
|
|
34
|
+
Enqueueing issues, retrying jobs, and applying review feedback require Claude (unless `--stub-agent`). Opening a PR always requires GitHub auth. The Overview banner prompts you when auth is incomplete.
|
|
31
35
|
|
|
32
36
|
`.env` is optional when you already use `gh auth login` and `claude auth login` on the machine.
|
|
33
37
|
|
|
@@ -39,7 +43,7 @@ Configure GitHub and Claude from **Settings** in the web UI (tokens are written
|
|
|
39
43
|
2. **Personal access token** — paste a PAT in **Settings → GitHub / PR auth**, or set `GH_TOKEN` (or `GITHUB_TOKEN`) in `.acdev/.env`. `gh` reads those env vars, so no interactive login is required. Non-interactive CLI equivalent: `echo YOUR_PAT | gh auth login --with-token -h github.com`.
|
|
40
44
|
3. **SSH remotes are not a substitute.** An SSH `origin` can push/fetch git, but it does **not** authenticate `gh` for issues or PRs. Settings may show the origin URL as read-only info.
|
|
41
45
|
|
|
42
|
-
On a machine
|
|
46
|
+
On a fresh machine: install `gh`, start `acdev`, then paste a PAT in Settings (or set `GH_TOKEN` in `.acdev/.env`). You can also copy `.acdev/.env.example` to `.acdev/.env` before starting.
|
|
43
47
|
|
|
44
48
|
### Claude
|
|
45
49
|
|
|
@@ -121,13 +125,13 @@ Optional **Rules** in Settings can move the Jira ticket to a target status (e.g.
|
|
|
121
125
|
|
|
122
126
|
## Usage
|
|
123
127
|
|
|
124
|
-
From inside a cloned git repository
|
|
128
|
+
From inside a cloned git repository:
|
|
125
129
|
|
|
126
130
|
```bash
|
|
127
131
|
acdev
|
|
128
132
|
```
|
|
129
133
|
|
|
130
|
-
The tool starts a local server (default port `4848`), opens the browser, and shows the web UI.
|
|
134
|
+
The tool starts a local server (default port `4848`), opens the browser, and shows the web UI. If GitHub or Claude auth is missing, the CLI warns and the UI shows a banner — open **Settings → Authentication** to add tokens, then enqueue jobs.
|
|
131
135
|
|
|
132
136
|
### CLI flags
|
|
133
137
|
|
|
@@ -228,7 +232,7 @@ npm test
|
|
|
228
232
|
|
|
229
233
|
## Architecture
|
|
230
234
|
|
|
231
|
-
- `bin/acdev.js` — CLI entry
|
|
235
|
+
- `bin/acdev.js` — CLI entry; soft-checks `gh` / Claude auth (warns, does not exit). Tokens in `.acdev/.env` are loaded before auth checks. Job APIs reject until auth is OK.
|
|
232
236
|
- `src/claude-auth.js` — Anthropic API key / Claude Code subscription auth detection
|
|
233
237
|
- `src/gh-auth.js` — GitHub CLI auth (`gh auth login` or `GH_TOKEN`)
|
|
234
238
|
- `src/server.js` — Express API, SSE logs, job queue, config + Jira test endpoints
|
package/bin/acdev.js
CHANGED
|
@@ -58,18 +58,21 @@ async function main() {
|
|
|
58
58
|
migrateLegacyWorktreesDir(repoRoot);
|
|
59
59
|
loadEnv(repoRoot);
|
|
60
60
|
|
|
61
|
+
// Soft auth: never exit for missing GitHub/Claude credentials.
|
|
62
|
+
// Always start the server so Settings can add tokens; job endpoints
|
|
63
|
+
// reject with a clear 4xx until auth is configured.
|
|
61
64
|
const ghAuth = checkGhAuth();
|
|
62
65
|
if (!ghAuth.ok) {
|
|
63
|
-
console.
|
|
64
|
-
process.exit(1);
|
|
66
|
+
console.warn(formatGhAuthError(ghAuth));
|
|
65
67
|
}
|
|
66
68
|
|
|
67
69
|
if (!opts.stubAgent) {
|
|
68
70
|
const claudeAuth = checkClaudeAuth();
|
|
69
71
|
if (!claudeAuth.ok) {
|
|
70
|
-
console.
|
|
71
|
-
process.exit(1);
|
|
72
|
+
console.warn(formatClaudeAuthError(claudeAuth));
|
|
72
73
|
}
|
|
74
|
+
} else {
|
|
75
|
+
console.log('✓ Stub agent enabled (Claude auth not required)');
|
|
73
76
|
}
|
|
74
77
|
|
|
75
78
|
const config = loadConfig(repoRoot);
|
package/package.json
CHANGED
package/public/app.js
CHANGED
|
@@ -163,6 +163,7 @@ let ticketSource = 'github';
|
|
|
163
163
|
* anthropicApiKeyMasked?: string | null,
|
|
164
164
|
* claudeOauthTokenSet?: boolean,
|
|
165
165
|
* claudeOauthTokenMasked?: string | null,
|
|
166
|
+
* stubAgent?: boolean,
|
|
166
167
|
* }} */
|
|
167
168
|
let appConfig = {};
|
|
168
169
|
|
|
@@ -178,6 +179,9 @@ const els = {
|
|
|
178
179
|
navBadgeRuns: document.getElementById('nav-badge-runs'),
|
|
179
180
|
navBadgeReview: document.getElementById('nav-badge-review'),
|
|
180
181
|
navBadgeAlerts: document.getElementById('nav-badge-alerts'),
|
|
182
|
+
authBanner: document.getElementById('auth-banner'),
|
|
183
|
+
authBannerText: document.getElementById('auth-banner-text'),
|
|
184
|
+
authBannerBtn: document.getElementById('auth-banner-btn'),
|
|
181
185
|
issueUrls: document.getElementById('issue-urls'),
|
|
182
186
|
issueUrlsLabel: document.getElementById('issue-urls-label'),
|
|
183
187
|
enqueueFeedback: document.getElementById('enqueue-feedback'),
|
|
@@ -3427,11 +3431,33 @@ function applyConfigSnapshot(data) {
|
|
|
3427
3431
|
if (els.repoBranch) {
|
|
3428
3432
|
els.repoBranch.textContent = data?.baseBranch || '—';
|
|
3429
3433
|
}
|
|
3434
|
+
updateAuthBanner(appConfig);
|
|
3430
3435
|
if (currentView === 'settings') {
|
|
3431
3436
|
fillSettingsForm(appConfig);
|
|
3432
3437
|
}
|
|
3433
3438
|
}
|
|
3434
3439
|
|
|
3440
|
+
/**
|
|
3441
|
+
* Surface missing GitHub / Claude auth on Overview so users can open Settings.
|
|
3442
|
+
* @param {typeof appConfig} cfg
|
|
3443
|
+
*/
|
|
3444
|
+
function updateAuthBanner(cfg) {
|
|
3445
|
+
if (!els.authBanner || !els.authBannerText) return;
|
|
3446
|
+
const missing = [];
|
|
3447
|
+
if (!cfg.ghAuthOk) missing.push('GitHub');
|
|
3448
|
+
if (!cfg.claudeAuthOk && !cfg.stubAgent) missing.push('Claude');
|
|
3449
|
+
if (missing.length === 0) {
|
|
3450
|
+
els.authBanner.classList.add('hidden');
|
|
3451
|
+
els.authBanner.hidden = true;
|
|
3452
|
+
els.authBannerText.textContent = '';
|
|
3453
|
+
return;
|
|
3454
|
+
}
|
|
3455
|
+
const label = missing.join(' and ');
|
|
3456
|
+
els.authBannerText.textContent = `Configure ${label} in Settings → Authentication before starting jobs.`;
|
|
3457
|
+
els.authBanner.classList.remove('hidden');
|
|
3458
|
+
els.authBanner.hidden = false;
|
|
3459
|
+
}
|
|
3460
|
+
|
|
3435
3461
|
async function fetchConfig() {
|
|
3436
3462
|
try {
|
|
3437
3463
|
const res = await fetch('/api/config');
|
|
@@ -3561,6 +3587,11 @@ document.querySelectorAll('.nav-item').forEach((btn) => {
|
|
|
3561
3587
|
btn.addEventListener('click', () => setView(/** @type {ViewId} */ (btn.dataset.view)));
|
|
3562
3588
|
});
|
|
3563
3589
|
|
|
3590
|
+
els.authBannerBtn?.addEventListener('click', () => {
|
|
3591
|
+
setView('settings');
|
|
3592
|
+
setSettingsTab('auth');
|
|
3593
|
+
});
|
|
3594
|
+
|
|
3564
3595
|
els.themeToggle.addEventListener('click', () => {
|
|
3565
3596
|
const next = els.app.dataset.theme === 'dark' ? 'light' : 'dark';
|
|
3566
3597
|
applyTheme(next);
|
package/public/index.html
CHANGED
|
@@ -100,6 +100,12 @@
|
|
|
100
100
|
<!-- OVERVIEW -->
|
|
101
101
|
<section class="view" id="view-overview" data-view-panel="overview">
|
|
102
102
|
<div class="stack stack-lg max-w-overview">
|
|
103
|
+
<div id="auth-banner" class="auth-banner hidden" role="status" hidden>
|
|
104
|
+
<p id="auth-banner-text" class="auth-banner-text"></p>
|
|
105
|
+
<button type="button" class="btn btn-secondary btn-sm" id="auth-banner-btn">
|
|
106
|
+
Open Settings
|
|
107
|
+
</button>
|
|
108
|
+
</div>
|
|
103
109
|
<section>
|
|
104
110
|
<div class="section-label">Add issues</div>
|
|
105
111
|
<div class="card enqueue-card">
|
package/public/styles.css
CHANGED
|
@@ -1907,6 +1907,30 @@ body.diff-fs-open {
|
|
|
1907
1907
|
color: var(--danger, #b42318);
|
|
1908
1908
|
}
|
|
1909
1909
|
|
|
1910
|
+
.auth-banner {
|
|
1911
|
+
display: flex;
|
|
1912
|
+
align-items: center;
|
|
1913
|
+
justify-content: space-between;
|
|
1914
|
+
gap: 12px;
|
|
1915
|
+
flex-wrap: wrap;
|
|
1916
|
+
padding: 12px 14px;
|
|
1917
|
+
background: var(--amber-100);
|
|
1918
|
+
color: var(--amber-800);
|
|
1919
|
+
border-radius: 8px;
|
|
1920
|
+
font-size: 13px;
|
|
1921
|
+
}
|
|
1922
|
+
|
|
1923
|
+
.auth-banner.hidden,
|
|
1924
|
+
.auth-banner[hidden] {
|
|
1925
|
+
display: none;
|
|
1926
|
+
}
|
|
1927
|
+
|
|
1928
|
+
.auth-banner-text {
|
|
1929
|
+
margin: 0;
|
|
1930
|
+
flex: 1;
|
|
1931
|
+
min-width: 12rem;
|
|
1932
|
+
}
|
|
1933
|
+
|
|
1910
1934
|
.auth-clear {
|
|
1911
1935
|
display: inline-block;
|
|
1912
1936
|
margin-top: 6px;
|
package/src/claude-auth.js
CHANGED
|
@@ -90,22 +90,16 @@ export function publicClaudeAuthMethod(result) {
|
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
/**
|
|
93
|
-
* Human-readable startup
|
|
93
|
+
* Human-readable startup warning for a failed {@link checkClaudeAuth}.
|
|
94
|
+
* Soft-auth: this is not fatal — the server still starts so Settings can configure auth.
|
|
94
95
|
* @param {ClaudeAuthResult} [_result]
|
|
95
96
|
*/
|
|
96
97
|
export function formatClaudeAuthError(_result) {
|
|
97
98
|
return [
|
|
98
|
-
'
|
|
99
|
-
'',
|
|
100
|
-
'
|
|
101
|
-
'
|
|
102
|
-
'
|
|
103
|
-
' needs the CLI on this machine.',
|
|
104
|
-
' 2. Subscription token: `claude setup-token`, then set CLAUDE_CODE_OAUTH_TOKEN',
|
|
105
|
-
' in Settings → Claude authentication or `.acdev/.env`',
|
|
106
|
-
' 3. Anthropic API key (API billing): set ANTHROPIC_API_KEY in Settings or `.acdev/.env`',
|
|
107
|
-
'',
|
|
108
|
-
'API keys take precedence over subscription login. For UI-only testing without',
|
|
109
|
-
'auth: pass `--stub-agent`.',
|
|
99
|
+
'⚠ Claude is not authenticated — server will still start.',
|
|
100
|
+
' Open Settings → Authentication to add an API key or OAuth token',
|
|
101
|
+
' (or set ANTHROPIC_API_KEY / CLAUDE_CODE_OAUTH_TOKEN in `.acdev/.env`).',
|
|
102
|
+
' Browser login still needs `claude auth login` on this machine.',
|
|
103
|
+
' For UI-only testing without auth: pass `--stub-agent`.',
|
|
110
104
|
].join('\n');
|
|
111
105
|
}
|
package/src/config.js
CHANGED
|
@@ -391,6 +391,7 @@ export function updateConfig(repoRoot, config, patch) {
|
|
|
391
391
|
* @param {object} config
|
|
392
392
|
* @param {{
|
|
393
393
|
* repoRoot?: string,
|
|
394
|
+
* stubAgent?: boolean,
|
|
394
395
|
* ghAuth?: import('./gh-auth.js').GhAuthResult,
|
|
395
396
|
* claudeAuth?: import('./claude-auth.js').ClaudeAuthResult,
|
|
396
397
|
* }} [opts]
|
|
@@ -461,6 +462,7 @@ export function publicConfig(config, opts = {}) {
|
|
|
461
462
|
anthropicApiKeyMasked: anthropicMask.masked,
|
|
462
463
|
claudeOauthTokenSet: claudeOauthMask.set,
|
|
463
464
|
claudeOauthTokenMasked: claudeOauthMask.masked,
|
|
465
|
+
stubAgent: opts.stubAgent === true,
|
|
464
466
|
...(repoName ? { repoName } : {}),
|
|
465
467
|
};
|
|
466
468
|
}
|
package/src/gh-auth.js
CHANGED
|
@@ -138,21 +138,22 @@ export function checkGhAuth() {
|
|
|
138
138
|
}
|
|
139
139
|
|
|
140
140
|
/**
|
|
141
|
-
* Human-readable startup
|
|
141
|
+
* Human-readable startup warning for a failed {@link checkGhAuth}.
|
|
142
|
+
* Soft-auth: this is not fatal — the server still starts so Settings can configure auth.
|
|
142
143
|
* @param {GhAuthResult} result
|
|
143
144
|
*/
|
|
144
145
|
export function formatGhAuthError(result) {
|
|
145
146
|
if (result.reason === 'not-found') {
|
|
146
|
-
return
|
|
147
|
+
return [
|
|
148
|
+
'⚠ GitHub CLI (gh) not found in PATH — server will still start.',
|
|
149
|
+
' Install it from https://cli.github.com/, then open Settings → Authentication',
|
|
150
|
+
' (or set GH_TOKEN in `.acdev/.env`) before enqueueing jobs.',
|
|
151
|
+
].join('\n');
|
|
147
152
|
}
|
|
148
153
|
return [
|
|
149
|
-
'
|
|
150
|
-
'',
|
|
151
|
-
'
|
|
152
|
-
'
|
|
153
|
-
' 2. Personal access token: Settings → GitHub / PR auth, or set GH_TOKEN in `.acdev/.env`',
|
|
154
|
-
' 3. Non-interactive CLI: echo YOUR_PAT | gh auth login --with-token -h github.com',
|
|
155
|
-
'',
|
|
156
|
-
'SSH remotes can push/fetch git, but do not authenticate gh for issues or PRs.',
|
|
154
|
+
'⚠ GitHub is not authenticated — server will still start.',
|
|
155
|
+
' Open Settings → Authentication to paste a PAT, or set GH_TOKEN in `.acdev/.env`.',
|
|
156
|
+
' Or run: gh auth login -h github.com',
|
|
157
|
+
' (SSH remotes push/fetch git but do not authenticate gh for issues/PRs.)',
|
|
157
158
|
].join('\n');
|
|
158
159
|
}
|
package/src/server.js
CHANGED
|
@@ -36,6 +36,8 @@ import { upsertEnvVars } from './env.js';
|
|
|
36
36
|
import { listModels } from './models.js';
|
|
37
37
|
import { splitIssueUrls } from './urls.js';
|
|
38
38
|
import { usageFromLogs, withJobUsage } from './usage.js';
|
|
39
|
+
import { checkGhAuth } from './gh-auth.js';
|
|
40
|
+
import { checkClaudeAuth } from './claude-auth.js';
|
|
39
41
|
|
|
40
42
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
41
43
|
|
|
@@ -169,6 +171,8 @@ export function normalizeReviewComments(body) {
|
|
|
169
171
|
* addIssueLabel?: Function,
|
|
170
172
|
* closeIssue?: Function,
|
|
171
173
|
* resolveJiraCredentials?: Function,
|
|
174
|
+
* checkGhAuth?: typeof checkGhAuth,
|
|
175
|
+
* checkClaudeAuth?: typeof checkClaudeAuth,
|
|
172
176
|
* },
|
|
173
177
|
* }} options
|
|
174
178
|
*/
|
|
@@ -182,6 +186,52 @@ export function createServer({ repoRoot, config, store, useStubAgent = false, de
|
|
|
182
186
|
const doGetDiff = deps.getDiff || getDiff;
|
|
183
187
|
const doListChangedFiles = deps.listChangedFiles || listChangedFiles;
|
|
184
188
|
const doApplyFileExclusions = deps.applyFileExclusions || applyFileExclusions;
|
|
189
|
+
const doCheckGhAuth = deps.checkGhAuth || checkGhAuth;
|
|
190
|
+
const doCheckClaudeAuth = deps.checkClaudeAuth || checkClaudeAuth;
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Reject enqueue / agent / PR actions when required auth is missing.
|
|
194
|
+
* @param {{ needGh?: boolean, needClaude?: boolean }} [opts]
|
|
195
|
+
* @returns {{ status: number, error: string, code: string } | null}
|
|
196
|
+
*/
|
|
197
|
+
function authGate(opts = {}) {
|
|
198
|
+
const needGh = opts.needGh !== false;
|
|
199
|
+
const needClaude = opts.needClaude === true && !useStubAgent;
|
|
200
|
+
|
|
201
|
+
if (needGh) {
|
|
202
|
+
const gh = doCheckGhAuth();
|
|
203
|
+
if (!gh.ok) {
|
|
204
|
+
const error =
|
|
205
|
+
gh.reason === 'not-found'
|
|
206
|
+
? 'GitHub CLI (gh) is not installed. Install it from https://cli.github.com/, then restart acdev.'
|
|
207
|
+
: 'GitHub is not authenticated. Configure a PAT in Settings → Authentication, or run gh auth login.';
|
|
208
|
+
return { status: 400, error, code: 'gh_auth_required' };
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (needClaude) {
|
|
213
|
+
const claude = doCheckClaudeAuth();
|
|
214
|
+
if (!claude.ok) {
|
|
215
|
+
return {
|
|
216
|
+
status: 400,
|
|
217
|
+
error:
|
|
218
|
+
'Claude is not authenticated. Add an API key or OAuth token in Settings → Authentication, run claude auth login, or start with --stub-agent.',
|
|
219
|
+
code: 'claude_auth_required',
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function publicConfigPayload() {
|
|
228
|
+
return publicConfig(config, {
|
|
229
|
+
repoRoot,
|
|
230
|
+
stubAgent: useStubAgent,
|
|
231
|
+
ghAuth: doCheckGhAuth(),
|
|
232
|
+
claudeAuth: doCheckClaudeAuth(),
|
|
233
|
+
});
|
|
234
|
+
}
|
|
185
235
|
|
|
186
236
|
/** @type {Map<string, Set<import('http').ServerResponse>>} */
|
|
187
237
|
const subscribers = new Map();
|
|
@@ -221,6 +271,17 @@ export function createServer({ repoRoot, config, store, useStubAgent = false, de
|
|
|
221
271
|
let job = store.getJob(jobId);
|
|
222
272
|
if (!job) return;
|
|
223
273
|
|
|
274
|
+
const gate = authGate({ needGh: true, needClaude: true });
|
|
275
|
+
if (gate) {
|
|
276
|
+
appendLog(job, 'error', gate.error);
|
|
277
|
+
store.updateJob(jobId, { status: 'failed', error: gate.error });
|
|
278
|
+
const updated = store.getJob(jobId);
|
|
279
|
+
if (updated?.logs?.length) {
|
|
280
|
+
emitEvent(jobId, updated.logs[updated.logs.length - 1]);
|
|
281
|
+
}
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
|
|
224
285
|
try {
|
|
225
286
|
job = setStatus(jobId, 'syncing');
|
|
226
287
|
if (!job) return;
|
|
@@ -349,6 +410,21 @@ export function createServer({ repoRoot, config, store, useStubAgent = false, de
|
|
|
349
410
|
let job = store.getJob(jobId);
|
|
350
411
|
if (!job || job.status !== 'applying_feedback') return;
|
|
351
412
|
|
|
413
|
+
const gate = authGate({ needGh: false, needClaude: true });
|
|
414
|
+
if (gate) {
|
|
415
|
+
appendLog(job, 'error', gate.error);
|
|
416
|
+
store.updateJob(jobId, {
|
|
417
|
+
status: 'failed',
|
|
418
|
+
error: gate.error,
|
|
419
|
+
pendingReviewFeedback: undefined,
|
|
420
|
+
});
|
|
421
|
+
const updated = store.getJob(jobId);
|
|
422
|
+
if (updated?.logs?.length) {
|
|
423
|
+
emitEvent(jobId, updated.logs[updated.logs.length - 1]);
|
|
424
|
+
}
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
|
|
352
428
|
const feedback = job.pendingReviewFeedback;
|
|
353
429
|
if (!feedback || (!feedback.generalComment && !(feedback.lineComments || []).length)) {
|
|
354
430
|
const message = 'Missing review feedback payload';
|
|
@@ -461,7 +537,7 @@ export function createServer({ repoRoot, config, store, useStubAgent = false, de
|
|
|
461
537
|
|
|
462
538
|
app.get('/api/config', (_req, res) => {
|
|
463
539
|
try {
|
|
464
|
-
res.json(
|
|
540
|
+
res.json(publicConfigPayload());
|
|
465
541
|
} catch (err) {
|
|
466
542
|
res.status(500).json({ error: err.message });
|
|
467
543
|
}
|
|
@@ -517,7 +593,7 @@ export function createServer({ repoRoot, config, store, useStubAgent = false, de
|
|
|
517
593
|
...configPatch
|
|
518
594
|
} = patch;
|
|
519
595
|
updateConfig(repoRoot, config, configPatch);
|
|
520
|
-
res.json(
|
|
596
|
+
res.json(publicConfigPayload());
|
|
521
597
|
} catch (err) {
|
|
522
598
|
res.status(400).json({ error: err.message });
|
|
523
599
|
}
|
|
@@ -547,6 +623,11 @@ export function createServer({ repoRoot, config, store, useStubAgent = false, de
|
|
|
547
623
|
|
|
548
624
|
app.post('/api/issues', (req, res) => {
|
|
549
625
|
try {
|
|
626
|
+
const gate = authGate({ needGh: true, needClaude: true });
|
|
627
|
+
if (gate) {
|
|
628
|
+
return res.status(gate.status).json({ error: gate.error, code: gate.code });
|
|
629
|
+
}
|
|
630
|
+
|
|
550
631
|
const urls = splitIssueUrls(req.body?.urls);
|
|
551
632
|
if (urls.length === 0) {
|
|
552
633
|
return res.status(400).json({
|
|
@@ -722,6 +803,11 @@ export function createServer({ repoRoot, config, store, useStubAgent = false, de
|
|
|
722
803
|
|
|
723
804
|
app.post('/api/jobs/:id/review', (req, res) => {
|
|
724
805
|
try {
|
|
806
|
+
const gate = authGate({ needGh: false, needClaude: true });
|
|
807
|
+
if (gate) {
|
|
808
|
+
return res.status(gate.status).json({ error: gate.error, code: gate.code });
|
|
809
|
+
}
|
|
810
|
+
|
|
725
811
|
const job = store.getJob(req.params.id);
|
|
726
812
|
if (!job) {
|
|
727
813
|
return res.status(404).json({ error: 'Job not found' });
|
|
@@ -767,6 +853,11 @@ export function createServer({ repoRoot, config, store, useStubAgent = false, de
|
|
|
767
853
|
|
|
768
854
|
app.post('/api/jobs/:id/approve', async (req, res) => {
|
|
769
855
|
try {
|
|
856
|
+
const gate = authGate({ needGh: true, needClaude: false });
|
|
857
|
+
if (gate) {
|
|
858
|
+
return res.status(gate.status).json({ error: gate.error, code: gate.code });
|
|
859
|
+
}
|
|
860
|
+
|
|
770
861
|
const job = store.getJob(req.params.id);
|
|
771
862
|
if (!job) {
|
|
772
863
|
return res.status(404).json({ error: 'Job not found' });
|
|
@@ -945,6 +1036,11 @@ export function createServer({ repoRoot, config, store, useStubAgent = false, de
|
|
|
945
1036
|
|
|
946
1037
|
app.post('/api/jobs/:id/retry', (req, res) => {
|
|
947
1038
|
try {
|
|
1039
|
+
const gate = authGate({ needGh: true, needClaude: true });
|
|
1040
|
+
if (gate) {
|
|
1041
|
+
return res.status(gate.status).json({ error: gate.error, code: gate.code });
|
|
1042
|
+
}
|
|
1043
|
+
|
|
948
1044
|
const job = store.getJob(req.params.id);
|
|
949
1045
|
if (!job) {
|
|
950
1046
|
return res.status(404).json({ error: 'Job not found' });
|