@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.
- package/dist/console-ui/assets/{index-Sb57DW4B.js → index-BpanIvmi.js} +1 -1
- package/dist/console-ui/index.html +1 -1
- package/dist/daemon/workflow-runner.d.ts +16 -2
- package/dist/daemon/workflow-runner.js +98 -0
- package/dist/manifest.json +41 -25
- package/dist/trigger/adapters/github-queue-poller.d.ts +34 -0
- package/dist/trigger/adapters/github-queue-poller.js +200 -0
- package/dist/trigger/github-queue-config.d.ts +18 -0
- package/dist/trigger/github-queue-config.js +155 -0
- package/dist/trigger/notification-service.d.ts +1 -1
- package/dist/trigger/notification-service.js +4 -0
- package/dist/trigger/polling-scheduler.d.ts +1 -0
- package/dist/trigger/polling-scheduler.js +185 -6
- package/dist/trigger/trigger-router.js +8 -0
- package/dist/trigger/trigger-store.js +35 -2
- package/dist/trigger/types.d.ts +18 -0
- package/dist/v2/usecases/console-routes.js +3 -0
- package/docs/design/design-candidates-stuck-escalation.md +183 -0
- package/docs/design/design-review-findings-stuck-escalation.md +93 -0
- package/docs/design/implementation-plan-stuck-escalation.md +172 -0
- package/package.json +1 -1
|
@@ -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,
|
package/dist/manifest.json
CHANGED
|
@@ -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": "
|
|
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": "
|
|
554
|
-
"bytes":
|
|
553
|
+
"sha256": "4c67cc7a44c934469c190f11a71bd18bf0dfc31f59ab0c315b98315b96d59cce",
|
|
554
|
+
"bytes": 7048
|
|
555
555
|
},
|
|
556
556
|
"daemon/workflow-runner.js": {
|
|
557
|
-
"sha256": "
|
|
558
|
-
"bytes":
|
|
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": "
|
|
1594
|
-
"bytes":
|
|
1609
|
+
"sha256": "25509f290a11ac9a8aa03e5d7e44011950c841436e9458eb855b94d15d036d68",
|
|
1610
|
+
"bytes": 1582
|
|
1595
1611
|
},
|
|
1596
1612
|
"trigger/notification-service.js": {
|
|
1597
|
-
"sha256": "
|
|
1598
|
-
"bytes":
|
|
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": "
|
|
1610
|
-
"bytes":
|
|
1625
|
+
"sha256": "009b3340f5d46a3fc22b9cd087abcf484abe34c526a46dd4f958768fb8f61c9c",
|
|
1626
|
+
"bytes": 792
|
|
1611
1627
|
},
|
|
1612
1628
|
"trigger/polling-scheduler.js": {
|
|
1613
|
-
"sha256": "
|
|
1614
|
-
"bytes":
|
|
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": "
|
|
1630
|
-
"bytes":
|
|
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": "
|
|
1638
|
-
"bytes":
|
|
1653
|
+
"sha256": "8015b54da7ab5b1cec78e1ecea099d5b457eaa5de267bb1ed52cbb75eca207c4",
|
|
1654
|
+
"bytes": 38148
|
|
1639
1655
|
},
|
|
1640
1656
|
"trigger/types.d.ts": {
|
|
1641
|
-
"sha256": "
|
|
1642
|
-
"bytes":
|
|
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": "
|
|
2958
|
-
"bytes":
|
|
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>>;
|