@exaudeus/workrail 3.43.0 → 3.45.0

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.
@@ -1081,6 +1081,16 @@ function makeSpawnAgentTool(sessionId, ctx, apiKey, thisWorkrailSessionId, curre
1081
1081
  notes: childResult.message,
1082
1082
  };
1083
1083
  }
1084
+ else if (childResult._tag === 'stuck') {
1085
+ resultObj = {
1086
+ childSessionId,
1087
+ outcome: 'stuck',
1088
+ notes: childResult.message,
1089
+ ...(childResult.issueSummaries !== undefined
1090
+ ? { issueSummaries: childResult.issueSummaries }
1091
+ : {}),
1092
+ };
1093
+ }
1084
1094
  else {
1085
1095
  (0, assert_never_js_1.assertNever)(childResult);
1086
1096
  }
@@ -1093,6 +1103,31 @@ function makeSpawnAgentTool(sessionId, ctx, apiKey, thisWorkrailSessionId, curre
1093
1103
  },
1094
1104
  };
1095
1105
  }
1106
+ async function writeStuckOutboxEntry(opts) {
1107
+ try {
1108
+ const outboxPath = path.join(os.homedir(), '.workrail', 'outbox.jsonl');
1109
+ await fs.mkdir(path.dirname(outboxPath), { recursive: true });
1110
+ const entry = JSON.stringify({
1111
+ id: (0, node_crypto_1.randomUUID)(),
1112
+ kind: 'stuck',
1113
+ message: `Session stuck (${opts.reason}): workflowId=${opts.workflowId}` +
1114
+ (opts.issueSummaries && opts.issueSummaries.length > 0
1115
+ ? ` -- issues: ${opts.issueSummaries.join('; ')}`
1116
+ : ''),
1117
+ timestamp: new Date().toISOString(),
1118
+ workflowId: opts.workflowId,
1119
+ reason: opts.reason,
1120
+ ...(opts.issueSummaries && opts.issueSummaries.length > 0
1121
+ ? { issueSummaries: opts.issueSummaries }
1122
+ : {}),
1123
+ });
1124
+ await fs.appendFile(outboxPath, entry + '\n');
1125
+ }
1126
+ catch (err) {
1127
+ console.warn(`[WorkflowRunner] Could not write stuck outbox entry: ` +
1128
+ `${err instanceof Error ? err.message : String(err)}`);
1129
+ }
1130
+ }
1096
1131
  async function appendIssueAsync(issuesDir, sessionId, record) {
1097
1132
  await fs.mkdir(issuesDir, { recursive: true });
1098
1133
  const filePath = path.join(issuesDir, `${sessionId}.jsonl`);
@@ -1484,6 +1519,19 @@ async function runWorkflow(trigger, ctx, apiKey, daemonRegistry, emitter, steerR
1484
1519
  };
1485
1520
  }
1486
1521
  }
1522
+ if (trigger.botIdentity) {
1523
+ try {
1524
+ await execFileAsync('git', ['-C', sessionWorkspacePath, 'config', 'user.name', trigger.botIdentity.name]);
1525
+ await execFileAsync('git', ['-C', sessionWorkspacePath, 'config', 'user.email', trigger.botIdentity.email]);
1526
+ console.log(`[WorkflowRunner] Bot identity set: sessionId=${sessionId} ` +
1527
+ `name=${trigger.botIdentity.name} email=${trigger.botIdentity.email}`);
1528
+ }
1529
+ catch (identityErr) {
1530
+ console.warn(`[WorkflowRunner] WARNING: Failed to set bot identity for sessionId=${sessionId}: ` +
1531
+ `${identityErr instanceof Error ? identityErr.message : String(identityErr)}. ` +
1532
+ `Commits will use default git config.`);
1533
+ }
1534
+ }
1487
1535
  if (firstStep.isComplete) {
1488
1536
  await fs.unlink(path.join(exports.DAEMON_SESSIONS_DIR, `${sessionId}.json`)).catch(() => { });
1489
1537
  emitter?.emit({ kind: 'session_completed', sessionId, workflowId: trigger.workflowId, outcome: 'success', detail: 'stop', ...withWorkrailSession(workrailSessionId) });
@@ -1575,7 +1623,10 @@ async function runWorkflow(trigger, ctx, apiKey, daemonRegistry, emitter, steerR
1575
1623
  });
1576
1624
  const sessionTimeoutMs = (trigger.agentConfig?.maxSessionMinutes ?? DEFAULT_SESSION_TIMEOUT_MINUTES) * 60 * 1000;
1577
1625
  const maxTurns = trigger.agentConfig?.maxTurns ?? DEFAULT_MAX_TURNS;
1626
+ const sessionStartMs = Date.now();
1627
+ void sessionStartMs;
1578
1628
  let timeoutReason = null;
1629
+ let stuckReason = null;
1579
1630
  let turnCount = 0;
1580
1631
  const unsubscribe = agent.subscribe(async (event) => {
1581
1632
  if (event.type !== 'turn_end')
@@ -1610,6 +1661,17 @@ async function runWorkflow(trigger, ctx, apiKey, daemonRegistry, emitter, steerR
1610
1661
  argsSummary: lastNToolCalls[0]?.argsSummary,
1611
1662
  ...withWorkrailSession(workrailSessionId),
1612
1663
  });
1664
+ void writeStuckOutboxEntry({
1665
+ workflowId: trigger.workflowId,
1666
+ reason: 'repeated_tool_call',
1667
+ ...(issueSummaries.length > 0 ? { issueSummaries: [...issueSummaries] } : {}),
1668
+ });
1669
+ const stuckPolicy = trigger.agentConfig?.stuckAbortPolicy ?? 'abort';
1670
+ if (stuckPolicy !== 'notify_only' && stuckReason === null && timeoutReason === null) {
1671
+ stuckReason = 'repeated_tool_call';
1672
+ agent.abort();
1673
+ return;
1674
+ }
1613
1675
  }
1614
1676
  if (maxTurns > 0 &&
1615
1677
  turnCount >= Math.floor(maxTurns * 0.8) &&
@@ -1621,6 +1683,20 @@ async function runWorkflow(trigger, ctx, apiKey, daemonRegistry, emitter, steerR
1621
1683
  detail: `${turnCount} turns used, 0 step advances (${maxTurns} turn limit)`,
1622
1684
  ...withWorkrailSession(workrailSessionId),
1623
1685
  });
1686
+ const noProgressAbortEnabled = trigger.agentConfig?.noProgressAbortEnabled ?? false;
1687
+ if (noProgressAbortEnabled) {
1688
+ void writeStuckOutboxEntry({
1689
+ workflowId: trigger.workflowId,
1690
+ reason: 'no_progress',
1691
+ ...(issueSummaries.length > 0 ? { issueSummaries: [...issueSummaries] } : {}),
1692
+ });
1693
+ const noProgressPolicy = trigger.agentConfig?.stuckAbortPolicy ?? 'abort';
1694
+ if (noProgressPolicy !== 'notify_only' && stuckReason === null && timeoutReason === null) {
1695
+ stuckReason = 'no_progress';
1696
+ agent.abort();
1697
+ return;
1698
+ }
1699
+ }
1624
1700
  }
1625
1701
  if (timeoutReason !== null) {
1626
1702
  emitter?.emit({
@@ -1680,6 +1756,26 @@ async function runWorkflow(trigger, ctx, apiKey, daemonRegistry, emitter, steerR
1680
1756
  }
1681
1757
  console.log(`[WorkflowRunner] Agent loop ended: sessionId=${sessionId} stopReason=${stopReason}${errorMessage ? ` error=${errorMessage.slice(0, 120)}` : ''}`);
1682
1758
  }
1759
+ if (stuckReason !== null) {
1760
+ emitter?.emit({
1761
+ kind: 'session_completed',
1762
+ sessionId,
1763
+ workflowId: trigger.workflowId,
1764
+ outcome: 'timeout',
1765
+ detail: stuckReason,
1766
+ ...withWorkrailSession(workrailSessionId),
1767
+ });
1768
+ if (workrailSessionId !== null)
1769
+ daemonRegistry?.unregister(workrailSessionId, 'failed');
1770
+ return {
1771
+ _tag: 'stuck',
1772
+ workflowId: trigger.workflowId,
1773
+ reason: stuckReason,
1774
+ message: `Session aborted: stuck heuristic fired (${stuckReason})`,
1775
+ stopReason: 'aborted',
1776
+ ...(issueSummaries.length > 0 ? { issueSummaries: [...issueSummaries] } : {}),
1777
+ };
1778
+ }
1683
1779
  if (timeoutReason !== null) {
1684
1780
  emitter?.emit({ kind: 'session_completed', sessionId, workflowId: trigger.workflowId, outcome: 'timeout', detail: timeoutReason, ...withWorkrailSession(workrailSessionId) });
1685
1781
  if (workrailSessionId !== null)
@@ -1687,6 +1783,7 @@ async function runWorkflow(trigger, ctx, apiKey, daemonRegistry, emitter, steerR
1687
1783
  const limitDescription = timeoutReason === 'wall_clock'
1688
1784
  ? `${trigger.agentConfig?.maxSessionMinutes ?? DEFAULT_SESSION_TIMEOUT_MINUTES} minutes`
1689
1785
  : `${trigger.agentConfig?.maxTurns ?? DEFAULT_MAX_TURNS} turns`;
1786
+ await fs.unlink(path.join(exports.DAEMON_SESSIONS_DIR, `${sessionId}.json`)).catch(() => { });
1690
1787
  return {
1691
1788
  _tag: 'timeout',
1692
1789
  workflowId: trigger.workflowId,
@@ -1711,6 +1808,7 @@ async function runWorkflow(trigger, ctx, apiKey, daemonRegistry, emitter, steerR
1711
1808
  ...(lastToolCalled !== null && { lastToolCalled }),
1712
1809
  ...(issueSummaries.length > 0 && { issueSummaries }),
1713
1810
  })}`;
1811
+ await fs.unlink(path.join(exports.DAEMON_SESSIONS_DIR, `${sessionId}.json`)).catch(() => { });
1714
1812
  return {
1715
1813
  _tag: 'error',
1716
1814
  workflowId: trigger.workflowId,
@@ -449,16 +449,16 @@
449
449
  "sha256": "5fe866e54f796975dec5d8ba9983aefd86074db212d3fccd64eed04bc9f0b3da",
450
450
  "bytes": 8011
451
451
  },
452
+ "console-ui/assets/index-BpanIvmi.js": {
453
+ "sha256": "e5c3e897dbda3f810ce737422d3f84d06b8e146c7923041fbd96b276538435c6",
454
+ "bytes": 760528
455
+ },
452
456
  "console-ui/assets/index-DGj8EsFR.css": {
453
457
  "sha256": "3bdb55ec0957928e0ebbb86a7d6b36d28f7ba7d5c0f3e236fd8f2e2aacee2fa4",
454
458
  "bytes": 60631
455
459
  },
456
- "console-ui/assets/index-Sb57DW4B.js": {
457
- "sha256": "54d09def45773f707ebf2bc17d109411a36ae1098d97d1f81a7423c69686520a",
458
- "bytes": 760528
459
- },
460
460
  "console-ui/index.html": {
461
- "sha256": "5f4e96c20ab0286c7be620fbaa3fd18728866e25b320ea8622fdda58f81b047a",
461
+ "sha256": "587aa7591502ca4dadbea5c7f5c56136d19f18d179c735abe693e9ee2c290e1e",
462
462
  "bytes": 417
463
463
  },
464
464
  "console/standalone-console.d.ts": {
@@ -550,12 +550,12 @@
550
550
  "bytes": 1512
551
551
  },
552
552
  "daemon/workflow-runner.d.ts": {
553
- "sha256": "ba96e9b4437632c760a6da9b3d70e676a2ad0a0f0a6ae037ec83419d7ffaf7a8",
554
- "bytes": 6529
553
+ "sha256": "4c67cc7a44c934469c190f11a71bd18bf0dfc31f59ab0c315b98315b96d59cce",
554
+ "bytes": 7048
555
555
  },
556
556
  "daemon/workflow-runner.js": {
557
- "sha256": "24acc5dce22f6619a05d7a5268a82f801a25d99e1fd8d7c021d2b47ce01deba8",
558
- "bytes": 88602
557
+ "sha256": "a5d74ec723ff0dce45d2335b811959ff0c6e6f8851edf399a70853f6bf127893",
558
+ "bytes": 93222
559
559
  },
560
560
  "di/container.d.ts": {
561
561
  "sha256": "003bb7fb7478d627524b9b1e76bd0a963a243794a687ff233b96dc0e33a06d9f",
@@ -1549,6 +1549,14 @@
1549
1549
  "sha256": "dd1a1bc20f1bf6da550960887c75fab4a794c19a1b79f3c89f4277145d009c2e",
1550
1550
  "bytes": 6723
1551
1551
  },
1552
+ "trigger/adapters/github-queue-poller.d.ts": {
1553
+ "sha256": "f36b01b118e6986e8f89701fc5d88fa7a1873fb38acaa67b1612895056bd5ef8",
1554
+ "bytes": 1363
1555
+ },
1556
+ "trigger/adapters/github-queue-poller.js": {
1557
+ "sha256": "73270636634fb8673c63b0c0347e6e5d7f676606d36b87a4c1aadc11f1939eb2",
1558
+ "bytes": 7340
1559
+ },
1552
1560
  "trigger/adapters/gitlab-poller.d.ts": {
1553
1561
  "sha256": "f685490fafad77194fdd0f0bbaf80dbc56730aeb344853da365199a120fbe399",
1554
1562
  "bytes": 911
@@ -1581,6 +1589,14 @@
1581
1589
  "sha256": "da358ced4e99c327493b6d3ca975a623aca21f72e68787a092b2760601801c99",
1582
1590
  "bytes": 1269
1583
1591
  },
1592
+ "trigger/github-queue-config.d.ts": {
1593
+ "sha256": "bff922c1b435da55e02b4b11d3fde9f06e999f13b8ac0494788cac4a4fc4c432",
1594
+ "bytes": 766
1595
+ },
1596
+ "trigger/github-queue-config.js": {
1597
+ "sha256": "a0ec09a7725ac1ee1adb9d41fdcb4c9545128f0b9515c6de4df9535d4bec1acc",
1598
+ "bytes": 6857
1599
+ },
1584
1600
  "trigger/index.d.ts": {
1585
1601
  "sha256": "a9cfd053714173e2a8cc5a282fd5b09a5c3f3001304d507facd0e12de9cc0733",
1586
1602
  "bytes": 735
@@ -1590,12 +1606,12 @@
1590
1606
  "bytes": 1222
1591
1607
  },
1592
1608
  "trigger/notification-service.d.ts": {
1593
- "sha256": "c78406d3748953548f7879df8ac60cecd5e42f2f3b283f777343168ce2470b8d",
1594
- "bytes": 1572
1609
+ "sha256": "25509f290a11ac9a8aa03e5d7e44011950c841436e9458eb855b94d15d036d68",
1610
+ "bytes": 1582
1595
1611
  },
1596
1612
  "trigger/notification-service.js": {
1597
- "sha256": "693f617adc30b3a4fcebeca6a78b0da1c58819001660c017a4d0901652d675b8",
1598
- "bytes": 6373
1613
+ "sha256": "9d9a5951229f2c6ffaa413a7421efa6611d9ca0ed456edf5c297a4506e84a80e",
1614
+ "bytes": 6521
1599
1615
  },
1600
1616
  "trigger/polled-event-store.d.ts": {
1601
1617
  "sha256": "2952a25804177b2389d4273bfc41192477d100bc26100683861dedf28520dec1",
@@ -1606,12 +1622,12 @@
1606
1622
  "bytes": 6968
1607
1623
  },
1608
1624
  "trigger/polling-scheduler.d.ts": {
1609
- "sha256": "c5f984df836dbc78ec51bfc78fa5a26973aff56e6f88e0cf11776057f940b2b6",
1610
- "bytes": 761
1625
+ "sha256": "009b3340f5d46a3fc22b9cd087abcf484abe34c526a46dd4f958768fb8f61c9c",
1626
+ "bytes": 792
1611
1627
  },
1612
1628
  "trigger/polling-scheduler.js": {
1613
- "sha256": "e77a5eb32a54d98e9f7178fc15d7c237809519c183eea716cd01b7b9a2aae62e",
1614
- "bytes": 10255
1629
+ "sha256": "ec3c73739c9dfbbe3b2d7a55237fd35a290d6aab7bbc30ec5408bde69b2c95e0",
1630
+ "bytes": 19769
1615
1631
  },
1616
1632
  "trigger/trigger-listener.d.ts": {
1617
1633
  "sha256": "92e971ab8f47c3c867860cffc01f54c4aae54fcc4ae199b9469210f4ce639423",
@@ -1626,20 +1642,20 @@
1626
1642
  "bytes": 2123
1627
1643
  },
1628
1644
  "trigger/trigger-router.js": {
1629
- "sha256": "605cdce397bd19e5b991fe7378faf17b4f25b4421749e1b5349413a208a4f3dd",
1630
- "bytes": 17250
1645
+ "sha256": "8a4a4699df4210b5631211e7370ed6b70d972e82321cf8c66dcc9f60661e5d2c",
1646
+ "bytes": 17750
1631
1647
  },
1632
1648
  "trigger/trigger-store.d.ts": {
1633
1649
  "sha256": "7afb05127d55bc3757a550dd15d4b797766b3fff29d1bfe76b303764b93322e7",
1634
1650
  "bytes": 1588
1635
1651
  },
1636
1652
  "trigger/trigger-store.js": {
1637
- "sha256": "f4e5c6d28db4c9c05df07b29eb627bc7a48f234a2c16ea4adfe57de24ca6bf38",
1638
- "bytes": 36591
1653
+ "sha256": "8015b54da7ab5b1cec78e1ecea099d5b457eaa5de267bb1ed52cbb75eca207c4",
1654
+ "bytes": 38148
1639
1655
  },
1640
1656
  "trigger/types.d.ts": {
1641
- "sha256": "f9ccdc2bea0bee7557362cc7b64d89ed3ab26f7cd954e05395ce4cabe536ab02",
1642
- "bytes": 2929
1657
+ "sha256": "a1336ad769dbe4760e7acd3b2a92961251492aa29245b5d906ad89011ea934fa",
1658
+ "bytes": 3587
1643
1659
  },
1644
1660
  "trigger/types.js": {
1645
1661
  "sha256": "45b4e4f23a6d1a2b07350196871b0c53840e5d8142b47f7acedd2f40ae7a6b73",
@@ -2954,8 +2970,8 @@
2954
2970
  "bytes": 798
2955
2971
  },
2956
2972
  "v2/usecases/console-routes.js": {
2957
- "sha256": "89749c3462728cd2a821af7e4cad61d2d42b8f580765f644c9f4e9d1e9187bd1",
2958
- "bytes": 29558
2973
+ "sha256": "3e8bd3adfdc66926d91044506d2c77253b784b9e4356a8610c585e6a27153d4b",
2974
+ "bytes": 29776
2959
2975
  },
2960
2976
  "v2/usecases/console-service.d.ts": {
2961
2977
  "sha256": "fc8fe65427fa9f4f3535344b385b36f66ca06b7e3bfaea708931817a3edcad2b",
@@ -0,0 +1,34 @@
1
+ import type { GitHubQueuePollingSource } from '../types.js';
2
+ import type { GitHubQueueConfig } from '../github-queue-config.js';
3
+ import type { Result } from '../../runtime/result.js';
4
+ export interface GitHubQueueLabel {
5
+ readonly name: string;
6
+ }
7
+ export interface GitHubQueueIssue {
8
+ readonly id: number;
9
+ readonly number: number;
10
+ readonly title: string;
11
+ readonly body: string;
12
+ readonly url: string;
13
+ readonly labels: readonly GitHubQueueLabel[];
14
+ readonly createdAt: string;
15
+ }
16
+ export type GitHubQueuePollError = {
17
+ readonly kind: 'http_error';
18
+ readonly status: number;
19
+ readonly message: string;
20
+ } | {
21
+ readonly kind: 'network_error';
22
+ readonly message: string;
23
+ } | {
24
+ readonly kind: 'parse_error';
25
+ readonly message: string;
26
+ } | {
27
+ readonly kind: 'not_implemented';
28
+ readonly message: string;
29
+ };
30
+ export type FetchFn = (url: string, init: RequestInit) => Promise<Response>;
31
+ export declare const DEFAULT_SESSIONS_DIR: string;
32
+ export declare function pollGitHubQueueIssues(source: GitHubQueuePollingSource, config: GitHubQueueConfig, fetchFn?: FetchFn): Promise<Result<GitHubQueueIssue[], GitHubQueuePollError>>;
33
+ export declare function inferMaturity(body: string): 'idea' | 'specced' | 'ready';
34
+ export declare function checkIdempotency(issueNumber: number, sessionsDir?: string): Promise<'clear' | 'active'>;
@@ -0,0 +1,200 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.DEFAULT_SESSIONS_DIR = void 0;
37
+ exports.pollGitHubQueueIssues = pollGitHubQueueIssues;
38
+ exports.inferMaturity = inferMaturity;
39
+ exports.checkIdempotency = checkIdempotency;
40
+ const result_js_1 = require("../../runtime/result.js");
41
+ const fs = __importStar(require("node:fs/promises"));
42
+ const path = __importStar(require("node:path"));
43
+ const os = __importStar(require("node:os"));
44
+ exports.DEFAULT_SESSIONS_DIR = path.join(os.homedir(), '.workrail', 'daemon-sessions');
45
+ function checkRateLimit(response) {
46
+ const remainingHeader = response.headers.get('X-RateLimit-Remaining');
47
+ const resetHeader = response.headers.get('X-RateLimit-Reset');
48
+ if (remainingHeader === null)
49
+ return true;
50
+ const remaining = parseInt(remainingHeader, 10);
51
+ if (isNaN(remaining) || remaining >= 100)
52
+ return true;
53
+ const resetTs = parseInt(resetHeader ?? '0', 10);
54
+ const resetAt = resetTs > 0 ? new Date(resetTs * 1000).toISOString() : 'unknown';
55
+ console.warn(`[GitHubQueuePoller] Rate limit low: remaining=${remaining}, resets at ${resetAt}. ` +
56
+ `Skipping poll cycle to avoid exhaustion.`);
57
+ return false;
58
+ }
59
+ async function pollGitHubQueueIssues(source, config, fetchFn = globalThis.fetch) {
60
+ if (config.type !== 'assignee') {
61
+ return (0, result_js_1.err)({
62
+ kind: 'not_implemented',
63
+ message: `Queue type '${config.type}' is not implemented. Only 'assignee' is supported.`,
64
+ });
65
+ }
66
+ const [owner, repo] = source.repo.split('/');
67
+ const url = new URL(`https://api.github.com/repos/${owner}/${repo}/issues`);
68
+ url.searchParams.set('state', 'open');
69
+ url.searchParams.set('per_page', '100');
70
+ if (config.user) {
71
+ url.searchParams.set('assignee', config.user);
72
+ }
73
+ let response;
74
+ try {
75
+ response = await fetchFn(url.toString(), {
76
+ headers: {
77
+ 'Authorization': `Bearer ${source.token}`,
78
+ 'Accept': 'application/vnd.github+json',
79
+ 'X-GitHub-Api-Version': '2022-11-28',
80
+ },
81
+ });
82
+ }
83
+ catch (e) {
84
+ return (0, result_js_1.err)({
85
+ kind: 'network_error',
86
+ message: e instanceof Error ? e.message : String(e),
87
+ });
88
+ }
89
+ if (!response.ok) {
90
+ return (0, result_js_1.err)({
91
+ kind: 'http_error',
92
+ status: response.status,
93
+ message: `GitHub API returned HTTP ${response.status}: ${response.statusText}`,
94
+ });
95
+ }
96
+ if (!checkRateLimit(response)) {
97
+ return (0, result_js_1.ok)([]);
98
+ }
99
+ let raw;
100
+ try {
101
+ raw = await response.json();
102
+ }
103
+ catch (e) {
104
+ return (0, result_js_1.err)({
105
+ kind: 'parse_error',
106
+ message: `Failed to parse GitHub Issues API response: ${e instanceof Error ? e.message : String(e)}`,
107
+ });
108
+ }
109
+ if (!Array.isArray(raw)) {
110
+ return (0, result_js_1.err)({
111
+ kind: 'parse_error',
112
+ message: `Expected array from GitHub Issues API, got: ${typeof raw}`,
113
+ });
114
+ }
115
+ const issues = [];
116
+ for (const item of raw) {
117
+ const shaped = toGitHubQueueIssue(item);
118
+ if (shaped !== null) {
119
+ issues.push(shaped);
120
+ }
121
+ }
122
+ return (0, result_js_1.ok)(issues);
123
+ }
124
+ function inferMaturity(body) {
125
+ const specLineMatch = /upstream_spec:\s*(https?:\/\/\S+)/i.exec(body);
126
+ if (specLineMatch)
127
+ return 'ready';
128
+ const firstPara = body.split(/\n\s*\n/)[0] ?? '';
129
+ if (/https?:\/\/\S*\/(?:pitch|prd|spec|brd|rfc|design)\b/i.test(firstPara))
130
+ return 'ready';
131
+ if (/- \[ \]/.test(body))
132
+ return 'specced';
133
+ if (/^#{1,6}\s*(Acceptance Criteria|Implementation Plan)\s*$/im.test(body))
134
+ return 'specced';
135
+ return 'idea';
136
+ }
137
+ async function checkIdempotency(issueNumber, sessionsDir = exports.DEFAULT_SESSIONS_DIR) {
138
+ let files;
139
+ try {
140
+ files = await fs.readdir(sessionsDir);
141
+ }
142
+ catch {
143
+ return 'clear';
144
+ }
145
+ const jsonFiles = files.filter(f => f.endsWith('.json'));
146
+ for (const filename of jsonFiles) {
147
+ try {
148
+ const content = await fs.readFile(path.join(sessionsDir, filename), 'utf8');
149
+ const parsed = JSON.parse(content);
150
+ if (typeof parsed !== 'object' || parsed === null) {
151
+ return 'active';
152
+ }
153
+ const session = parsed;
154
+ const context = session['context'];
155
+ if (typeof context !== 'object' || context === null) {
156
+ continue;
157
+ }
158
+ const ctx = context;
159
+ const taskCandidate = ctx['taskCandidate'];
160
+ if (typeof taskCandidate !== 'object' || taskCandidate === null) {
161
+ continue;
162
+ }
163
+ const tc = taskCandidate;
164
+ if (tc['issueNumber'] === issueNumber) {
165
+ return 'active';
166
+ }
167
+ }
168
+ catch {
169
+ return 'active';
170
+ }
171
+ }
172
+ return 'clear';
173
+ }
174
+ function toGitHubQueueIssue(item) {
175
+ if (typeof item !== 'object' || item === null)
176
+ return null;
177
+ const obj = item;
178
+ if (typeof obj['id'] !== 'number' ||
179
+ typeof obj['number'] !== 'number' ||
180
+ typeof obj['title'] !== 'string' ||
181
+ typeof obj['html_url'] !== 'string') {
182
+ return null;
183
+ }
184
+ const body = typeof obj['body'] === 'string' ? obj['body'] : '';
185
+ const createdAt = typeof obj['created_at'] === 'string' ? obj['created_at'] : '';
186
+ const rawLabels = Array.isArray(obj['labels']) ? obj['labels'] : [];
187
+ const labels = rawLabels
188
+ .filter((l) => typeof l === 'object' && l !== null)
189
+ .filter((l) => typeof l['name'] === 'string')
190
+ .map((l) => ({ name: l['name'] }));
191
+ return {
192
+ id: obj['id'],
193
+ number: obj['number'],
194
+ title: obj['title'],
195
+ body,
196
+ url: obj['html_url'],
197
+ labels,
198
+ createdAt,
199
+ };
200
+ }
@@ -0,0 +1,18 @@
1
+ import type { Result } from '../runtime/result.js';
2
+ export interface GitHubQueueConfig {
3
+ readonly type: 'assignee' | 'label' | 'mention' | 'query';
4
+ readonly user?: string;
5
+ readonly name?: string;
6
+ readonly handle?: string;
7
+ readonly search?: string;
8
+ readonly workOnAll?: boolean;
9
+ readonly pollIntervalSeconds: number;
10
+ readonly maxTotalConcurrentSessions: number;
11
+ readonly excludeLabels: readonly string[];
12
+ readonly repo: string;
13
+ readonly token: string;
14
+ readonly botName?: string;
15
+ readonly botEmail?: string;
16
+ }
17
+ export declare const WORKRAIL_CONFIG_PATH: string;
18
+ export declare function loadQueueConfig(configPath?: string, env?: Record<string, string | undefined>): Promise<Result<GitHubQueueConfig | null, string>>;