@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.
@@ -0,0 +1,155 @@
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.WORKRAIL_CONFIG_PATH = void 0;
37
+ exports.loadQueueConfig = loadQueueConfig;
38
+ const fs = __importStar(require("node:fs/promises"));
39
+ const path = __importStar(require("node:path"));
40
+ const os = __importStar(require("node:os"));
41
+ const result_js_1 = require("../runtime/result.js");
42
+ exports.WORKRAIL_CONFIG_PATH = path.join(os.homedir(), '.workrail', 'config.json');
43
+ async function loadQueueConfig(configPath = exports.WORKRAIL_CONFIG_PATH, env = process.env) {
44
+ let raw;
45
+ try {
46
+ raw = await fs.readFile(configPath, 'utf8');
47
+ }
48
+ catch (e) {
49
+ const error = e;
50
+ if (error.code === 'ENOENT') {
51
+ return (0, result_js_1.ok)(null);
52
+ }
53
+ return (0, result_js_1.err)(`Failed to read config file at ${configPath}: ${error.message ?? String(e)}`);
54
+ }
55
+ let parsed;
56
+ try {
57
+ parsed = JSON.parse(raw);
58
+ }
59
+ catch (e) {
60
+ return (0, result_js_1.err)(`Failed to parse config JSON at ${configPath}: ${e instanceof Error ? e.message : String(e)}`);
61
+ }
62
+ if (typeof parsed !== 'object' || parsed === null) {
63
+ return (0, result_js_1.err)(`Config at ${configPath} is not a JSON object`);
64
+ }
65
+ const config = parsed;
66
+ if (!('queue' in config)) {
67
+ return (0, result_js_1.ok)(null);
68
+ }
69
+ const queue = config['queue'];
70
+ if (typeof queue !== 'object' || queue === null) {
71
+ return (0, result_js_1.err)('config.queue is not an object');
72
+ }
73
+ const q = queue;
74
+ const rawType = q['type'];
75
+ if (typeof rawType !== 'string') {
76
+ return (0, result_js_1.err)('config.queue.type is required and must be a string');
77
+ }
78
+ const VALID_TYPES = new Set(['assignee', 'label', 'mention', 'query']);
79
+ if (!VALID_TYPES.has(rawType)) {
80
+ return (0, result_js_1.err)(`config.queue.type must be one of: assignee, label, mention, query. Got: "${rawType}"`);
81
+ }
82
+ const rawRepo = q['repo'];
83
+ if (typeof rawRepo !== 'string' || !rawRepo.trim()) {
84
+ return (0, result_js_1.err)('config.queue.repo is required and must be a non-empty string');
85
+ }
86
+ const rawToken = q['token'];
87
+ if (typeof rawToken !== 'string' || !rawToken.trim()) {
88
+ return (0, result_js_1.err)('config.queue.token is required and must be a non-empty string');
89
+ }
90
+ let resolvedToken;
91
+ if (rawToken.startsWith('$')) {
92
+ const envVarName = rawToken.slice(1);
93
+ const envValue = env[envVarName];
94
+ if (!envValue) {
95
+ return (0, result_js_1.err)(`config.queue.token references env var $${envVarName} which is unset or empty`);
96
+ }
97
+ resolvedToken = envValue;
98
+ }
99
+ else {
100
+ resolvedToken = rawToken.trim();
101
+ }
102
+ const rawPollInterval = q['pollIntervalSeconds'];
103
+ let pollIntervalSeconds = 300;
104
+ if (rawPollInterval !== undefined) {
105
+ if (typeof rawPollInterval !== 'number' || !Number.isInteger(rawPollInterval) || rawPollInterval <= 0) {
106
+ return (0, result_js_1.err)('config.queue.pollIntervalSeconds must be a positive integer');
107
+ }
108
+ pollIntervalSeconds = rawPollInterval;
109
+ }
110
+ const rawMaxConcurrent = q['maxTotalConcurrentSessions'] ?? q['maxConcurrentSelf'];
111
+ let maxTotalConcurrentSessions = 1;
112
+ if (rawMaxConcurrent !== undefined) {
113
+ if (typeof rawMaxConcurrent !== 'number' || !Number.isInteger(rawMaxConcurrent) || rawMaxConcurrent <= 0) {
114
+ return (0, result_js_1.err)('config.queue.maxTotalConcurrentSessions must be a positive integer');
115
+ }
116
+ maxTotalConcurrentSessions = rawMaxConcurrent;
117
+ }
118
+ const rawExcludeLabels = q['excludeLabels'];
119
+ let excludeLabels = [];
120
+ if (rawExcludeLabels !== undefined) {
121
+ if (!Array.isArray(rawExcludeLabels) || !rawExcludeLabels.every(l => typeof l === 'string')) {
122
+ return (0, result_js_1.err)('config.queue.excludeLabels must be an array of strings');
123
+ }
124
+ excludeLabels = rawExcludeLabels;
125
+ }
126
+ const rawUser = q['user'];
127
+ const user = typeof rawUser === 'string' && rawUser.trim() ? rawUser.trim() : undefined;
128
+ const rawName = q['name'];
129
+ const labelName = typeof rawName === 'string' && rawName.trim() ? rawName.trim() : undefined;
130
+ const rawHandle = q['handle'];
131
+ const handle = typeof rawHandle === 'string' && rawHandle.trim() ? rawHandle.trim() : undefined;
132
+ const rawSearch = q['search'];
133
+ const search = typeof rawSearch === 'string' && rawSearch.trim() ? rawSearch.trim() : undefined;
134
+ const rawWorkOnAll = q['workOnAll'];
135
+ const workOnAll = rawWorkOnAll === true;
136
+ const rawBotName = q['botName'];
137
+ const botName = typeof rawBotName === 'string' && rawBotName.trim() ? rawBotName.trim() : undefined;
138
+ const rawBotEmail = q['botEmail'];
139
+ const botEmail = typeof rawBotEmail === 'string' && rawBotEmail.trim() ? rawBotEmail.trim() : undefined;
140
+ return (0, result_js_1.ok)({
141
+ type: rawType,
142
+ ...(user !== undefined ? { user } : {}),
143
+ ...(labelName !== undefined ? { name: labelName } : {}),
144
+ ...(handle !== undefined ? { handle } : {}),
145
+ ...(search !== undefined ? { search } : {}),
146
+ ...(workOnAll ? { workOnAll } : {}),
147
+ pollIntervalSeconds,
148
+ maxTotalConcurrentSessions,
149
+ excludeLabels,
150
+ repo: rawRepo.trim(),
151
+ token: resolvedToken,
152
+ ...(botName !== undefined ? { botName } : {}),
153
+ ...(botEmail !== undefined ? { botEmail } : {}),
154
+ });
155
+ }
@@ -21,7 +21,7 @@ export interface NotificationConfig {
21
21
  export interface NotificationPayload {
22
22
  readonly event: 'session_completed';
23
23
  readonly workflowId: string;
24
- readonly outcome: 'success' | 'error' | 'timeout' | 'delivery_failed';
24
+ readonly outcome: 'success' | 'error' | 'timeout' | 'stuck' | 'delivery_failed';
25
25
  readonly detail: string;
26
26
  readonly goal: string;
27
27
  readonly timestamp: string;
@@ -48,6 +48,8 @@ function buildNotificationBody(result, goal) {
48
48
  return `Session failed: ${truncated}`;
49
49
  case 'timeout':
50
50
  return `Session timed out: ${truncated}`;
51
+ case 'stuck':
52
+ return `Session stuck (${result.reason}): ${truncated}`;
51
53
  case 'delivery_failed':
52
54
  return `Session completed but result delivery failed: ${truncated}`;
53
55
  }
@@ -63,6 +65,8 @@ function buildDetail(result) {
63
65
  return result.message;
64
66
  case 'timeout':
65
67
  return result.message;
68
+ case 'stuck':
69
+ return result.message;
66
70
  case 'delivery_failed':
67
71
  return `stopReason: ${result.stopReason}; deliveryError: ${result.deliveryError}`;
68
72
  }
@@ -17,4 +17,5 @@ export declare class PollingScheduler {
17
17
  private doPollGitLab;
18
18
  private doPollGitHub;
19
19
  private dispatchAndRecord;
20
+ private doPollGitHubQueue;
20
21
  }
@@ -1,8 +1,46 @@
1
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
+ })();
2
35
  Object.defineProperty(exports, "__esModule", { value: true });
3
36
  exports.PollingScheduler = void 0;
4
37
  const gitlab_poller_js_1 = require("./adapters/gitlab-poller.js");
5
38
  const github_poller_js_1 = require("./adapters/github-poller.js");
39
+ const github_queue_poller_js_1 = require("./adapters/github-queue-poller.js");
40
+ const github_queue_config_js_1 = require("./github-queue-config.js");
41
+ const fs = __importStar(require("node:fs/promises"));
42
+ const os = __importStar(require("node:os"));
43
+ const path = __importStar(require("node:path"));
6
44
  function isPollingTrigger(trigger) {
7
45
  return trigger.pollingSource !== undefined;
8
46
  }
@@ -78,6 +116,9 @@ class PollingScheduler {
78
116
  case 'github_prs_poll':
79
117
  await this.doPollGitHub(trigger, triggerId, pollStartAt, lastPollAt, trigger.pollingSource, 'prs');
80
118
  break;
119
+ case 'github_queue_poll':
120
+ await this.doPollGitHubQueue(trigger, triggerId, trigger.pollingSource);
121
+ break;
81
122
  default: {
82
123
  const _exhaustive = trigger.pollingSource;
83
124
  console.warn(`[PollingScheduler] Unknown provider '${String(_exhaustive.provider)}' ` +
@@ -150,6 +191,111 @@ class PollingScheduler {
150
191
  console.log(`[PollingScheduler] Dispatched ${newIds.length} new event(s) for trigger '${triggerId}'`);
151
192
  }
152
193
  }
194
+ async doPollGitHubQueue(trigger, triggerId, source) {
195
+ const cycleStart = Date.now();
196
+ const configResult = await (0, github_queue_config_js_1.loadQueueConfig)();
197
+ if (configResult.kind === 'err') {
198
+ console.warn(`[QueuePoll] Failed to load queue config for trigger '${triggerId}': ${configResult.error}. Skipping cycle.`);
199
+ return;
200
+ }
201
+ const queueConfig = configResult.value;
202
+ if (queueConfig === null)
203
+ return;
204
+ if (queueConfig.type !== 'assignee') {
205
+ console.error(`[QueuePoll] Queue type '${queueConfig.type}' is not implemented. Only 'assignee' is supported. Skipping cycle.`);
206
+ await appendQueuePollLog({ event: 'poll_cycle_complete', triggerId, reason: 'not_implemented', queueType: queueConfig.type, ts: new Date().toISOString() });
207
+ return;
208
+ }
209
+ const sessionsDir = path.join(os.homedir(), '.workrail', 'daemon-sessions');
210
+ const activeSessions = await countActiveSessions(sessionsDir);
211
+ if (activeSessions >= queueConfig.maxTotalConcurrentSessions) {
212
+ console.log(`[QueuePoll] Skipping cycle: active sessions (${activeSessions}) >= maxTotalConcurrentSessions (${queueConfig.maxTotalConcurrentSessions}).`);
213
+ await appendQueuePollLog({ event: 'poll_cycle_skipped', triggerId, reason: 'max_concurrency_reached', activeSessions, maxTotalConcurrentSessions: queueConfig.maxTotalConcurrentSessions, ts: new Date().toISOString() });
214
+ return;
215
+ }
216
+ const fetchResult = await (0, github_queue_poller_js_1.pollGitHubQueueIssues)(source, queueConfig, this.fetchFn);
217
+ if (fetchResult.kind === 'err') {
218
+ console.warn(`[QueuePoll] GitHub API error for trigger '${triggerId}': ${fetchResult.error.kind}. Skipping cycle.`);
219
+ return;
220
+ }
221
+ const issues = fetchResult.value;
222
+ console.log(`[QueuePoll] cycle start repo=${source.repo} issues_fetched=${issues.length}`);
223
+ const candidates = [];
224
+ const skipped = [];
225
+ for (const issue of issues) {
226
+ const issueLabels = issue.labels.map((l) => l.name);
227
+ const excludedLabel = queueConfig.excludeLabels.find((el) => issueLabels.includes(el));
228
+ if (excludedLabel) {
229
+ skipped.push({ issue, reason: `excluded_label: ${excludedLabel}` });
230
+ continue;
231
+ }
232
+ if (issueLabels.includes('worktrain:in-progress')) {
233
+ skipped.push({ issue, reason: 'active_session_or_in_progress' });
234
+ continue;
235
+ }
236
+ if (/sess_[a-z0-9]+/.test(issue.body)) {
237
+ skipped.push({ issue, reason: 'active_session_or_in_progress' });
238
+ continue;
239
+ }
240
+ const idempotencyStatus = await (0, github_queue_poller_js_1.checkIdempotency)(issue.number, sessionsDir);
241
+ if (idempotencyStatus === 'active') {
242
+ skipped.push({ issue, reason: 'active_session' });
243
+ continue;
244
+ }
245
+ const maturity = (0, github_queue_poller_js_1.inferMaturity)(issue.body);
246
+ candidates.push({ issue, maturity });
247
+ }
248
+ const MATURITY_RANK = { ready: 0, specced: 1, idea: 2 };
249
+ candidates.sort((a, b) => {
250
+ const rankDiff = (MATURITY_RANK[a.maturity] ?? 2) - (MATURITY_RANK[b.maturity] ?? 2);
251
+ return rankDiff !== 0 ? rankDiff : a.issue.number - b.issue.number;
252
+ });
253
+ for (const { issue, reason } of skipped) {
254
+ console.log(`[QueuePoll] skipped #${issue.number} "${issue.title}" reason=${reason}`);
255
+ await appendQueuePollLog({ event: 'task_skipped', issueNumber: issue.number, title: issue.title, reason, ts: new Date().toISOString() });
256
+ }
257
+ if (candidates.length === 0) {
258
+ console.log('[QueuePoll] No actionable issues found in this poll cycle.');
259
+ await appendQueuePollLog({ event: 'poll_cycle_complete', selected: 0, skipped: skipped.length, elapsed: Date.now() - cycleStart, ts: new Date().toISOString() });
260
+ return;
261
+ }
262
+ const top = candidates[0];
263
+ const upstreamSpecUrl = extractUpstreamSpecUrl(top.issue.body);
264
+ const taskCandidate = {
265
+ issueNumber: top.issue.number,
266
+ title: top.issue.title,
267
+ body: top.issue.body,
268
+ url: top.issue.url,
269
+ inferredMaturity: top.maturity,
270
+ ...(upstreamSpecUrl !== undefined ? { upstreamSpecUrl } : {}),
271
+ queueConfigType: queueConfig.type,
272
+ };
273
+ const workflowTrigger = {
274
+ workflowId: trigger.workflowId,
275
+ goal: top.issue.title,
276
+ workspacePath: trigger.workspacePath,
277
+ context: { taskCandidate },
278
+ ...(trigger.referenceUrls !== undefined ? { referenceUrls: trigger.referenceUrls } : {}),
279
+ ...(trigger.agentConfig !== undefined ? { agentConfig: trigger.agentConfig } : {}),
280
+ ...(trigger.soulFile !== undefined ? { soulFile: trigger.soulFile } : {}),
281
+ botIdentity: {
282
+ name: queueConfig.botName ?? 'worktrain',
283
+ email: queueConfig.botEmail ?? 'worktrain@users.noreply.github.com',
284
+ },
285
+ };
286
+ const maturityReason = describeMaturityReason(top.maturity);
287
+ console.log(`[QueuePoll] selected #${top.issue.number} "${top.issue.title}" maturity=${top.maturity} reason="${maturityReason}"`);
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);
290
+ for (let i = 1; i < candidates.length; i++) {
291
+ const { issue, maturity } = candidates[i];
292
+ console.log(`[QueuePoll] skipped #${issue.number} "${issue.title}" reason=lower_priority_${maturity}`);
293
+ await appendQueuePollLog({ event: 'task_skipped', issueNumber: issue.number, title: issue.title, inferredMaturity: maturity, reason: `lower_priority_${maturity}`, ts: new Date().toISOString() });
294
+ }
295
+ const elapsed = Date.now() - cycleStart;
296
+ console.log(`[QueuePoll] cycle complete selected=1 skipped=${skipped.length + candidates.length - 1} elapsed=${elapsed}ms`);
297
+ await appendQueuePollLog({ event: 'poll_cycle_complete', selected: 1, skipped: skipped.length + candidates.length - 1, elapsed, ts: new Date().toISOString() });
298
+ }
153
299
  }
154
300
  exports.PollingScheduler = PollingScheduler;
155
301
  function buildGitLabWorkflowTrigger(trigger, mr) {
@@ -232,12 +378,12 @@ function interpolateGoalFromPayload(trigger, payload) {
232
378
  return template.replace(/\{\{([^}]+)\}\}/g, (_, token) => resolved.get(token) ?? trigger.goal);
233
379
  }
234
380
  function extractDotPath(obj, rawPath) {
235
- let path = rawPath.trim();
236
- if (path.startsWith('$.'))
237
- path = path.slice(2);
238
- else if (path.startsWith('$'))
239
- path = path.slice(1);
240
- const segments = path.split('.');
381
+ let dotPath = rawPath.trim();
382
+ if (dotPath.startsWith('$.'))
383
+ dotPath = dotPath.slice(2);
384
+ else if (dotPath.startsWith('$'))
385
+ dotPath = dotPath.slice(1);
386
+ const segments = dotPath.split('.');
241
387
  let current = obj;
242
388
  for (const segment of segments) {
243
389
  if (segment.includes('[') || current === null || typeof current !== 'object') {
@@ -247,3 +393,36 @@ function extractDotPath(obj, rawPath) {
247
393
  }
248
394
  return current;
249
395
  }
396
+ async function countActiveSessions(sessionsDir) {
397
+ try {
398
+ const files = await fs.readdir(sessionsDir);
399
+ return files.filter((f) => f.endsWith('.json')).length;
400
+ }
401
+ catch {
402
+ return 0;
403
+ }
404
+ }
405
+ async function appendQueuePollLog(entry) {
406
+ const logPath = path.join(os.homedir(), '.workrail', 'queue-poll.jsonl');
407
+ try {
408
+ await fs.appendFile(logPath, JSON.stringify(entry) + '\n', 'utf8');
409
+ }
410
+ catch (e) {
411
+ console.warn(`[QueuePoll] Failed to write queue-poll.jsonl: ${e instanceof Error ? e.message : String(e)}`);
412
+ }
413
+ }
414
+ function extractUpstreamSpecUrl(body) {
415
+ const specLineMatch = /upstream_spec:\s*(https?:\/\/\S+)/i.exec(body);
416
+ if (specLineMatch?.[1])
417
+ return specLineMatch[1];
418
+ const firstPara = body.split(/\n\s*\n/)[0] ?? '';
419
+ const urlMatch = /(https?:\/\/\S+)/.exec(firstPara);
420
+ return urlMatch?.[1];
421
+ }
422
+ function describeMaturityReason(maturity) {
423
+ switch (maturity) {
424
+ case 'ready': return 'has upstream spec URL';
425
+ case 'specced': return 'has acceptance criteria or checklist';
426
+ case 'idea': return 'maturity=idea, no upstream spec';
427
+ }
428
+ }
@@ -327,6 +327,10 @@ class TriggerRouter {
327
327
  console.log(`[TriggerRouter] Workflow failed: triggerId=${trigger.id} ` +
328
328
  `workflowId=${trigger.workflowId} error=${result.message} stopReason=${result.stopReason}`);
329
329
  }
330
+ else if (result._tag === 'stuck') {
331
+ console.log(`[TriggerRouter] Workflow stuck: triggerId=${trigger.id} ` +
332
+ `workflowId=${trigger.workflowId} reason=${result.reason} message=${result.message}`);
333
+ }
330
334
  else {
331
335
  (0, assert_never_js_1.assertNever)(result);
332
336
  }
@@ -366,6 +370,10 @@ class TriggerRouter {
366
370
  console.log(`[TriggerRouter] Dispatch failed: workflowId=${workflowTrigger.workflowId} ` +
367
371
  `error=${result.message} stopReason=${result.stopReason}`);
368
372
  }
373
+ else if (result._tag === 'stuck') {
374
+ console.log(`[TriggerRouter] Dispatch stuck: workflowId=${workflowTrigger.workflowId} ` +
375
+ `reason=${result.reason} message=${result.message}`);
376
+ }
369
377
  else {
370
378
  (0, assert_never_js_1.assertNever)(result);
371
379
  }
@@ -41,7 +41,7 @@ const path = __importStar(require("node:path"));
41
41
  const fs = __importStar(require("node:fs/promises"));
42
42
  const result_js_1 = require("../runtime/result.js");
43
43
  const types_js_1 = require("./types.js");
44
- const SUPPORTED_PROVIDERS = new Set(['generic', 'gitlab_poll', 'github_issues_poll', 'github_prs_poll']);
44
+ const SUPPORTED_PROVIDERS = new Set(['generic', 'gitlab_poll', 'github_issues_poll', 'github_prs_poll', 'github_queue_poll']);
45
45
  function unquoteYamlScalar(raw) {
46
46
  const s = raw.trim();
47
47
  if ((s.startsWith('"') && s.endsWith('"')) ||
@@ -754,10 +754,43 @@ function validateAndResolveTrigger(raw, env, workspaces = {}) {
754
754
  };
755
755
  }
756
756
  }
757
+ else if (provider === 'github_queue_poll') {
758
+ if (!raw.source) {
759
+ return (0, result_js_1.err)({ kind: 'missing_field', field: 'source', triggerId: rawId });
760
+ }
761
+ const queueSrc = raw.source;
762
+ if (!queueSrc.repo?.trim()) {
763
+ return (0, result_js_1.err)({ kind: 'missing_field', field: 'source.repo', triggerId: rawId });
764
+ }
765
+ if (!queueSrc.token?.trim()) {
766
+ return (0, result_js_1.err)({ kind: 'missing_field', field: 'source.token', triggerId: rawId });
767
+ }
768
+ const queueTokenResult = resolveSecret(queueSrc.token.trim(), rawId, env);
769
+ if (queueTokenResult.kind === 'err')
770
+ return queueTokenResult;
771
+ let queuePollIntervalSeconds = 300;
772
+ if (queueSrc.pollIntervalSeconds?.trim()) {
773
+ const asNumber = Number(queueSrc.pollIntervalSeconds.trim());
774
+ if (!Number.isInteger(asNumber) || asNumber <= 0) {
775
+ return (0, result_js_1.err)({
776
+ kind: 'invalid_field_value',
777
+ field: `source.pollIntervalSeconds (must be a positive integer, got: ${queueSrc.pollIntervalSeconds})`,
778
+ triggerId: rawId,
779
+ });
780
+ }
781
+ queuePollIntervalSeconds = asNumber;
782
+ }
783
+ pollingSource = {
784
+ provider: 'github_queue_poll',
785
+ repo: queueSrc.repo.trim(),
786
+ token: queueTokenResult.value,
787
+ pollIntervalSeconds: queuePollIntervalSeconds,
788
+ };
789
+ }
757
790
  else if (raw.source) {
758
791
  console.warn(`[TriggerStore] WARNING: trigger '${rawId}' has provider='${provider}' but also ` +
759
792
  `defines a source: block. The source: block is only used for polling providers ` +
760
- `(gitlab_poll, github_issues_poll, github_prs_poll). It will be ignored for this trigger.`);
793
+ `(gitlab_poll, github_issues_poll, github_prs_poll, github_queue_poll). It will be ignored for this trigger.`);
761
794
  }
762
795
  const trigger = {
763
796
  id: (0, types_js_1.asTriggerId)(rawId),
@@ -34,12 +34,28 @@ export interface GitHubPollingSource {
34
34
  readonly notLabels: readonly string[];
35
35
  readonly labelFilter: readonly string[];
36
36
  }
37
+ export interface GitHubQueuePollingSource {
38
+ readonly repo: string;
39
+ readonly token: string;
40
+ readonly pollIntervalSeconds: number;
41
+ }
42
+ export interface TaskCandidate {
43
+ readonly issueNumber: number;
44
+ readonly title: string;
45
+ readonly body: string;
46
+ readonly url: string;
47
+ readonly inferredMaturity: 'idea' | 'specced' | 'ready';
48
+ readonly upstreamSpecUrl?: string;
49
+ readonly queueConfigType: 'assignee' | 'label' | 'mention' | 'query';
50
+ }
37
51
  export type PollingSource = (GitLabPollingSource & {
38
52
  readonly provider: 'gitlab_poll';
39
53
  }) | (GitHubPollingSource & {
40
54
  readonly provider: 'github_issues_poll';
41
55
  }) | (GitHubPollingSource & {
42
56
  readonly provider: 'github_prs_poll';
57
+ }) | (GitHubQueuePollingSource & {
58
+ readonly provider: 'github_queue_poll';
43
59
  });
44
60
  export interface TriggerDefinition {
45
61
  readonly id: TriggerId;
@@ -55,6 +71,8 @@ export interface TriggerDefinition {
55
71
  readonly model?: string;
56
72
  readonly maxSessionMinutes?: number;
57
73
  readonly maxTurns?: number;
74
+ readonly stuckAbortPolicy?: 'abort' | 'notify_only';
75
+ readonly noProgressAbortEnabled?: boolean;
58
76
  };
59
77
  readonly concurrencyMode: 'serial' | 'parallel';
60
78
  readonly callbackUrl?: string;
@@ -604,6 +604,9 @@ function mountConsoleRoutes(app, consoleService, workflowService, timingRingBuff
604
604
  else if (result._tag === 'error') {
605
605
  console.log(`[ConsoleRoutes] Auto dispatch failed: workflowId=${workflowId} error=${result.message}`);
606
606
  }
607
+ else if (result._tag === 'stuck') {
608
+ console.log(`[ConsoleRoutes] Auto dispatch stuck: workflowId=${workflowId} reason=${result.reason} message=${result.message}`);
609
+ }
607
610
  else {
608
611
  (0, assert_never_js_1.assertNever)(result);
609
612
  }