@exaudeus/workrail 3.43.0 → 3.44.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-Bi38ITiQ.js} +1 -1
- package/dist/console-ui/index.html +1 -1
- package/dist/daemon/workflow-runner.d.ts +4 -0
- package/dist/daemon/workflow-runner.js +15 -0
- package/dist/manifest.json +33 -17
- 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/polling-scheduler.d.ts +1 -0
- package/dist/trigger/polling-scheduler.js +185 -6
- package/dist/trigger/trigger-store.js +35 -2
- package/dist/trigger/types.d.ts +16 -0
- package/package.json +1 -1
|
@@ -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
|
|
236
|
-
if (
|
|
237
|
-
|
|
238
|
-
else if (
|
|
239
|
-
|
|
240
|
-
const segments =
|
|
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
|
+
}
|
|
@@ -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),
|
package/dist/trigger/types.d.ts
CHANGED
|
@@ -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;
|