@exaudeus/workrail 3.46.0 → 3.47.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-BQFhoMcY.js → index-B77l3WBR.js} +1 -1
- package/dist/console-ui/index.html +1 -1
- package/dist/coordinators/modes/implement-shared.d.ts +2 -1
- package/dist/coordinators/modes/implement-shared.js +7 -3
- package/dist/manifest.json +29 -29
- package/dist/trigger/adapters/github-queue-poller.js +10 -7
- package/dist/trigger/github-queue-config.d.ts +1 -0
- package/dist/trigger/github-queue-config.js +9 -0
- package/dist/trigger/polling-scheduler.js +8 -1
- package/dist/trigger/trigger-listener.js +296 -1
- package/dist/trigger/trigger-router.d.ts +4 -2
- package/dist/trigger/trigger-router.js +19 -3
- package/dist/trigger/trigger-store.js +10 -0
- package/dist/trigger/types.d.ts +2 -0
- package/dist/v2/durable-core/schemas/artifacts/review-verdict.d.ts +5 -0
- package/dist/v2/durable-core/schemas/artifacts/review-verdict.js +12 -0
- package/docs/design/connect-adaptive-dispatch-candidates.md +153 -0
- package/docs/design/connect-adaptive-dispatch-design-review.md +88 -0
- package/docs/design/connect-adaptive-dispatch-implementation-plan.md +209 -0
- package/docs/design/queue-label-support-candidates.md +83 -0
- package/docs/design/queue-label-support-design-review.md +62 -0
- package/docs/design/queue-label-support-implementation-plan.md +158 -0
- package/docs/ideas/backlog.md +74 -0
- package/package.json +1 -1
- package/workflows/mr-review-workflow.agentic.v2.json +1 -1
package/dist/manifest.json
CHANGED
|
@@ -457,8 +457,8 @@
|
|
|
457
457
|
"sha256": "5fe866e54f796975dec5d8ba9983aefd86074db212d3fccd64eed04bc9f0b3da",
|
|
458
458
|
"bytes": 8011
|
|
459
459
|
},
|
|
460
|
-
"console-ui/assets/index-
|
|
461
|
-
"sha256": "
|
|
460
|
+
"console-ui/assets/index-B77l3WBR.js": {
|
|
461
|
+
"sha256": "0c8b1d161e8b0cfc37d5b358b8753b3d6fa00e844bbca6ba56f7969afcc95606",
|
|
462
462
|
"bytes": 760528
|
|
463
463
|
},
|
|
464
464
|
"console-ui/assets/index-DGj8EsFR.css": {
|
|
@@ -466,7 +466,7 @@
|
|
|
466
466
|
"bytes": 60631
|
|
467
467
|
},
|
|
468
468
|
"console-ui/index.html": {
|
|
469
|
-
"sha256": "
|
|
469
|
+
"sha256": "93463e0e8b5f6e0fd0e5b6c5807f4ac673abfa00f35b10dea9a3b7ea0792e178",
|
|
470
470
|
"bytes": 417
|
|
471
471
|
},
|
|
472
472
|
"console/standalone-console.d.ts": {
|
|
@@ -526,12 +526,12 @@
|
|
|
526
526
|
"bytes": 11782
|
|
527
527
|
},
|
|
528
528
|
"coordinators/modes/implement-shared.d.ts": {
|
|
529
|
-
"sha256": "
|
|
530
|
-
"bytes":
|
|
529
|
+
"sha256": "fbad9d91d84d2112b273175618686489a7f106385e0e62d6cab80804d6d0f2d7",
|
|
530
|
+
"bytes": 708
|
|
531
531
|
},
|
|
532
532
|
"coordinators/modes/implement-shared.js": {
|
|
533
|
-
"sha256": "
|
|
534
|
-
"bytes":
|
|
533
|
+
"sha256": "a1727713839630d279377e9607b26068821a5a393bfbd42696222fb97ec4fe5f",
|
|
534
|
+
"bytes": 11400
|
|
535
535
|
},
|
|
536
536
|
"coordinators/modes/implement.d.ts": {
|
|
537
537
|
"sha256": "23919c24d62a0bf15296a52fbc594cca8b1b34e6f8d98dcf7dede8d97ad4cabb",
|
|
@@ -1618,8 +1618,8 @@
|
|
|
1618
1618
|
"bytes": 1363
|
|
1619
1619
|
},
|
|
1620
1620
|
"trigger/adapters/github-queue-poller.js": {
|
|
1621
|
-
"sha256": "
|
|
1622
|
-
"bytes":
|
|
1621
|
+
"sha256": "ea6c402fe42dd3bac1b15e001d572d5e2a503f3ac0a3d3dac4175f80e603bb35",
|
|
1622
|
+
"bytes": 7442
|
|
1623
1623
|
},
|
|
1624
1624
|
"trigger/adapters/gitlab-poller.d.ts": {
|
|
1625
1625
|
"sha256": "f685490fafad77194fdd0f0bbaf80dbc56730aeb344853da365199a120fbe399",
|
|
@@ -1654,12 +1654,12 @@
|
|
|
1654
1654
|
"bytes": 1269
|
|
1655
1655
|
},
|
|
1656
1656
|
"trigger/github-queue-config.d.ts": {
|
|
1657
|
-
"sha256": "
|
|
1658
|
-
"bytes":
|
|
1657
|
+
"sha256": "4dd68c2b880f808aa67c3b6be1867299c0b49ce5cf7e39f4f093dbd341d1a1e8",
|
|
1658
|
+
"bytes": 800
|
|
1659
1659
|
},
|
|
1660
1660
|
"trigger/github-queue-config.js": {
|
|
1661
|
-
"sha256": "
|
|
1662
|
-
"bytes":
|
|
1661
|
+
"sha256": "dd508fc3f9f58a70cdc076029154a3c1610569c611471e9b67bf1cd4be64a520",
|
|
1662
|
+
"bytes": 7347
|
|
1663
1663
|
},
|
|
1664
1664
|
"trigger/index.d.ts": {
|
|
1665
1665
|
"sha256": "a9cfd053714173e2a8cc5a282fd5b09a5c3f3001304d507facd0e12de9cc0733",
|
|
@@ -1690,36 +1690,36 @@
|
|
|
1690
1690
|
"bytes": 792
|
|
1691
1691
|
},
|
|
1692
1692
|
"trigger/polling-scheduler.js": {
|
|
1693
|
-
"sha256": "
|
|
1694
|
-
"bytes":
|
|
1693
|
+
"sha256": "dd5532f3dff75377685fb68d78820cb5916f823626b91d6306ad7aa3d9801be5",
|
|
1694
|
+
"bytes": 20188
|
|
1695
1695
|
},
|
|
1696
1696
|
"trigger/trigger-listener.d.ts": {
|
|
1697
1697
|
"sha256": "92e971ab8f47c3c867860cffc01f54c4aae54fcc4ae199b9469210f4ce639423",
|
|
1698
1698
|
"bytes": 1639
|
|
1699
1699
|
},
|
|
1700
1700
|
"trigger/trigger-listener.js": {
|
|
1701
|
-
"sha256": "
|
|
1702
|
-
"bytes":
|
|
1701
|
+
"sha256": "1c73115b26d9274c86fd3b1e19f5eae2754d58e248949b6eaf0e7b7074b91c8c",
|
|
1702
|
+
"bytes": 25421
|
|
1703
1703
|
},
|
|
1704
1704
|
"trigger/trigger-router.d.ts": {
|
|
1705
|
-
"sha256": "
|
|
1706
|
-
"bytes":
|
|
1705
|
+
"sha256": "44f1bfefafee4955bad74db9e61ecb6a49ea426c46100753f6bdb3a7f7b583dd",
|
|
1706
|
+
"bytes": 2671
|
|
1707
1707
|
},
|
|
1708
1708
|
"trigger/trigger-router.js": {
|
|
1709
|
-
"sha256": "
|
|
1710
|
-
"bytes":
|
|
1709
|
+
"sha256": "1f84935ad94e36be5befbef34d10deb7467c708e6d06cd36903843806f4b49a6",
|
|
1710
|
+
"bytes": 19067
|
|
1711
1711
|
},
|
|
1712
1712
|
"trigger/trigger-store.d.ts": {
|
|
1713
1713
|
"sha256": "7afb05127d55bc3757a550dd15d4b797766b3fff29d1bfe76b303764b93322e7",
|
|
1714
1714
|
"bytes": 1588
|
|
1715
1715
|
},
|
|
1716
1716
|
"trigger/trigger-store.js": {
|
|
1717
|
-
"sha256": "
|
|
1718
|
-
"bytes":
|
|
1717
|
+
"sha256": "8e85e0bdbd1596e70c23667e84ed380d6f0cee10028cd1a41163cda3467c2bdc",
|
|
1718
|
+
"bytes": 38559
|
|
1719
1719
|
},
|
|
1720
1720
|
"trigger/types.d.ts": {
|
|
1721
|
-
"sha256": "
|
|
1722
|
-
"bytes":
|
|
1721
|
+
"sha256": "611f047631d8334a966acb7d1a71f5aa0d8cda65da127e07081b363290bcfdc2",
|
|
1722
|
+
"bytes": 3654
|
|
1723
1723
|
},
|
|
1724
1724
|
"trigger/types.js": {
|
|
1725
1725
|
"sha256": "45b4e4f23a6d1a2b07350196871b0c53840e5d8142b47f7acedd2f40ae7a6b73",
|
|
@@ -2262,12 +2262,12 @@
|
|
|
2262
2262
|
"bytes": 2115
|
|
2263
2263
|
},
|
|
2264
2264
|
"v2/durable-core/schemas/artifacts/review-verdict.d.ts": {
|
|
2265
|
-
"sha256": "
|
|
2266
|
-
"bytes":
|
|
2265
|
+
"sha256": "2b9f7ae6b3fafe6c26f266bb249c8c286144eb1f753c803d4358ef81e30d1736",
|
|
2266
|
+
"bytes": 2211
|
|
2267
2267
|
},
|
|
2268
2268
|
"v2/durable-core/schemas/artifacts/review-verdict.js": {
|
|
2269
|
-
"sha256": "
|
|
2270
|
-
"bytes":
|
|
2269
|
+
"sha256": "67e03f956523b68783f0d01414bf04992070204d8bb828cdbb303bd0d914a220",
|
|
2270
|
+
"bytes": 1557
|
|
2271
2271
|
},
|
|
2272
2272
|
"v2/durable-core/schemas/compiled-workflow/index.d.ts": {
|
|
2273
2273
|
"sha256": "66e5a5e8d87e1c4743acfad71fa81c22775d377b566701d71c17227a4f4c969c",
|
|
@@ -57,19 +57,22 @@ function checkRateLimit(response) {
|
|
|
57
57
|
return false;
|
|
58
58
|
}
|
|
59
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
60
|
const [owner, repo] = source.repo.split('/');
|
|
67
61
|
const url = new URL(`https://api.github.com/repos/${owner}/${repo}/issues`);
|
|
68
62
|
url.searchParams.set('state', 'open');
|
|
69
63
|
url.searchParams.set('per_page', '100');
|
|
70
|
-
if (config.user) {
|
|
64
|
+
if (config.type === 'assignee' && config.user) {
|
|
71
65
|
url.searchParams.set('assignee', config.user);
|
|
72
66
|
}
|
|
67
|
+
else if (config.type === 'label' && config.queueLabel) {
|
|
68
|
+
url.searchParams.set('labels', config.queueLabel);
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
return (0, result_js_1.err)({
|
|
72
|
+
kind: 'not_implemented',
|
|
73
|
+
message: `Queue type "${config.type}" is not yet implemented`,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
73
76
|
let response;
|
|
74
77
|
try {
|
|
75
78
|
response = await fetchFn(url.toString(), {
|
|
@@ -2,6 +2,7 @@ import type { Result } from '../runtime/result.js';
|
|
|
2
2
|
export interface GitHubQueueConfig {
|
|
3
3
|
readonly type: 'assignee' | 'label' | 'mention' | 'query';
|
|
4
4
|
readonly user?: string;
|
|
5
|
+
readonly queueLabel?: string;
|
|
5
6
|
readonly name?: string;
|
|
6
7
|
readonly handle?: string;
|
|
7
8
|
readonly search?: string;
|
|
@@ -123,10 +123,18 @@ async function loadQueueConfig(configPath = exports.WORKRAIL_CONFIG_PATH, env =
|
|
|
123
123
|
}
|
|
124
124
|
excludeLabels = rawExcludeLabels;
|
|
125
125
|
}
|
|
126
|
+
if (rawType === 'label') {
|
|
127
|
+
const rawQueueLabel = q['queueLabel'];
|
|
128
|
+
if (typeof rawQueueLabel !== 'string' || !rawQueueLabel.trim()) {
|
|
129
|
+
return (0, result_js_1.err)('config.queue.queueLabel is required when type is "label"');
|
|
130
|
+
}
|
|
131
|
+
}
|
|
126
132
|
const rawUser = q['user'];
|
|
127
133
|
const user = typeof rawUser === 'string' && rawUser.trim() ? rawUser.trim() : undefined;
|
|
128
134
|
const rawName = q['name'];
|
|
129
135
|
const labelName = typeof rawName === 'string' && rawName.trim() ? rawName.trim() : undefined;
|
|
136
|
+
const rawQueueLabel = q['queueLabel'];
|
|
137
|
+
const queueLabel = typeof rawQueueLabel === 'string' && rawQueueLabel.trim() ? rawQueueLabel.trim() : undefined;
|
|
130
138
|
const rawHandle = q['handle'];
|
|
131
139
|
const handle = typeof rawHandle === 'string' && rawHandle.trim() ? rawHandle.trim() : undefined;
|
|
132
140
|
const rawSearch = q['search'];
|
|
@@ -140,6 +148,7 @@ async function loadQueueConfig(configPath = exports.WORKRAIL_CONFIG_PATH, env =
|
|
|
140
148
|
return (0, result_js_1.ok)({
|
|
141
149
|
type: rawType,
|
|
142
150
|
...(user !== undefined ? { user } : {}),
|
|
151
|
+
...(queueLabel !== undefined ? { queueLabel } : {}),
|
|
143
152
|
...(labelName !== undefined ? { name: labelName } : {}),
|
|
144
153
|
...(handle !== undefined ? { handle } : {}),
|
|
145
154
|
...(search !== undefined ? { search } : {}),
|
|
@@ -286,7 +286,14 @@ class PollingScheduler {
|
|
|
286
286
|
const maturityReason = describeMaturityReason(top.maturity);
|
|
287
287
|
console.log(`[QueuePoll] selected #${top.issue.number} "${top.issue.title}" maturity=${top.maturity} reason="${maturityReason}"`);
|
|
288
288
|
await appendQueuePollLog({ event: 'task_selected', issueNumber: top.issue.number, title: top.issue.title, maturity: top.maturity, reason: maturityReason, ts: new Date().toISOString() });
|
|
289
|
-
this.router
|
|
289
|
+
if ('dispatchAdaptivePipeline' in this.router &&
|
|
290
|
+
typeof this.router.dispatchAdaptivePipeline === 'function') {
|
|
291
|
+
void this.router.dispatchAdaptivePipeline(workflowTrigger.goal, workflowTrigger.workspacePath, workflowTrigger.context);
|
|
292
|
+
console.log(`[QueuePoll] dispatched via adaptivePipeline goal="${workflowTrigger.goal.slice(0, 80)}"`);
|
|
293
|
+
}
|
|
294
|
+
else {
|
|
295
|
+
this.router.dispatch(workflowTrigger);
|
|
296
|
+
}
|
|
290
297
|
for (let i = 1; i < candidates.length; i++) {
|
|
291
298
|
const { issue, maturity } = candidates[i];
|
|
292
299
|
console.log(`[QueuePoll] skipped #${issue.number} "${issue.title}" reason=lower_priority_${maturity}`);
|
|
@@ -41,6 +41,12 @@ exports.startTriggerListener = startTriggerListener;
|
|
|
41
41
|
require("reflect-metadata");
|
|
42
42
|
const express_1 = __importDefault(require("express"));
|
|
43
43
|
const http = __importStar(require("node:http"));
|
|
44
|
+
const fs = __importStar(require("node:fs"));
|
|
45
|
+
const os = __importStar(require("node:os"));
|
|
46
|
+
const path = __importStar(require("node:path"));
|
|
47
|
+
const node_child_process_1 = require("node:child_process");
|
|
48
|
+
const node_util_1 = require("node:util");
|
|
49
|
+
const node_crypto_1 = require("node:crypto");
|
|
44
50
|
const trigger_store_js_1 = require("./trigger-store.js");
|
|
45
51
|
const trigger_router_js_1 = require("./trigger-router.js");
|
|
46
52
|
const config_file_js_1 = require("../config/config-file.js");
|
|
@@ -49,6 +55,12 @@ const workflow_runner_js_1 = require("../daemon/workflow-runner.js");
|
|
|
49
55
|
const types_js_1 = require("./types.js");
|
|
50
56
|
const polling_scheduler_js_1 = require("./polling-scheduler.js");
|
|
51
57
|
const polled_event_store_js_1 = require("./polled-event-store.js");
|
|
58
|
+
const index_js_1 = require("../context-assembly/index.js");
|
|
59
|
+
const infra_js_1 = require("../context-assembly/infra.js");
|
|
60
|
+
const quick_review_js_1 = require("../coordinators/modes/quick-review.js");
|
|
61
|
+
const review_only_js_1 = require("../coordinators/modes/review-only.js");
|
|
62
|
+
const implement_js_1 = require("../coordinators/modes/implement.js");
|
|
63
|
+
const full_pipeline_js_1 = require("../coordinators/modes/full-pipeline.js");
|
|
52
64
|
const DEFAULT_TRIGGER_PORT = 3200;
|
|
53
65
|
function createTriggerApp(router) {
|
|
54
66
|
const app = (0, express_1.default)();
|
|
@@ -184,8 +196,291 @@ async function startTriggerListener(ctx, options) {
|
|
|
184
196
|
? new notification_service_js_1.NotificationService({ macOs: notifyMacOs, webhookUrl: notifyWebhook })
|
|
185
197
|
: undefined;
|
|
186
198
|
const steerRegistry = new Map();
|
|
199
|
+
const DAEMON_CONSOLE_PORT = 3456;
|
|
200
|
+
const execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
|
|
201
|
+
const coordinatorDeps = {
|
|
202
|
+
spawnSession: async (workflowId, goal, workspace, context) => {
|
|
203
|
+
const url = `http://127.0.0.1:${DAEMON_CONSOLE_PORT}/api/v2/auto/dispatch`;
|
|
204
|
+
try {
|
|
205
|
+
const response = await globalThis.fetch(url, {
|
|
206
|
+
method: 'POST',
|
|
207
|
+
headers: { 'Content-Type': 'application/json' },
|
|
208
|
+
body: JSON.stringify({
|
|
209
|
+
workflowId,
|
|
210
|
+
goal,
|
|
211
|
+
workspacePath: workspace,
|
|
212
|
+
...(context !== undefined ? { context } : {}),
|
|
213
|
+
}),
|
|
214
|
+
signal: AbortSignal.timeout(30000),
|
|
215
|
+
});
|
|
216
|
+
const body = await response.json();
|
|
217
|
+
if (!response.ok) {
|
|
218
|
+
const errMsg = typeof body['error'] === 'string' ? body['error'] : `HTTP ${response.status}`;
|
|
219
|
+
if (response.status === 503) {
|
|
220
|
+
return { kind: 'err', error: `WorkTrain daemon is not ready: ${errMsg}` };
|
|
221
|
+
}
|
|
222
|
+
return { kind: 'err', error: `Dispatch failed: ${errMsg}` };
|
|
223
|
+
}
|
|
224
|
+
if (body['success'] !== true || typeof body['data'] !== 'object') {
|
|
225
|
+
return { kind: 'err', error: 'Unexpected response from dispatch endpoint' };
|
|
226
|
+
}
|
|
227
|
+
const data = body['data'];
|
|
228
|
+
const handle = typeof data['sessionHandle'] === 'string' ? data['sessionHandle'] : '';
|
|
229
|
+
if (!handle) {
|
|
230
|
+
return { kind: 'err', error: 'Dispatch succeeded but no session handle returned' };
|
|
231
|
+
}
|
|
232
|
+
return { kind: 'ok', value: handle };
|
|
233
|
+
}
|
|
234
|
+
catch (e) {
|
|
235
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
236
|
+
const isConnRefused = msg.includes('ECONNREFUSED') || msg.includes('fetch failed');
|
|
237
|
+
if (isConnRefused) {
|
|
238
|
+
return { kind: 'err', error: `Could not connect to WorkTrain daemon on port ${DAEMON_CONSOLE_PORT}. Ensure the daemon is running.` };
|
|
239
|
+
}
|
|
240
|
+
if (e instanceof Error && e.name === 'TimeoutError') {
|
|
241
|
+
return { kind: 'err', error: `Daemon request timed out after 30s` };
|
|
242
|
+
}
|
|
243
|
+
return { kind: 'err', error: `Dispatch request failed: ${msg}` };
|
|
244
|
+
}
|
|
245
|
+
},
|
|
246
|
+
contextAssembler: (0, index_js_1.createContextAssembler)({
|
|
247
|
+
execGit: async (args, cwd) => {
|
|
248
|
+
try {
|
|
249
|
+
const { stdout } = await execFileAsync('git', [...args], { cwd });
|
|
250
|
+
return { kind: 'ok', value: stdout };
|
|
251
|
+
}
|
|
252
|
+
catch (e) {
|
|
253
|
+
return { kind: 'err', error: e instanceof Error ? e.message : String(e) };
|
|
254
|
+
}
|
|
255
|
+
},
|
|
256
|
+
execGh: async (args, cwd) => {
|
|
257
|
+
try {
|
|
258
|
+
const { stdout } = await execFileAsync('gh', [...args], { cwd });
|
|
259
|
+
return { kind: 'ok', value: stdout };
|
|
260
|
+
}
|
|
261
|
+
catch (e) {
|
|
262
|
+
return { kind: 'err', error: e instanceof Error ? e.message : String(e) };
|
|
263
|
+
}
|
|
264
|
+
},
|
|
265
|
+
listRecentSessions: (0, infra_js_1.createListRecentSessions)(),
|
|
266
|
+
nowIso: () => new Date().toISOString(),
|
|
267
|
+
}),
|
|
268
|
+
awaitSessions: async (handles, timeoutMs) => {
|
|
269
|
+
const { executeWorktrainAwaitCommand } = await Promise.resolve().then(() => __importStar(require('../cli/commands/worktrain-await.js')));
|
|
270
|
+
let resolvedResult = null;
|
|
271
|
+
await executeWorktrainAwaitCommand({
|
|
272
|
+
fetch: (url) => globalThis.fetch(url),
|
|
273
|
+
readFile: (p) => fs.promises.readFile(p, 'utf-8'),
|
|
274
|
+
stdout: (line) => {
|
|
275
|
+
try {
|
|
276
|
+
resolvedResult = JSON.parse(line);
|
|
277
|
+
}
|
|
278
|
+
catch { }
|
|
279
|
+
},
|
|
280
|
+
stderr: (line) => process.stderr.write(line + '\n'),
|
|
281
|
+
homedir: os.homedir,
|
|
282
|
+
joinPath: path.join,
|
|
283
|
+
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
284
|
+
now: () => Date.now(),
|
|
285
|
+
}, {
|
|
286
|
+
sessions: [...handles].join(','),
|
|
287
|
+
mode: 'all',
|
|
288
|
+
timeout: `${Math.round(timeoutMs / 1000)}s`,
|
|
289
|
+
port: DAEMON_CONSOLE_PORT,
|
|
290
|
+
});
|
|
291
|
+
if (resolvedResult === null) {
|
|
292
|
+
process.stderr.write(`[WARN coord:reason=await_failed] awaitSessions: could not get session results -- daemon may be unreachable or timed out. Returning all ${handles.length} session(s) as failed.\n`);
|
|
293
|
+
}
|
|
294
|
+
return resolvedResult ?? {
|
|
295
|
+
results: [...handles].map((h) => ({
|
|
296
|
+
handle: h,
|
|
297
|
+
outcome: 'failed',
|
|
298
|
+
status: null,
|
|
299
|
+
durationMs: 0,
|
|
300
|
+
})),
|
|
301
|
+
allSucceeded: false,
|
|
302
|
+
};
|
|
303
|
+
},
|
|
304
|
+
getAgentResult: async (sessionHandle) => {
|
|
305
|
+
const emptyResult = { recapMarkdown: null, artifacts: [] };
|
|
306
|
+
try {
|
|
307
|
+
const sessionUrl = `http://127.0.0.1:${DAEMON_CONSOLE_PORT}/api/v2/sessions/${encodeURIComponent(sessionHandle)}`;
|
|
308
|
+
const sessionRes = await globalThis.fetch(sessionUrl, { signal: AbortSignal.timeout(30000) });
|
|
309
|
+
if (!sessionRes.ok) {
|
|
310
|
+
process.stderr.write(`[WARN coord:reason=http_error status=${sessionRes.status} handle=${sessionHandle.slice(0, 16)}] getAgentResult: session fetch returned HTTP ${sessionRes.status}\n`);
|
|
311
|
+
return emptyResult;
|
|
312
|
+
}
|
|
313
|
+
const sessionBody = await sessionRes.json();
|
|
314
|
+
if (sessionBody['success'] !== true) {
|
|
315
|
+
return emptyResult;
|
|
316
|
+
}
|
|
317
|
+
const data = sessionBody['data'];
|
|
318
|
+
if (!data)
|
|
319
|
+
return emptyResult;
|
|
320
|
+
const runs = data['runs'];
|
|
321
|
+
if (!Array.isArray(runs) || runs.length === 0)
|
|
322
|
+
return emptyResult;
|
|
323
|
+
const firstRun = runs[0];
|
|
324
|
+
const tipNodeId = typeof firstRun['preferredTipNodeId'] === 'string'
|
|
325
|
+
? firstRun['preferredTipNodeId']
|
|
326
|
+
: null;
|
|
327
|
+
if (!tipNodeId)
|
|
328
|
+
return emptyResult;
|
|
329
|
+
const allNodes = Array.isArray(firstRun['nodes'])
|
|
330
|
+
? firstRun['nodes']
|
|
331
|
+
: [];
|
|
332
|
+
const allNodeIds = allNodes
|
|
333
|
+
.map((n) => (typeof n['nodeId'] === 'string' ? n['nodeId'] : null))
|
|
334
|
+
.filter((id) => id !== null);
|
|
335
|
+
const nodeIdsToFetch = allNodeIds.length > 0 ? allNodeIds : [tipNodeId];
|
|
336
|
+
const baseNodeUrl = `http://127.0.0.1:${DAEMON_CONSOLE_PORT}/api/v2/sessions/${encodeURIComponent(sessionHandle)}/nodes/`;
|
|
337
|
+
let recap = null;
|
|
338
|
+
const collectedArtifacts = [];
|
|
339
|
+
for (const nodeId of nodeIdsToFetch) {
|
|
340
|
+
try {
|
|
341
|
+
const nodeRes = await globalThis.fetch(baseNodeUrl + encodeURIComponent(nodeId), { signal: AbortSignal.timeout(30000) });
|
|
342
|
+
if (!nodeRes.ok)
|
|
343
|
+
continue;
|
|
344
|
+
const nodeBody = await nodeRes.json();
|
|
345
|
+
if (nodeBody['success'] !== true)
|
|
346
|
+
continue;
|
|
347
|
+
const nodeData = nodeBody['data'];
|
|
348
|
+
if (!nodeData)
|
|
349
|
+
continue;
|
|
350
|
+
if (nodeId === tipNodeId) {
|
|
351
|
+
recap = typeof nodeData['recapMarkdown'] === 'string' ? nodeData['recapMarkdown'] : null;
|
|
352
|
+
}
|
|
353
|
+
const nodeArtifacts = nodeData['artifacts'];
|
|
354
|
+
if (Array.isArray(nodeArtifacts) && nodeArtifacts.length > 0) {
|
|
355
|
+
collectedArtifacts.push(...nodeArtifacts);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
catch (nodeErr) {
|
|
359
|
+
const msg = nodeErr instanceof Error ? nodeErr.message : String(nodeErr);
|
|
360
|
+
process.stderr.write(`[WARN coord:reason=node_exception handle=${sessionHandle.slice(0, 16)} node=${nodeId.slice(0, 16)}] getAgentResult: ${msg}\n`);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
return { recapMarkdown: recap, artifacts: collectedArtifacts };
|
|
364
|
+
}
|
|
365
|
+
catch (e) {
|
|
366
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
367
|
+
process.stderr.write(`[WARN coord:reason=exception handle=${sessionHandle.slice(0, 16)}] getAgentResult: ${msg}\n`);
|
|
368
|
+
return emptyResult;
|
|
369
|
+
}
|
|
370
|
+
},
|
|
371
|
+
listOpenPRs: async (workspace) => {
|
|
372
|
+
try {
|
|
373
|
+
const { stdout } = await execFileAsync('gh', ['pr', 'list', '--json', 'number,title,headRefName'], {
|
|
374
|
+
cwd: workspace,
|
|
375
|
+
timeout: 30000,
|
|
376
|
+
});
|
|
377
|
+
const parsed = JSON.parse(stdout);
|
|
378
|
+
return parsed.map((p) => ({ number: p.number, title: p.title, headRef: p.headRefName }));
|
|
379
|
+
}
|
|
380
|
+
catch {
|
|
381
|
+
return [];
|
|
382
|
+
}
|
|
383
|
+
},
|
|
384
|
+
mergePR: async (prNumber, workspace) => {
|
|
385
|
+
try {
|
|
386
|
+
await execFileAsync('gh', ['pr', 'merge', String(prNumber), '--squash', '--auto'], {
|
|
387
|
+
cwd: workspace,
|
|
388
|
+
timeout: 60000,
|
|
389
|
+
});
|
|
390
|
+
return { kind: 'ok', value: undefined };
|
|
391
|
+
}
|
|
392
|
+
catch (e) {
|
|
393
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
394
|
+
return { kind: 'err', error: msg };
|
|
395
|
+
}
|
|
396
|
+
},
|
|
397
|
+
writeFile: async (filePath, content) => {
|
|
398
|
+
await fs.promises.writeFile(filePath, content, 'utf-8');
|
|
399
|
+
},
|
|
400
|
+
readFile: (filePath) => fs.promises.readFile(filePath, 'utf-8'),
|
|
401
|
+
appendFile: (filePath, content) => fs.promises.appendFile(filePath, content, 'utf-8'),
|
|
402
|
+
mkdir: (dirPath, opts) => fs.promises.mkdir(dirPath, opts),
|
|
403
|
+
homedir: os.homedir,
|
|
404
|
+
joinPath: path.join,
|
|
405
|
+
nowIso: () => new Date().toISOString(),
|
|
406
|
+
generateId: () => (0, node_crypto_1.randomUUID)(),
|
|
407
|
+
stderr: (line) => process.stderr.write(line + '\n'),
|
|
408
|
+
now: () => Date.now(),
|
|
409
|
+
port: DAEMON_CONSOLE_PORT,
|
|
410
|
+
fileExists: (p) => fs.existsSync(p),
|
|
411
|
+
archiveFile: (src, dest) => fs.promises.rename(src, dest),
|
|
412
|
+
pollForPR: async (branchPattern, timeoutMs) => {
|
|
413
|
+
const pollIntervalMs = 30000;
|
|
414
|
+
const deadline = Date.now() + timeoutMs;
|
|
415
|
+
while (Date.now() < deadline) {
|
|
416
|
+
try {
|
|
417
|
+
const { stdout } = await execFileAsync('gh', ['pr', 'list', '--head', branchPattern, '--json', 'url', '--limit', '1'], { timeout: 30000 });
|
|
418
|
+
const parsed = JSON.parse(stdout);
|
|
419
|
+
if (parsed.length > 0 && parsed[0] && parsed[0].url) {
|
|
420
|
+
return parsed[0].url;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
catch {
|
|
424
|
+
}
|
|
425
|
+
const remaining = deadline - Date.now();
|
|
426
|
+
if (remaining <= 0)
|
|
427
|
+
break;
|
|
428
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(pollIntervalMs, remaining)));
|
|
429
|
+
}
|
|
430
|
+
return null;
|
|
431
|
+
},
|
|
432
|
+
postToOutbox: async (message, metadata) => {
|
|
433
|
+
const workrailDir = path.join(os.homedir(), '.workrail');
|
|
434
|
+
const outboxPath = path.join(workrailDir, 'outbox.jsonl');
|
|
435
|
+
await fs.promises.mkdir(workrailDir, { recursive: true });
|
|
436
|
+
const entry = JSON.stringify({
|
|
437
|
+
id: (0, node_crypto_1.randomUUID)(),
|
|
438
|
+
message,
|
|
439
|
+
metadata,
|
|
440
|
+
timestamp: new Date().toISOString(),
|
|
441
|
+
});
|
|
442
|
+
await fs.promises.appendFile(outboxPath, entry + '\n', 'utf-8');
|
|
443
|
+
},
|
|
444
|
+
pollOutboxAck: async (requestId, timeoutMs) => {
|
|
445
|
+
const pollIntervalMs = 5 * 60 * 1000;
|
|
446
|
+
const workrailDir = path.join(os.homedir(), '.workrail');
|
|
447
|
+
const outboxPath = path.join(workrailDir, 'outbox.jsonl');
|
|
448
|
+
const cursorPath = path.join(workrailDir, 'inbox-cursor.json');
|
|
449
|
+
let snapshotCount = 0;
|
|
450
|
+
try {
|
|
451
|
+
const outboxContent = await fs.promises.readFile(outboxPath, 'utf-8');
|
|
452
|
+
snapshotCount = outboxContent.split('\n').filter((l) => l.trim() !== '').length;
|
|
453
|
+
}
|
|
454
|
+
catch {
|
|
455
|
+
}
|
|
456
|
+
void requestId;
|
|
457
|
+
const deadline = Date.now() + timeoutMs;
|
|
458
|
+
while (Date.now() < deadline) {
|
|
459
|
+
const remaining = deadline - Date.now();
|
|
460
|
+
if (remaining <= 0)
|
|
461
|
+
break;
|
|
462
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(pollIntervalMs, remaining)));
|
|
463
|
+
try {
|
|
464
|
+
const cursorContent = await fs.promises.readFile(cursorPath, 'utf-8');
|
|
465
|
+
const cursor = JSON.parse(cursorContent);
|
|
466
|
+
if (typeof cursor.lastReadCount === 'number' && cursor.lastReadCount > snapshotCount) {
|
|
467
|
+
return 'acked';
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
catch {
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
return 'timeout';
|
|
474
|
+
},
|
|
475
|
+
};
|
|
476
|
+
const modeExecutors = {
|
|
477
|
+
runQuickReview: quick_review_js_1.runQuickReviewPipeline,
|
|
478
|
+
runReviewOnly: review_only_js_1.runReviewOnlyPipeline,
|
|
479
|
+
runImplement: implement_js_1.runImplementPipeline,
|
|
480
|
+
runFull: full_pipeline_js_1.runFullPipeline,
|
|
481
|
+
};
|
|
187
482
|
const runWorkflowFn = options.runWorkflowFn ?? workflow_runner_js_1.runWorkflow;
|
|
188
|
-
const router = new trigger_router_js_1.TriggerRouter(triggerIndex, ctx, apiKey, runWorkflowFn, undefined, maxConcurrentSessions, options.emitter, notificationService, steerRegistry);
|
|
483
|
+
const router = new trigger_router_js_1.TriggerRouter(triggerIndex, ctx, apiKey, runWorkflowFn, undefined, maxConcurrentSessions, options.emitter, notificationService, steerRegistry, coordinatorDeps, modeExecutors);
|
|
189
484
|
const app = createTriggerApp(router);
|
|
190
485
|
const allTriggers = [...triggerIndex.values()];
|
|
191
486
|
const polledEventStore = new polled_event_store_js_1.PolledEventStore(env);
|
|
@@ -36,11 +36,13 @@ export declare class TriggerRouter {
|
|
|
36
36
|
private readonly emitter;
|
|
37
37
|
private readonly notificationService;
|
|
38
38
|
private readonly steerRegistry;
|
|
39
|
-
|
|
39
|
+
private readonly _coordinatorDeps;
|
|
40
|
+
private readonly _modeExecutors;
|
|
41
|
+
constructor(index: ReadonlyMap<string, TriggerDefinition>, ctx: V2ToolContext, apiKey: string, runWorkflowFn: RunWorkflowFn, execFn?: ExecFn, maxConcurrentSessions?: number, emitter?: DaemonEventEmitter, notificationService?: NotificationService, steerRegistry?: SteerRegistry, coordinatorDeps?: AdaptiveCoordinatorDeps, modeExecutors?: ModeExecutors);
|
|
40
42
|
get activeSessions(): number;
|
|
41
43
|
get maxConcurrentSessions(): number;
|
|
42
44
|
route(event: WebhookEvent): RouteResult;
|
|
43
45
|
dispatch(workflowTrigger: WorkflowTrigger): string;
|
|
44
46
|
listTriggers(): readonly TriggerDefinition[];
|
|
45
|
-
dispatchAdaptivePipeline(goal: string, workspace: string,
|
|
47
|
+
dispatchAdaptivePipeline(goal: string, workspace: string, context?: Readonly<Record<string, unknown>>, coordinatorDeps?: AdaptiveCoordinatorDeps, modeExecutors?: ModeExecutors): ReturnType<typeof runAdaptivePipeline>;
|
|
46
48
|
}
|
|
@@ -204,7 +204,7 @@ class Semaphore {
|
|
|
204
204
|
}
|
|
205
205
|
const DEFAULT_MAX_CONCURRENT_SESSIONS = 3;
|
|
206
206
|
class TriggerRouter {
|
|
207
|
-
constructor(index, ctx, apiKey, runWorkflowFn, execFn, maxConcurrentSessions, emitter, notificationService, steerRegistry) {
|
|
207
|
+
constructor(index, ctx, apiKey, runWorkflowFn, execFn, maxConcurrentSessions, emitter, notificationService, steerRegistry, coordinatorDeps, modeExecutors) {
|
|
208
208
|
this.index = index;
|
|
209
209
|
this.ctx = ctx;
|
|
210
210
|
this.apiKey = apiKey;
|
|
@@ -214,6 +214,8 @@ class TriggerRouter {
|
|
|
214
214
|
this.emitter = emitter;
|
|
215
215
|
this.notificationService = notificationService;
|
|
216
216
|
this.steerRegistry = steerRegistry;
|
|
217
|
+
this._coordinatorDeps = coordinatorDeps;
|
|
218
|
+
this._modeExecutors = modeExecutors;
|
|
217
219
|
const requested = maxConcurrentSessions ?? DEFAULT_MAX_CONCURRENT_SESSIONS;
|
|
218
220
|
const cap = Number.isNaN(requested) ? DEFAULT_MAX_CONCURRENT_SESSIONS : requested;
|
|
219
221
|
if (cap < 1) {
|
|
@@ -385,13 +387,27 @@ class TriggerRouter {
|
|
|
385
387
|
listTriggers() {
|
|
386
388
|
return [...this.index.values()];
|
|
387
389
|
}
|
|
388
|
-
async dispatchAdaptivePipeline(goal, workspace, coordinatorDeps, modeExecutors
|
|
390
|
+
async dispatchAdaptivePipeline(goal, workspace, context, coordinatorDeps, modeExecutors) {
|
|
391
|
+
const effectiveDeps = coordinatorDeps ?? this._coordinatorDeps;
|
|
392
|
+
const effectiveExecutors = modeExecutors ?? this._modeExecutors;
|
|
393
|
+
if (effectiveDeps === undefined || effectiveExecutors === undefined) {
|
|
394
|
+
console.warn('[TriggerRouter] dispatchAdaptivePipeline called but coordinatorDeps not injected -- ' +
|
|
395
|
+
'adaptive dispatch disabled. Inject coordinatorDeps and modeExecutors in the ' +
|
|
396
|
+
'TriggerRouter constructor to activate. Returning escalated outcome.');
|
|
397
|
+
return {
|
|
398
|
+
kind: 'escalated',
|
|
399
|
+
escalationReason: {
|
|
400
|
+
phase: 'dispatch',
|
|
401
|
+
reason: 'coordinatorDeps or modeExecutors not injected into TriggerRouter',
|
|
402
|
+
},
|
|
403
|
+
};
|
|
404
|
+
}
|
|
389
405
|
const opts = {
|
|
390
406
|
goal,
|
|
391
407
|
workspace,
|
|
392
408
|
taskCandidate: context,
|
|
393
409
|
};
|
|
394
|
-
return (0, adaptive_pipeline_js_1.runAdaptivePipeline)(
|
|
410
|
+
return (0, adaptive_pipeline_js_1.runAdaptivePipeline)(effectiveDeps, opts, effectiveExecutors);
|
|
395
411
|
}
|
|
396
412
|
}
|
|
397
413
|
exports.TriggerRouter = TriggerRouter;
|
|
@@ -400,6 +400,12 @@ function setTriggerField(trigger, key, value) {
|
|
|
400
400
|
case 'branchPrefix':
|
|
401
401
|
trigger.branchPrefix = value;
|
|
402
402
|
break;
|
|
403
|
+
case 'queueType':
|
|
404
|
+
trigger.queueType = value;
|
|
405
|
+
break;
|
|
406
|
+
case 'queueLabel':
|
|
407
|
+
trigger.queueLabel = value;
|
|
408
|
+
break;
|
|
403
409
|
default:
|
|
404
410
|
break;
|
|
405
411
|
}
|
|
@@ -780,11 +786,15 @@ function validateAndResolveTrigger(raw, env, workspaces = {}) {
|
|
|
780
786
|
}
|
|
781
787
|
queuePollIntervalSeconds = asNumber;
|
|
782
788
|
}
|
|
789
|
+
const rawQueueType = raw.queueType?.trim();
|
|
790
|
+
const rawQueueLabel = raw.queueLabel?.trim();
|
|
783
791
|
pollingSource = {
|
|
784
792
|
provider: 'github_queue_poll',
|
|
785
793
|
repo: queueSrc.repo.trim(),
|
|
786
794
|
token: queueTokenResult.value,
|
|
787
795
|
pollIntervalSeconds: queuePollIntervalSeconds,
|
|
796
|
+
...(rawQueueType ? { queueType: rawQueueType } : {}),
|
|
797
|
+
...(rawQueueLabel ? { queueLabel: rawQueueLabel } : {}),
|
|
788
798
|
};
|
|
789
799
|
}
|
|
790
800
|
else if (raw.source) {
|
package/dist/trigger/types.d.ts
CHANGED
|
@@ -38,6 +38,8 @@ export interface GitHubQueuePollingSource {
|
|
|
38
38
|
readonly repo: string;
|
|
39
39
|
readonly token: string;
|
|
40
40
|
readonly pollIntervalSeconds: number;
|
|
41
|
+
readonly queueType?: string;
|
|
42
|
+
readonly queueLabel?: string;
|
|
41
43
|
}
|
|
42
44
|
export interface TaskCandidate {
|
|
43
45
|
readonly issueNumber: number;
|