@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.
@@ -457,8 +457,8 @@
457
457
  "sha256": "5fe866e54f796975dec5d8ba9983aefd86074db212d3fccd64eed04bc9f0b3da",
458
458
  "bytes": 8011
459
459
  },
460
- "console-ui/assets/index-BQFhoMcY.js": {
461
- "sha256": "722cfaf2b1b513c79b9f4d62cc79bd0f59c953c959480a8745efbdccf80df3d8",
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": "b4d9928edd90bb6778c3685eb511c9e7600937720b6212e018a017fbbbe7b8c5",
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": "64e3ba7e31bf9965b74645d7157a85fb67d9df183696a4d792cec870b22047e0",
530
- "bytes": 554
529
+ "sha256": "fbad9d91d84d2112b273175618686489a7f106385e0e62d6cab80804d6d0f2d7",
530
+ "bytes": 708
531
531
  },
532
532
  "coordinators/modes/implement-shared.js": {
533
- "sha256": "c39e7b1c088663a75f86b2dd81326940537c53408c7182a55f2b1288796cd002",
534
- "bytes": 11023
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": "73270636634fb8673c63b0c0347e6e5d7f676606d36b87a4c1aadc11f1939eb2",
1622
- "bytes": 7340
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": "bff922c1b435da55e02b4b11d3fde9f06e999f13b8ac0494788cac4a4fc4c432",
1658
- "bytes": 766
1657
+ "sha256": "4dd68c2b880f808aa67c3b6be1867299c0b49ce5cf7e39f4f093dbd341d1a1e8",
1658
+ "bytes": 800
1659
1659
  },
1660
1660
  "trigger/github-queue-config.js": {
1661
- "sha256": "a0ec09a7725ac1ee1adb9d41fdcb4c9545128f0b9515c6de4df9535d4bec1acc",
1662
- "bytes": 6857
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": "ec3c73739c9dfbbe3b2d7a55237fd35a290d6aab7bbc30ec5408bde69b2c95e0",
1694
- "bytes": 19769
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": "347ce68b2b6d14be5277488e7d54ac0d9e80173407ee12dbb2f7df90765bac3c",
1702
- "bytes": 10668
1701
+ "sha256": "1c73115b26d9274c86fd3b1e19f5eae2754d58e248949b6eaf0e7b7074b91c8c",
1702
+ "bytes": 25421
1703
1703
  },
1704
1704
  "trigger/trigger-router.d.ts": {
1705
- "sha256": "b3154af4d32d6d71ae38fb6befc54850d137a12b2f602a34247b0c5f9b57a29d",
1706
- "bytes": 2519
1705
+ "sha256": "44f1bfefafee4955bad74db9e61ecb6a49ea426c46100753f6bdb3a7f7b583dd",
1706
+ "bytes": 2671
1707
1707
  },
1708
1708
  "trigger/trigger-router.js": {
1709
- "sha256": "682bf6e3a9d0fa5e25378c361813fdc52e5b828dff7267fd084d83f242585492",
1710
- "bytes": 18144
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": "8015b54da7ab5b1cec78e1ecea099d5b457eaa5de267bb1ed52cbb75eca207c4",
1718
- "bytes": 38148
1717
+ "sha256": "8e85e0bdbd1596e70c23667e84ed380d6f0cee10028cd1a41163cda3467c2bdc",
1718
+ "bytes": 38559
1719
1719
  },
1720
1720
  "trigger/types.d.ts": {
1721
- "sha256": "a1336ad769dbe4760e7acd3b2a92961251492aa29245b5d906ad89011ea934fa",
1722
- "bytes": 3587
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": "3190c4c99a6f7fe85d56b4e2f16ee2239d5f8b1addf995b76359f231909cc3d3",
2266
- "bytes": 1562
2265
+ "sha256": "2b9f7ae6b3fafe6c26f266bb249c8c286144eb1f753c803d4358ef81e30d1736",
2266
+ "bytes": 2211
2267
2267
  },
2268
2268
  "v2/durable-core/schemas/artifacts/review-verdict.js": {
2269
- "sha256": "72915617cb08834e37a65c45b3eb84ff04c84d6e4a12c9e87be5a90a3d80cee5",
2270
- "bytes": 1206
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.dispatch(workflowTrigger);
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
- constructor(index: ReadonlyMap<string, TriggerDefinition>, ctx: V2ToolContext, apiKey: string, runWorkflowFn: RunWorkflowFn, execFn?: ExecFn, maxConcurrentSessions?: number, emitter?: DaemonEventEmitter, notificationService?: NotificationService, steerRegistry?: SteerRegistry);
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, coordinatorDeps: AdaptiveCoordinatorDeps, modeExecutors: ModeExecutors, context?: Readonly<Record<string, unknown>>): ReturnType<typeof runAdaptivePipeline>;
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, context) {
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)(coordinatorDeps, opts, modeExecutors);
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) {
@@ -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;