acdev 1.0.1 → 1.0.3

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/public/index.html CHANGED
@@ -73,10 +73,7 @@
73
73
  <div class="sidebar-divider"></div>
74
74
  <div class="model-block">
75
75
  <div class="model-label">Model</div>
76
- <div class="model-toggle" id="model-toggle" role="group" aria-label="Claude model">
77
- <button type="button" class="model-btn active" data-model="claude-sonnet-5">Sonnet 5</button>
78
- <button type="button" class="model-btn" data-model="claude-opus-5">Opus 5</button>
79
- </div>
76
+ <div class="model-current" id="model-current" title="Change in Settings → Configuration">—</div>
80
77
  </div>
81
78
  <div class="repo-meta">
82
79
  <div class="repo-name" id="repo-name">—</div>
@@ -304,87 +301,205 @@
304
301
  <!-- SETTINGS -->
305
302
  <section class="view hidden" id="view-settings" data-view-panel="settings">
306
303
  <div class="stack stack-lg max-w-settings">
307
- <section>
308
- <div class="section-label">Ticket source</div>
309
- <p class="settings-lead">
310
- Choose where tickets come from. Pull requests are always created on the local repo’s GitHub remote via <code>gh</code>.
311
- </p>
312
- <div class="card settings-card">
313
- <div class="settings-field settings-field-full">
314
- <div class="field-label">Ticket source</div>
315
- <div class="source-toggle" id="settings-ticket-source" role="group" aria-label="Ticket source">
316
- <button type="button" class="source-btn active" data-source="github">GitHub Issues</button>
317
- <button type="button" class="source-btn" data-source="jira">Jira</button>
318
- </div>
319
- <p class="field-hint">Switch anytime — queued jobs keep their original source.</p>
320
- </div>
304
+ <div
305
+ class="settings-tabs"
306
+ id="settings-tabs"
307
+ role="tablist"
308
+ aria-label="Settings sections"
309
+ >
310
+ <button
311
+ type="button"
312
+ class="settings-tab"
313
+ role="tab"
314
+ id="settings-tab-ticket"
315
+ data-settings-tab="ticket"
316
+ aria-controls="settings-panel-ticket"
317
+ aria-selected="true"
318
+ >Ticket source</button>
319
+ <button
320
+ type="button"
321
+ class="settings-tab"
322
+ role="tab"
323
+ id="settings-tab-auth"
324
+ data-settings-tab="auth"
325
+ aria-controls="settings-panel-auth"
326
+ aria-selected="false"
327
+ tabindex="-1"
328
+ >Authentication</button>
329
+ <button
330
+ type="button"
331
+ class="settings-tab"
332
+ role="tab"
333
+ id="settings-tab-rules"
334
+ data-settings-tab="rules"
335
+ aria-controls="settings-panel-rules"
336
+ aria-selected="false"
337
+ tabindex="-1"
338
+ >Rules</button>
339
+ <button
340
+ type="button"
341
+ class="settings-tab"
342
+ role="tab"
343
+ id="settings-tab-config"
344
+ data-settings-tab="config"
345
+ aria-controls="settings-panel-config"
346
+ aria-selected="false"
347
+ tabindex="-1"
348
+ >Configuration</button>
349
+ </div>
321
350
 
322
- <div id="jira-connect-panel" class="jira-panel hidden">
323
- <div class="settings-grid">
324
- <div class="settings-field settings-field-full">
325
- <label class="field-label" for="settings-jira-base-url">Jira Cloud base URL</label>
326
- <input id="settings-jira-base-url" class="input" type="url" name="jiraBaseUrl" autocomplete="off" placeholder="https://your-domain.atlassian.net">
327
- <p class="field-hint">Your Atlassian Cloud site (Jira Server/Data Center is not supported).</p>
351
+ <form id="settings-form" class="stack stack-lg" novalidate>
352
+ <div
353
+ class="settings-panel"
354
+ role="tabpanel"
355
+ id="settings-panel-ticket"
356
+ data-settings-panel="ticket"
357
+ aria-labelledby="settings-tab-ticket"
358
+ >
359
+ <p class="settings-lead">
360
+ Choose where tickets come from. Pull requests are always created on the local repo’s GitHub remote via <code>gh</code>.
361
+ </p>
362
+ <div class="card settings-card">
363
+ <div class="settings-field settings-field-full">
364
+ <div class="field-label">Ticket source</div>
365
+ <div class="source-toggle" id="settings-ticket-source" role="group" aria-label="Ticket source">
366
+ <button type="button" class="source-btn active" data-source="github">GitHub Issues</button>
367
+ <button type="button" class="source-btn" data-source="jira">Jira</button>
328
368
  </div>
329
- <div class="settings-field">
330
- <label class="field-label" for="settings-jira-email">Email</label>
331
- <input id="settings-jira-email" class="input" type="email" name="jiraEmail" autocomplete="username" placeholder="you@company.com">
369
+ <p class="field-hint">Switch anytime — queued jobs keep their original source.</p>
370
+ </div>
371
+
372
+ <div id="jira-connect-panel" class="jira-panel hidden">
373
+ <div class="settings-grid">
374
+ <div class="settings-field settings-field-full">
375
+ <label class="field-label" for="settings-jira-base-url">Jira Cloud base URL</label>
376
+ <input id="settings-jira-base-url" class="input" type="url" name="jiraBaseUrl" autocomplete="off" placeholder="https://your-domain.atlassian.net">
377
+ <p class="field-hint">Your Atlassian Cloud site (Jira Server/Data Center is not supported).</p>
378
+ </div>
379
+ <div class="settings-field">
380
+ <label class="field-label" for="settings-jira-email">Email</label>
381
+ <input id="settings-jira-email" class="input" type="email" name="jiraEmail" autocomplete="username" placeholder="you@company.com">
382
+ </div>
383
+ <div class="settings-field">
384
+ <label class="field-label" for="settings-jira-token">API token</label>
385
+ <input id="settings-jira-token" class="input" type="password" name="jiraApiToken" autocomplete="new-password" placeholder="Leave blank to keep existing">
386
+ <p class="field-hint" id="settings-jira-token-hint">Create a token at id.atlassian.com → Security → API tokens. Stored in <code>.acdev/.env</code> (not committed).</p>
387
+ </div>
388
+ <div class="settings-field settings-field-full">
389
+ <label class="field-label" for="settings-jira-pr-phrase">PR link phrase</label>
390
+ <input id="settings-jira-pr-phrase" class="input" type="text" name="jiraPrLinkPhrase" autocomplete="off" placeholder="Relates to">
391
+ <p class="field-hint">PR bodies use <code>{phrase} PROJ-123</code> instead of <code>Closes #N</code>.</p>
392
+ </div>
332
393
  </div>
333
- <div class="settings-field">
334
- <label class="field-label" for="settings-jira-token">API token</label>
335
- <input id="settings-jira-token" class="input" type="password" name="jiraApiToken" autocomplete="new-password" placeholder="Leave blank to keep existing">
336
- <p class="field-hint" id="settings-jira-token-hint">Create a token at id.atlassian.com → Security → API tokens. Stored in <code>.acdev/.env</code> (not committed).</p>
394
+ <div class="jira-actions">
395
+ <button id="jira-test-btn" type="button" class="btn btn-secondary">Test connection</button>
396
+ <span id="jira-status" class="jira-status" role="status"></span>
337
397
  </div>
338
- <div class="settings-field settings-field-full">
339
- <label class="field-label" for="settings-jira-pr-phrase">PR link phrase</label>
340
- <input id="settings-jira-pr-phrase" class="input" type="text" name="jiraPrLinkPhrase" autocomplete="off" placeholder="Relates to">
341
- <p class="field-hint">PR bodies use <code>{phrase} PROJ-123</code> instead of <code>Closes #N</code>.</p>
398
+ </div>
399
+ </div>
400
+ </div>
401
+
402
+ <div
403
+ class="settings-panel hidden"
404
+ role="tabpanel"
405
+ id="settings-panel-auth"
406
+ data-settings-panel="auth"
407
+ aria-labelledby="settings-tab-auth"
408
+ >
409
+ <p class="settings-lead">
410
+ Tokens save to <code>.acdev/.env</code> (not committed) when you click <strong>Save settings</strong>. Leave a field blank to keep the current secret.
411
+ </p>
412
+ <div class="stack">
413
+ <div class="card settings-card">
414
+ <div class="rules-heading">GitHub / PR auth</div>
415
+ <p id="settings-gh-status" class="auth-status" role="status">Checking GitHub CLI…</p>
416
+ <p id="settings-gh-origin" class="field-hint"></p>
417
+ <div class="settings-grid">
418
+ <div class="settings-field settings-field-full">
419
+ <label class="field-label" for="settings-gh-token">Personal access token</label>
420
+ <input id="settings-gh-token" class="input" type="password" name="ghToken" autocomplete="new-password" placeholder="Leave blank to keep existing">
421
+ <button type="button" class="auth-clear hidden" id="settings-gh-token-clear">Clear saved token</button>
422
+ <p class="field-hint" id="settings-gh-token-hint">
423
+ If <code>gh auth login</code> already works on this machine, leave this blank. Otherwise paste a PAT (<code>repo</code> and pull-request scopes). Stored as <code>GH_TOKEN</code> so <code>gh</code> can auth without a browser. SSH remotes can push/fetch git but do <strong>not</strong> authenticate <code>gh</code> for issues or PRs.
424
+ </p>
425
+ </div>
342
426
  </div>
343
427
  </div>
344
- <div class="jira-actions">
345
- <button id="jira-test-btn" type="button" class="btn btn-secondary">Test connection</button>
346
- <span id="jira-status" class="jira-status" role="status"></span>
428
+
429
+ <div class="card settings-card">
430
+ <div class="rules-heading">Claude authentication</div>
431
+ <p id="settings-claude-status" class="auth-status" role="status">Checking Claude auth…</p>
432
+ <div class="settings-grid">
433
+ <div class="settings-field settings-field-full">
434
+ <label class="field-label" for="settings-anthropic-key">Anthropic API key</label>
435
+ <input id="settings-anthropic-key" class="input" type="password" name="anthropicApiKey" autocomplete="new-password" placeholder="Leave blank to keep existing">
436
+ <button type="button" class="auth-clear hidden" id="settings-anthropic-key-clear">Clear saved API key</button>
437
+ <p class="field-hint" id="settings-anthropic-key-hint">
438
+ Console / pay-as-you-go billing. Takes precedence over subscription login. Stored as <code>ANTHROPIC_API_KEY</code>. Clear or omit the key to use <code>claude auth login</code> instead.
439
+ </p>
440
+ </div>
441
+ <div class="settings-field settings-field-full">
442
+ <label class="field-label" for="settings-claude-oauth">Claude Code OAuth token (optional)</label>
443
+ <input id="settings-claude-oauth" class="input" type="password" name="claudeOauthToken" autocomplete="new-password" placeholder="Leave blank to keep existing">
444
+ <button type="button" class="auth-clear hidden" id="settings-claude-oauth-clear">Clear saved OAuth token</button>
445
+ <p class="field-hint" id="settings-claude-oauth-hint">
446
+ From <code>claude setup-token</code> for non-interactive subscription auth. Stored as <code>CLAUDE_CODE_OAUTH_TOKEN</code>. Settings cannot complete browser OAuth — that still needs <code>claude auth login</code> on this host.
447
+ </p>
448
+ </div>
449
+ </div>
347
450
  </div>
348
451
  </div>
349
452
  </div>
350
- </section>
351
453
 
352
- <section>
353
- <div class="section-label">Configuration</div>
354
- <p class="settings-lead">
355
- Changes save to <code>.acdev/config.json</code> and apply to the next job — no restart needed. Includes post-PR rules below.
356
- </p>
357
- <form id="settings-form" class="stack stack-lg" novalidate>
454
+ <div
455
+ class="settings-panel hidden"
456
+ role="tabpanel"
457
+ id="settings-panel-rules"
458
+ data-settings-panel="rules"
459
+ aria-labelledby="settings-tab-rules"
460
+ >
461
+ <p class="settings-lead">
462
+ Optional automation after a PR is opened. Failures are logged as warnings and never undo a successful PR.
463
+ </p>
358
464
  <div class="card settings-card">
359
- <div class="rules-heading">Rules</div>
360
- <p class="field-hint">
361
- Optional automation after a PR is opened. Failures are logged as warnings and never undo a successful PR.
362
- </p>
363
-
364
- <div id="jira-rules-panel" class="rules-block hidden">
465
+ <div id="jira-rules-panel" class="rules-block">
365
466
  <div class="field-label">Jira — after PR opened</div>
366
467
  <p class="field-hint">
367
- Moves the ticket via a workflow transition whose target status name matches (case-insensitive), e.g. <code>In Review</code> or <code>Code Review</code>.
468
+ Move status transitions to a workflow status whose name matches (case-insensitive). Add label uses the Jira labels API. Close transitions to a Done-category (or Done/Closed/Resolved-like) status.
368
469
  </p>
369
470
  <div class="settings-grid">
370
471
  <div class="settings-field settings-field-full">
371
472
  <label class="rule-toggle">
372
473
  <input id="settings-jira-rule-enabled" type="checkbox" name="jiraRuleEnabled">
373
- <span>Enable status move</span>
474
+ <span>Enable action</span>
374
475
  </label>
375
476
  </div>
376
- <div class="settings-field settings-field-full">
477
+ <div class="settings-field">
478
+ <label class="field-label" for="settings-jira-rule-action">Action</label>
479
+ <select id="settings-jira-rule-action" class="input" name="jiraRuleAction">
480
+ <option value="none">None</option>
481
+ <option value="set_status">Move status</option>
482
+ <option value="add_label">Add label</option>
483
+ <option value="close_issue">Close issue</option>
484
+ </select>
485
+ </div>
486
+ <div class="settings-field" id="settings-jira-rule-status-field">
377
487
  <label class="field-label" for="settings-jira-rule-status">Target status name</label>
378
488
  <input id="settings-jira-rule-status" class="input" type="text" name="jiraRuleTargetStatus" autocomplete="off" placeholder="In Review">
379
489
  <p class="field-hint">Must match a status reachable from the issue’s current workflow (exact name, any casing).</p>
380
490
  </div>
491
+ <div class="settings-field" id="settings-jira-rule-label-field">
492
+ <label class="field-label" for="settings-jira-rule-label">Label</label>
493
+ <input id="settings-jira-rule-label" class="input" type="text" name="jiraRuleLabel" autocomplete="off" placeholder="in-review">
494
+ <p class="field-hint">Used when action is Add label.</p>
495
+ </div>
381
496
  </div>
382
497
  </div>
383
498
 
384
499
  <div id="github-rules-panel" class="rules-block">
385
500
  <div class="field-label">GitHub Issues — after PR opened</div>
386
501
  <p class="field-hint">
387
- GitHub issues don’t have Jira-like board statuses by default. Use a label (e.g. <code>in-review</code>) or close the issue. Projects board API is not supported in v1.
502
+ GitHub issues don’t have Jira-like workflow statuses. Move status applies a label named after the target status (e.g. <code>In Review</code>). You can also add a different label or close the issue. Projects board status is not supported.
388
503
  </p>
389
504
  <div class="settings-grid">
390
505
  <div class="settings-field settings-field-full">
@@ -397,10 +512,16 @@
397
512
  <label class="field-label" for="settings-github-rule-action">Action</label>
398
513
  <select id="settings-github-rule-action" class="input" name="githubRuleAction">
399
514
  <option value="none">None</option>
515
+ <option value="set_status">Move status</option>
400
516
  <option value="add_label">Add label</option>
401
517
  <option value="close_issue">Close issue</option>
402
518
  </select>
403
519
  </div>
520
+ <div class="settings-field" id="settings-github-rule-status-field">
521
+ <label class="field-label" for="settings-github-rule-status">Target status name</label>
522
+ <input id="settings-github-rule-status" class="input" type="text" name="githubRuleTargetStatus" autocomplete="off" placeholder="In Review">
523
+ <p class="field-hint">Applied as an issue label (exact name). Create the label in the repo first if it doesn’t exist.</p>
524
+ </div>
404
525
  <div class="settings-field" id="settings-github-rule-label-field">
405
526
  <label class="field-label" for="settings-github-rule-label">Label</label>
406
527
  <input id="settings-github-rule-label" class="input" type="text" name="githubRuleLabel" autocomplete="off" placeholder="in-review">
@@ -409,7 +530,18 @@
409
530
  </div>
410
531
  </div>
411
532
  </div>
533
+ </div>
412
534
 
535
+ <div
536
+ class="settings-panel hidden"
537
+ role="tabpanel"
538
+ id="settings-panel-config"
539
+ data-settings-panel="config"
540
+ aria-labelledby="settings-tab-config"
541
+ >
542
+ <p class="settings-lead">
543
+ Changes save to <code>.acdev/config.json</code> and apply to the next job — no restart needed.
544
+ </p>
413
545
  <div class="card settings-card">
414
546
  <div class="settings-grid">
415
547
  <div class="settings-field">
@@ -421,7 +553,7 @@
421
553
  <div class="settings-field">
422
554
  <label class="field-label" for="settings-model">Model</label>
423
555
  <select id="settings-model" class="input" name="model"></select>
424
- <p class="field-hint">Synced with the sidebar model toggle.</p>
556
+ <p class="field-hint" id="settings-model-hint">Claude model for agent runs.</p>
425
557
  </div>
426
558
 
427
559
  <div class="settings-field">
@@ -448,14 +580,14 @@
448
580
  <p class="field-hint">Tools the agent may use. Defaults: Read, Glob, Grep, Edit, Write, Bash.</p>
449
581
  </div>
450
582
  </div>
451
-
452
- <div class="settings-footer">
453
- <p id="settings-feedback" class="feedback hidden" role="status"></p>
454
- <button id="settings-save-btn" type="submit" class="btn btn-primary">Save settings</button>
455
- </div>
456
583
  </div>
457
- </form>
458
- </section>
584
+ </div>
585
+
586
+ <div class="settings-footer">
587
+ <p id="settings-feedback" class="feedback hidden" role="status"></p>
588
+ <button id="settings-save-btn" type="submit" class="btn btn-primary">Save settings</button>
589
+ </div>
590
+ </form>
459
591
  </div>
460
592
  </section>
461
593
  </div>
package/public/styles.css CHANGED
@@ -237,30 +237,16 @@ a { color: var(--primary); text-underline-offset: 3px; }
237
237
  letter-spacing: 0.08em;
238
238
  text-transform: uppercase;
239
239
  color: var(--text-muted);
240
- margin-bottom: 8px;
241
- }
242
-
243
- .model-toggle {
244
- display: flex;
245
- gap: 6px;
240
+ margin-bottom: 6px;
246
241
  }
247
242
 
248
- .model-btn {
249
- flex: 1;
250
- padding: 8px 0;
251
- font-size: 12px;
252
- font-weight: 600;
253
- border: none;
254
- border-radius: 8px;
255
- font-family: inherit;
256
- background: var(--surface-2);
243
+ .model-current {
244
+ font-family: 'JetBrains Mono', ui-monospace, monospace;
245
+ font-size: 11px;
246
+ font-weight: 500;
257
247
  color: var(--text);
258
- cursor: pointer;
259
- }
260
-
261
- .model-btn.active {
262
- background: var(--primary);
263
- color: var(--btn-on-primary);
248
+ line-height: 1.35;
249
+ word-break: break-all;
264
250
  }
265
251
 
266
252
  .repo-meta {
@@ -1722,6 +1708,42 @@ body.diff-fs-open {
1722
1708
  }
1723
1709
 
1724
1710
  /* —— Settings —— */
1711
+ .settings-tabs {
1712
+ display: flex;
1713
+ flex-wrap: wrap;
1714
+ gap: 6px;
1715
+ }
1716
+
1717
+ .settings-tab {
1718
+ border: 1px solid var(--border);
1719
+ background: var(--surface);
1720
+ color: var(--text-muted);
1721
+ font-family: inherit;
1722
+ font-size: 12px;
1723
+ font-weight: 600;
1724
+ padding: 7px 12px;
1725
+ border-radius: 8px;
1726
+ cursor: pointer;
1727
+ transition: background 0.12s ease, border-color 0.12s ease, color 0.12s ease;
1728
+ }
1729
+
1730
+ .settings-tab:hover {
1731
+ background: var(--surface-2);
1732
+ color: var(--text);
1733
+ }
1734
+
1735
+ .settings-tab[aria-selected="true"] {
1736
+ background: var(--primary-100);
1737
+ border-color: transparent;
1738
+ color: var(--primary);
1739
+ }
1740
+
1741
+ .settings-panel {
1742
+ display: flex;
1743
+ flex-direction: column;
1744
+ gap: 0;
1745
+ }
1746
+
1725
1747
  .settings-lead {
1726
1748
  margin: 0 0 14px;
1727
1749
  font-size: 13px;
@@ -1871,6 +1893,38 @@ body.diff-fs-open {
1871
1893
  color: var(--danger, #b42318);
1872
1894
  }
1873
1895
 
1896
+ .auth-status {
1897
+ margin: 0;
1898
+ font-size: 13px;
1899
+ color: var(--text-muted);
1900
+ }
1901
+
1902
+ .auth-status.ok {
1903
+ color: var(--success, #1a7f4b);
1904
+ }
1905
+
1906
+ .auth-status.err {
1907
+ color: var(--danger, #b42318);
1908
+ }
1909
+
1910
+ .auth-clear {
1911
+ display: inline-block;
1912
+ margin-top: 6px;
1913
+ padding: 0;
1914
+ border: 0;
1915
+ background: none;
1916
+ font: inherit;
1917
+ font-size: 12px;
1918
+ color: var(--text-muted);
1919
+ cursor: pointer;
1920
+ text-decoration: underline;
1921
+ text-underline-offset: 2px;
1922
+ }
1923
+
1924
+ .auth-clear:hover {
1925
+ color: var(--text);
1926
+ }
1927
+
1874
1928
  .rules-heading {
1875
1929
  font-size: 12px;
1876
1930
  font-weight: 700;
@@ -1886,6 +1940,12 @@ body.diff-fs-open {
1886
1940
  padding-top: 4px;
1887
1941
  }
1888
1942
 
1943
+ .rules-block + .rules-block {
1944
+ margin-top: 16px;
1945
+ padding-top: 16px;
1946
+ border-top: 1px solid var(--border);
1947
+ }
1948
+
1889
1949
  .rule-toggle {
1890
1950
  display: inline-flex;
1891
1951
  align-items: center;
@@ -1,11 +1,16 @@
1
1
  /**
2
- * Best-effort post-PR ticket rules (Jira transition / GitHub label or close).
2
+ * Best-effort post-PR ticket rules (Jira / GitHub: move status, add label, or close).
3
3
  * Failures must never undo a successful PR open.
4
4
  */
5
5
 
6
6
  import { normalizeGithubRules, normalizeJiraRules } from './config.js';
7
7
  import { addIssueLabel, closeIssue } from './github.js';
8
- import { resolveJiraCredentials, transitionJiraIssue } from './jira.js';
8
+ import {
9
+ addJiraIssueLabel,
10
+ closeJiraIssue,
11
+ resolveJiraCredentials,
12
+ transitionJiraIssue,
13
+ } from './jira.js';
9
14
 
10
15
  /**
11
16
  * @param {{
@@ -21,6 +26,8 @@ import { resolveJiraCredentials, transitionJiraIssue } from './jira.js';
21
26
  * appendLog: (job: object, type: string, payload: string) => object,
22
27
  * deps?: {
23
28
  * transitionJiraIssue?: typeof transitionJiraIssue,
29
+ * addJiraIssueLabel?: typeof addJiraIssueLabel,
30
+ * closeJiraIssue?: typeof closeJiraIssue,
24
31
  * addIssueLabel?: typeof addIssueLabel,
25
32
  * closeIssue?: typeof closeIssue,
26
33
  * resolveJiraCredentials?: typeof resolveJiraCredentials,
@@ -36,6 +43,8 @@ export async function applyAfterPrOpenedRules({
36
43
  deps = {},
37
44
  }) {
38
45
  const doTransition = deps.transitionJiraIssue || transitionJiraIssue;
46
+ const doAddJiraLabel = deps.addJiraIssueLabel || addJiraIssueLabel;
47
+ const doCloseJira = deps.closeJiraIssue || closeJiraIssue;
39
48
  const doAddLabel = deps.addIssueLabel || addIssueLabel;
40
49
  const doClose = deps.closeIssue || closeIssue;
41
50
  const doResolveCreds = deps.resolveJiraCredentials || resolveJiraCredentials;
@@ -46,7 +55,7 @@ export async function applyAfterPrOpenedRules({
46
55
  if (source === 'jira') {
47
56
  const rules = normalizeJiraRules(config.jiraRules);
48
57
  const rule = rules.afterPrOpened;
49
- if (!rule.enabled) {
58
+ if (!rule.enabled || rule.action === 'none') {
50
59
  return { applied: false };
51
60
  }
52
61
  if (!job.jiraKey) {
@@ -58,16 +67,47 @@ export async function applyAfterPrOpenedRules({
58
67
 
59
68
  const creds = doResolveCreds({ configBaseUrl: config.jiraBaseUrl });
60
69
  if ('error' in creds) {
61
- const msg = `Post-PR Jira transition failed: ${creds.error}`;
70
+ const msg = `Post-PR Jira rule failed: ${creds.error}`;
62
71
  console.warn(`[acdev] ${msg}`);
63
72
  appendLog(job, 'warn', msg);
64
73
  return { applied: false, message: msg };
65
74
  }
66
75
 
67
- const result = await doTransition(job.jiraKey, rule.targetStatus, creds);
68
- const msg = `Moved Jira ${result.key} to "${result.statusName}" after PR opened`;
69
- appendLog(job, 'info', msg);
70
- return { applied: true, message: msg };
76
+ if (rule.action === 'set_status') {
77
+ const status = String(rule.targetStatus || '').trim();
78
+ if (!status) {
79
+ const msg = 'Post-PR Jira rule skipped: set_status requires a targetStatus name';
80
+ console.warn(`[acdev] ${msg}`);
81
+ appendLog(job, 'warn', msg);
82
+ return { applied: false, message: msg };
83
+ }
84
+ const result = await doTransition(job.jiraKey, status, creds);
85
+ const msg = `Moved Jira ${result.key} to "${result.statusName}" after PR opened`;
86
+ appendLog(job, 'info', msg);
87
+ return { applied: true, message: msg };
88
+ }
89
+
90
+ if (rule.action === 'add_label') {
91
+ if (!rule.label) {
92
+ const msg = 'Post-PR Jira rule skipped: add_label requires a label name';
93
+ console.warn(`[acdev] ${msg}`);
94
+ appendLog(job, 'warn', msg);
95
+ return { applied: false, message: msg };
96
+ }
97
+ const result = await doAddJiraLabel(job.jiraKey, rule.label, creds);
98
+ const msg = `Added label "${result.label}" to Jira ${result.key} after PR opened`;
99
+ appendLog(job, 'info', msg);
100
+ return { applied: true, message: msg };
101
+ }
102
+
103
+ if (rule.action === 'close_issue') {
104
+ const result = await doCloseJira(job.jiraKey, creds);
105
+ const msg = `Closed Jira ${result.key} (status "${result.statusName}") after PR opened`;
106
+ appendLog(job, 'info', msg);
107
+ return { applied: true, message: msg };
108
+ }
109
+
110
+ return { applied: false };
71
111
  }
72
112
 
73
113
  const rules = normalizeGithubRules(config.githubRules);
@@ -85,6 +125,21 @@ export async function applyAfterPrOpenedRules({
85
125
 
86
126
  const cwd = job.worktreePath || repoRoot;
87
127
 
128
+ if (rule.action === 'set_status') {
129
+ const status = String(rule.targetStatus || '').trim();
130
+ if (!status) {
131
+ const msg = 'Post-PR GitHub rule skipped: set_status requires a targetStatus name';
132
+ console.warn(`[acdev] ${msg}`);
133
+ appendLog(job, 'warn', msg);
134
+ return { applied: false, message: msg };
135
+ }
136
+ // GitHub issues have no workflow statuses; model status as an issue label.
137
+ await doAddLabel(job.issueNumber, status, cwd);
138
+ const msg = `Moved GitHub issue #${job.issueNumber} status to "${status}" (label) after PR opened`;
139
+ appendLog(job, 'info', msg);
140
+ return { applied: true, message: msg };
141
+ }
142
+
88
143
  if (rule.action === 'add_label') {
89
144
  if (!rule.label) {
90
145
  const msg = 'Post-PR GitHub rule skipped: add_label requires a label name';
@@ -79,3 +79,33 @@ export function checkClaudeAuth() {
79
79
 
80
80
  return { ok: false, reason: 'claude-not-authenticated' };
81
81
  }
82
+
83
+ /**
84
+ * Method string for Settings / API — never a secret.
85
+ * @param {ClaudeAuthResult} result
86
+ * @returns {ClaudeAuthMethod | 'missing'}
87
+ */
88
+ export function publicClaudeAuthMethod(result) {
89
+ return result.ok ? result.method : 'missing';
90
+ }
91
+
92
+ /**
93
+ * Human-readable startup error for a failed {@link checkClaudeAuth}.
94
+ * @param {ClaudeAuthResult} [_result]
95
+ */
96
+ export function formatClaudeAuthError(_result) {
97
+ return [
98
+ '✖ No Anthropic / Claude Code authentication found.',
99
+ '',
100
+ 'Authenticate with one of:',
101
+ ' 1. Claude Pro/Max subscription (browser): install Claude Code, then run',
102
+ ' `claude auth login`. Settings cannot complete browser OAuth — that still',
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`.',
110
+ ].join('\n');
111
+ }