acdev 1.0.2 → 1.0.4

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>
@@ -103,6 +100,12 @@
103
100
  <!-- OVERVIEW -->
104
101
  <section class="view" id="view-overview" data-view-panel="overview">
105
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>
106
109
  <section>
107
110
  <div class="section-label">Add issues</div>
108
111
  <div class="card enqueue-card">
@@ -304,87 +307,205 @@
304
307
  <!-- SETTINGS -->
305
308
  <section class="view hidden" id="view-settings" data-view-panel="settings">
306
309
  <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>
310
+ <div
311
+ class="settings-tabs"
312
+ id="settings-tabs"
313
+ role="tablist"
314
+ aria-label="Settings sections"
315
+ >
316
+ <button
317
+ type="button"
318
+ class="settings-tab"
319
+ role="tab"
320
+ id="settings-tab-ticket"
321
+ data-settings-tab="ticket"
322
+ aria-controls="settings-panel-ticket"
323
+ aria-selected="true"
324
+ >Ticket source</button>
325
+ <button
326
+ type="button"
327
+ class="settings-tab"
328
+ role="tab"
329
+ id="settings-tab-auth"
330
+ data-settings-tab="auth"
331
+ aria-controls="settings-panel-auth"
332
+ aria-selected="false"
333
+ tabindex="-1"
334
+ >Authentication</button>
335
+ <button
336
+ type="button"
337
+ class="settings-tab"
338
+ role="tab"
339
+ id="settings-tab-rules"
340
+ data-settings-tab="rules"
341
+ aria-controls="settings-panel-rules"
342
+ aria-selected="false"
343
+ tabindex="-1"
344
+ >Rules</button>
345
+ <button
346
+ type="button"
347
+ class="settings-tab"
348
+ role="tab"
349
+ id="settings-tab-config"
350
+ data-settings-tab="config"
351
+ aria-controls="settings-panel-config"
352
+ aria-selected="false"
353
+ tabindex="-1"
354
+ >Configuration</button>
355
+ </div>
321
356
 
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>
357
+ <form id="settings-form" class="stack stack-lg" novalidate>
358
+ <div
359
+ class="settings-panel"
360
+ role="tabpanel"
361
+ id="settings-panel-ticket"
362
+ data-settings-panel="ticket"
363
+ aria-labelledby="settings-tab-ticket"
364
+ >
365
+ <p class="settings-lead">
366
+ Choose where tickets come from. Pull requests are always created on the local repo’s GitHub remote via <code>gh</code>.
367
+ </p>
368
+ <div class="card settings-card">
369
+ <div class="settings-field settings-field-full">
370
+ <div class="field-label">Ticket source</div>
371
+ <div class="source-toggle" id="settings-ticket-source" role="group" aria-label="Ticket source">
372
+ <button type="button" class="source-btn active" data-source="github">GitHub Issues</button>
373
+ <button type="button" class="source-btn" data-source="jira">Jira</button>
328
374
  </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">
375
+ <p class="field-hint">Switch anytime — queued jobs keep their original source.</p>
376
+ </div>
377
+
378
+ <div id="jira-connect-panel" class="jira-panel hidden">
379
+ <div class="settings-grid">
380
+ <div class="settings-field settings-field-full">
381
+ <label class="field-label" for="settings-jira-base-url">Jira Cloud base URL</label>
382
+ <input id="settings-jira-base-url" class="input" type="url" name="jiraBaseUrl" autocomplete="off" placeholder="https://your-domain.atlassian.net">
383
+ <p class="field-hint">Your Atlassian Cloud site (Jira Server/Data Center is not supported).</p>
384
+ </div>
385
+ <div class="settings-field">
386
+ <label class="field-label" for="settings-jira-email">Email</label>
387
+ <input id="settings-jira-email" class="input" type="email" name="jiraEmail" autocomplete="username" placeholder="you@company.com">
388
+ </div>
389
+ <div class="settings-field">
390
+ <label class="field-label" for="settings-jira-token">API token</label>
391
+ <input id="settings-jira-token" class="input" type="password" name="jiraApiToken" autocomplete="new-password" placeholder="Leave blank to keep existing">
392
+ <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>
393
+ </div>
394
+ <div class="settings-field settings-field-full">
395
+ <label class="field-label" for="settings-jira-pr-phrase">PR link phrase</label>
396
+ <input id="settings-jira-pr-phrase" class="input" type="text" name="jiraPrLinkPhrase" autocomplete="off" placeholder="Relates to">
397
+ <p class="field-hint">PR bodies use <code>{phrase} PROJ-123</code> instead of <code>Closes #N</code>.</p>
398
+ </div>
332
399
  </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>
400
+ <div class="jira-actions">
401
+ <button id="jira-test-btn" type="button" class="btn btn-secondary">Test connection</button>
402
+ <span id="jira-status" class="jira-status" role="status"></span>
337
403
  </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>
404
+ </div>
405
+ </div>
406
+ </div>
407
+
408
+ <div
409
+ class="settings-panel hidden"
410
+ role="tabpanel"
411
+ id="settings-panel-auth"
412
+ data-settings-panel="auth"
413
+ aria-labelledby="settings-tab-auth"
414
+ >
415
+ <p class="settings-lead">
416
+ 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.
417
+ </p>
418
+ <div class="stack">
419
+ <div class="card settings-card">
420
+ <div class="rules-heading">GitHub / PR auth</div>
421
+ <p id="settings-gh-status" class="auth-status" role="status">Checking GitHub CLI…</p>
422
+ <p id="settings-gh-origin" class="field-hint"></p>
423
+ <div class="settings-grid">
424
+ <div class="settings-field settings-field-full">
425
+ <label class="field-label" for="settings-gh-token">Personal access token</label>
426
+ <input id="settings-gh-token" class="input" type="password" name="ghToken" autocomplete="new-password" placeholder="Leave blank to keep existing">
427
+ <button type="button" class="auth-clear hidden" id="settings-gh-token-clear">Clear saved token</button>
428
+ <p class="field-hint" id="settings-gh-token-hint">
429
+ 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.
430
+ </p>
431
+ </div>
342
432
  </div>
343
433
  </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>
434
+
435
+ <div class="card settings-card">
436
+ <div class="rules-heading">Claude authentication</div>
437
+ <p id="settings-claude-status" class="auth-status" role="status">Checking Claude auth…</p>
438
+ <div class="settings-grid">
439
+ <div class="settings-field settings-field-full">
440
+ <label class="field-label" for="settings-anthropic-key">Anthropic API key</label>
441
+ <input id="settings-anthropic-key" class="input" type="password" name="anthropicApiKey" autocomplete="new-password" placeholder="Leave blank to keep existing">
442
+ <button type="button" class="auth-clear hidden" id="settings-anthropic-key-clear">Clear saved API key</button>
443
+ <p class="field-hint" id="settings-anthropic-key-hint">
444
+ 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.
445
+ </p>
446
+ </div>
447
+ <div class="settings-field settings-field-full">
448
+ <label class="field-label" for="settings-claude-oauth">Claude Code OAuth token (optional)</label>
449
+ <input id="settings-claude-oauth" class="input" type="password" name="claudeOauthToken" autocomplete="new-password" placeholder="Leave blank to keep existing">
450
+ <button type="button" class="auth-clear hidden" id="settings-claude-oauth-clear">Clear saved OAuth token</button>
451
+ <p class="field-hint" id="settings-claude-oauth-hint">
452
+ 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.
453
+ </p>
454
+ </div>
455
+ </div>
347
456
  </div>
348
457
  </div>
349
458
  </div>
350
- </section>
351
459
 
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>
460
+ <div
461
+ class="settings-panel hidden"
462
+ role="tabpanel"
463
+ id="settings-panel-rules"
464
+ data-settings-panel="rules"
465
+ aria-labelledby="settings-tab-rules"
466
+ >
467
+ <p class="settings-lead">
468
+ Optional automation after a PR is opened. Failures are logged as warnings and never undo a successful PR.
469
+ </p>
358
470
  <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">
471
+ <div id="jira-rules-panel" class="rules-block">
365
472
  <div class="field-label">Jira — after PR opened</div>
366
473
  <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>.
474
+ 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
475
  </p>
369
476
  <div class="settings-grid">
370
477
  <div class="settings-field settings-field-full">
371
478
  <label class="rule-toggle">
372
479
  <input id="settings-jira-rule-enabled" type="checkbox" name="jiraRuleEnabled">
373
- <span>Enable status move</span>
480
+ <span>Enable action</span>
374
481
  </label>
375
482
  </div>
376
- <div class="settings-field settings-field-full">
483
+ <div class="settings-field">
484
+ <label class="field-label" for="settings-jira-rule-action">Action</label>
485
+ <select id="settings-jira-rule-action" class="input" name="jiraRuleAction">
486
+ <option value="none">None</option>
487
+ <option value="set_status">Move status</option>
488
+ <option value="add_label">Add label</option>
489
+ <option value="close_issue">Close issue</option>
490
+ </select>
491
+ </div>
492
+ <div class="settings-field" id="settings-jira-rule-status-field">
377
493
  <label class="field-label" for="settings-jira-rule-status">Target status name</label>
378
494
  <input id="settings-jira-rule-status" class="input" type="text" name="jiraRuleTargetStatus" autocomplete="off" placeholder="In Review">
379
495
  <p class="field-hint">Must match a status reachable from the issue’s current workflow (exact name, any casing).</p>
380
496
  </div>
497
+ <div class="settings-field" id="settings-jira-rule-label-field">
498
+ <label class="field-label" for="settings-jira-rule-label">Label</label>
499
+ <input id="settings-jira-rule-label" class="input" type="text" name="jiraRuleLabel" autocomplete="off" placeholder="in-review">
500
+ <p class="field-hint">Used when action is Add label.</p>
501
+ </div>
381
502
  </div>
382
503
  </div>
383
504
 
384
505
  <div id="github-rules-panel" class="rules-block">
385
506
  <div class="field-label">GitHub Issues — after PR opened</div>
386
507
  <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.
508
+ 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
509
  </p>
389
510
  <div class="settings-grid">
390
511
  <div class="settings-field settings-field-full">
@@ -397,10 +518,16 @@
397
518
  <label class="field-label" for="settings-github-rule-action">Action</label>
398
519
  <select id="settings-github-rule-action" class="input" name="githubRuleAction">
399
520
  <option value="none">None</option>
521
+ <option value="set_status">Move status</option>
400
522
  <option value="add_label">Add label</option>
401
523
  <option value="close_issue">Close issue</option>
402
524
  </select>
403
525
  </div>
526
+ <div class="settings-field" id="settings-github-rule-status-field">
527
+ <label class="field-label" for="settings-github-rule-status">Target status name</label>
528
+ <input id="settings-github-rule-status" class="input" type="text" name="githubRuleTargetStatus" autocomplete="off" placeholder="In Review">
529
+ <p class="field-hint">Applied as an issue label (exact name). Create the label in the repo first if it doesn’t exist.</p>
530
+ </div>
404
531
  <div class="settings-field" id="settings-github-rule-label-field">
405
532
  <label class="field-label" for="settings-github-rule-label">Label</label>
406
533
  <input id="settings-github-rule-label" class="input" type="text" name="githubRuleLabel" autocomplete="off" placeholder="in-review">
@@ -409,7 +536,18 @@
409
536
  </div>
410
537
  </div>
411
538
  </div>
539
+ </div>
412
540
 
541
+ <div
542
+ class="settings-panel hidden"
543
+ role="tabpanel"
544
+ id="settings-panel-config"
545
+ data-settings-panel="config"
546
+ aria-labelledby="settings-tab-config"
547
+ >
548
+ <p class="settings-lead">
549
+ Changes save to <code>.acdev/config.json</code> and apply to the next job — no restart needed.
550
+ </p>
413
551
  <div class="card settings-card">
414
552
  <div class="settings-grid">
415
553
  <div class="settings-field">
@@ -421,7 +559,7 @@
421
559
  <div class="settings-field">
422
560
  <label class="field-label" for="settings-model">Model</label>
423
561
  <select id="settings-model" class="input" name="model"></select>
424
- <p class="field-hint">Synced with the sidebar model toggle.</p>
562
+ <p class="field-hint" id="settings-model-hint">Claude model for agent runs.</p>
425
563
  </div>
426
564
 
427
565
  <div class="settings-field">
@@ -448,14 +586,14 @@
448
586
  <p class="field-hint">Tools the agent may use. Defaults: Read, Glob, Grep, Edit, Write, Bash.</p>
449
587
  </div>
450
588
  </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
589
  </div>
457
- </form>
458
- </section>
590
+ </div>
591
+
592
+ <div class="settings-footer">
593
+ <p id="settings-feedback" class="feedback hidden" role="status"></p>
594
+ <button id="settings-save-btn" type="submit" class="btn btn-primary">Save settings</button>
595
+ </div>
596
+ </form>
459
597
  </div>
460
598
  </section>
461
599
  </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,62 @@ 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-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
+
1934
+ .auth-clear {
1935
+ display: inline-block;
1936
+ margin-top: 6px;
1937
+ padding: 0;
1938
+ border: 0;
1939
+ background: none;
1940
+ font: inherit;
1941
+ font-size: 12px;
1942
+ color: var(--text-muted);
1943
+ cursor: pointer;
1944
+ text-decoration: underline;
1945
+ text-underline-offset: 2px;
1946
+ }
1947
+
1948
+ .auth-clear:hover {
1949
+ color: var(--text);
1950
+ }
1951
+
1874
1952
  .rules-heading {
1875
1953
  font-size: 12px;
1876
1954
  font-weight: 700;
@@ -1886,6 +1964,12 @@ body.diff-fs-open {
1886
1964
  padding-top: 4px;
1887
1965
  }
1888
1966
 
1967
+ .rules-block + .rules-block {
1968
+ margin-top: 16px;
1969
+ padding-top: 16px;
1970
+ border-top: 1px solid var(--border);
1971
+ }
1972
+
1889
1973
  .rule-toggle {
1890
1974
  display: inline-flex;
1891
1975
  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';