@exaudeus/workrail 3.42.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-DwfWMKvv.js → index-Bi38ITiQ.js} +1 -1
- package/dist/console-ui/index.html +1 -1
- package/dist/daemon/workflow-runner.d.ts +15 -1
- package/dist/daemon/workflow-runner.js +86 -9
- package/dist/manifest.json +39 -23
- package/dist/trigger/adapters/github-queue-poller.d.ts +34 -0
- package/dist/trigger/adapters/github-queue-poller.js +200 -0
- package/dist/trigger/delivery-action.d.ts +2 -0
- package/dist/trigger/delivery-action.js +24 -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-router.js +24 -1
- package/dist/trigger/trigger-store.js +77 -2
- package/dist/trigger/types.d.ts +19 -0
- package/docs/design/adaptive-coordinator-context-candidates.md +265 -0
- package/docs/design/adaptive-coordinator-context-review.md +101 -0
- package/docs/design/adaptive-coordinator-context.md +504 -0
- package/docs/design/adaptive-coordinator-routing-candidates.md +340 -0
- package/docs/design/adaptive-coordinator-routing-design-review.md +135 -0
- package/docs/design/adaptive-coordinator-routing-review.md +156 -0
- package/docs/design/adaptive-coordinator-routing.md +660 -0
- package/docs/design/context-assembly-layer-design-review.md +110 -0
- package/docs/design/context-assembly-layer.md +622 -0
- package/docs/design/stuck-escalation-candidates.md +176 -0
- package/docs/design/stuck-escalation-design-review.md +70 -0
- package/docs/design/stuck-escalation.md +326 -0
- package/docs/design/worktrain-task-queue-candidates.md +252 -0
- package/docs/design/worktrain-task-queue-design-review.md +109 -0
- package/docs/design/worktrain-task-queue.md +443 -0
- package/docs/design/worktree-review-findings-candidates.md +101 -0
- package/docs/design/worktree-review-findings-design-review.md +65 -0
- package/docs/design/worktree-review-findings-implementation-plan.md +153 -0
- package/docs/ideas/backlog.md +148 -0
- package/package.json +3 -3
|
@@ -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
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -136,7 +136,17 @@ async function maybeRunDelivery(triggerId, trigger, result, execFn) {
|
|
|
136
136
|
`Ensure the workflow's final step produces a JSON block with commitType, filesChanged, etc.`);
|
|
137
137
|
return;
|
|
138
138
|
}
|
|
139
|
-
const
|
|
139
|
+
const deliveryCwd = result.sessionWorkspacePath ?? trigger.workspacePath;
|
|
140
|
+
const deliveryResult = await (0, delivery_action_js_1.runDelivery)(parseResult.value, deliveryCwd, {
|
|
141
|
+
autoCommit: trigger.autoCommit,
|
|
142
|
+
autoOpenPR: trigger.autoOpenPR,
|
|
143
|
+
...(trigger.branchStrategy === 'worktree' && result.sessionWorkspacePath
|
|
144
|
+
? {
|
|
145
|
+
sessionId: result.sessionId ?? '',
|
|
146
|
+
branchPrefix: trigger.branchPrefix ?? 'worktrain/',
|
|
147
|
+
}
|
|
148
|
+
: {}),
|
|
149
|
+
}, execFn);
|
|
140
150
|
switch (deliveryResult._tag) {
|
|
141
151
|
case 'committed':
|
|
142
152
|
console.log(`[TriggerRouter] Delivery committed: triggerId=${triggerId} sha=${deliveryResult.sha}`);
|
|
@@ -152,6 +162,16 @@ async function maybeRunDelivery(triggerId, trigger, result, execFn) {
|
|
|
152
162
|
`details=${deliveryResult.details}`);
|
|
153
163
|
break;
|
|
154
164
|
}
|
|
165
|
+
if (trigger.branchStrategy === 'worktree' && result.sessionWorkspacePath) {
|
|
166
|
+
try {
|
|
167
|
+
await execFn('git', ['-C', trigger.workspacePath, 'worktree', 'remove', '--force', result.sessionWorkspacePath], { cwd: trigger.workspacePath, timeout: 60000 });
|
|
168
|
+
console.log(`[TriggerRouter] Worktree removed: triggerId=${triggerId} path=${result.sessionWorkspacePath}`);
|
|
169
|
+
}
|
|
170
|
+
catch (err) {
|
|
171
|
+
console.warn(`[TriggerRouter] Could not remove worktree: triggerId=${triggerId} ` +
|
|
172
|
+
`path=${result.sessionWorkspacePath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
155
175
|
}
|
|
156
176
|
class Semaphore {
|
|
157
177
|
constructor(max) {
|
|
@@ -246,6 +266,9 @@ class TriggerRouter {
|
|
|
246
266
|
...(trigger.referenceUrls !== undefined ? { referenceUrls: trigger.referenceUrls } : {}),
|
|
247
267
|
...(trigger.agentConfig !== undefined ? { agentConfig: trigger.agentConfig } : {}),
|
|
248
268
|
...(trigger.soulFile !== undefined ? { soulFile: trigger.soulFile } : {}),
|
|
269
|
+
...(trigger.branchStrategy !== undefined ? { branchStrategy: trigger.branchStrategy } : {}),
|
|
270
|
+
...(trigger.baseBranch !== undefined ? { baseBranch: trigger.baseBranch } : {}),
|
|
271
|
+
...(trigger.branchPrefix !== undefined ? { branchPrefix: trigger.branchPrefix } : {}),
|
|
249
272
|
};
|
|
250
273
|
this.emitter?.emit({ kind: 'trigger_fired', triggerId: trigger.id, workflowId: trigger.workflowId });
|
|
251
274
|
const queueKey = trigger.concurrencyMode === 'parallel'
|
|
@@ -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('"')) ||
|
|
@@ -391,6 +391,15 @@ function setTriggerField(trigger, key, value) {
|
|
|
391
391
|
case 'soulFile':
|
|
392
392
|
trigger.soulFile = value;
|
|
393
393
|
break;
|
|
394
|
+
case 'branchStrategy':
|
|
395
|
+
trigger.branchStrategy = value;
|
|
396
|
+
break;
|
|
397
|
+
case 'baseBranch':
|
|
398
|
+
trigger.baseBranch = value;
|
|
399
|
+
break;
|
|
400
|
+
case 'branchPrefix':
|
|
401
|
+
trigger.branchPrefix = value;
|
|
402
|
+
break;
|
|
394
403
|
default:
|
|
395
404
|
break;
|
|
396
405
|
}
|
|
@@ -602,6 +611,36 @@ function validateAndResolveTrigger(raw, env, workspaces = {}) {
|
|
|
602
611
|
console.warn(`[TriggerStore] Warning: trigger "${rawId}" has autoOpenPR: true but autoCommit is not true. ` +
|
|
603
612
|
`A PR requires a commit -- delivery will be skipped unless autoCommit is also set to true.`);
|
|
604
613
|
}
|
|
614
|
+
const rawBranchStrategy = raw.branchStrategy?.trim();
|
|
615
|
+
if (rawBranchStrategy !== undefined && rawBranchStrategy !== 'worktree' && rawBranchStrategy !== 'none') {
|
|
616
|
+
return (0, result_js_1.err)({
|
|
617
|
+
kind: 'invalid_field_value',
|
|
618
|
+
field: `branchStrategy (must be "worktree" or "none", got: "${rawBranchStrategy}")`,
|
|
619
|
+
triggerId: rawId,
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
const branchStrategy = rawBranchStrategy === 'worktree' ? 'worktree' : rawBranchStrategy === 'none' ? 'none' : undefined;
|
|
623
|
+
const baseBranch = raw.baseBranch?.trim() || undefined;
|
|
624
|
+
const branchPrefix = raw.branchPrefix?.trim() || undefined;
|
|
625
|
+
const GIT_SAFE_RE = /^[a-zA-Z0-9._/-]+$/;
|
|
626
|
+
if (baseBranch !== undefined) {
|
|
627
|
+
if (!GIT_SAFE_RE.test(baseBranch) || baseBranch.startsWith('-')) {
|
|
628
|
+
return (0, result_js_1.err)({
|
|
629
|
+
kind: 'invalid_field_value',
|
|
630
|
+
field: `baseBranch (must match /^[a-zA-Z0-9._/-]+$/ and not start with "-", got: "${baseBranch}")`,
|
|
631
|
+
triggerId: rawId,
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
if (branchPrefix !== undefined) {
|
|
636
|
+
if (!GIT_SAFE_RE.test(branchPrefix) || branchPrefix.startsWith('-')) {
|
|
637
|
+
return (0, result_js_1.err)({
|
|
638
|
+
kind: 'invalid_field_value',
|
|
639
|
+
field: `branchPrefix (must match /^[a-zA-Z0-9._/-]+$/ and not start with "-", got: "${branchPrefix}")`,
|
|
640
|
+
triggerId: rawId,
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
}
|
|
605
644
|
function parsePollIntervalSeconds(raw2, triggerId2) {
|
|
606
645
|
const intervalRaw = raw2.pollIntervalSeconds?.trim();
|
|
607
646
|
if (!intervalRaw)
|
|
@@ -715,10 +754,43 @@ function validateAndResolveTrigger(raw, env, workspaces = {}) {
|
|
|
715
754
|
};
|
|
716
755
|
}
|
|
717
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
|
+
}
|
|
718
790
|
else if (raw.source) {
|
|
719
791
|
console.warn(`[TriggerStore] WARNING: trigger '${rawId}' has provider='${provider}' but also ` +
|
|
720
792
|
`defines a source: block. The source: block is only used for polling providers ` +
|
|
721
|
-
`(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.`);
|
|
722
794
|
}
|
|
723
795
|
const trigger = {
|
|
724
796
|
id: (0, types_js_1.asTriggerId)(rawId),
|
|
@@ -741,6 +813,9 @@ function validateAndResolveTrigger(raw, env, workspaces = {}) {
|
|
|
741
813
|
...(pollingSource !== undefined ? { pollingSource } : {}),
|
|
742
814
|
...(resolvedWorkspaceName !== undefined ? { workspaceName: resolvedWorkspaceName } : {}),
|
|
743
815
|
...(resolvedSoulFile ? { soulFile: resolvedSoulFile } : {}),
|
|
816
|
+
...(branchStrategy !== undefined ? { branchStrategy } : {}),
|
|
817
|
+
...(baseBranch !== undefined ? { baseBranch } : {}),
|
|
818
|
+
...(branchPrefix !== undefined ? { branchPrefix } : {}),
|
|
744
819
|
};
|
|
745
820
|
return (0, result_js_1.ok)(trigger);
|
|
746
821
|
}
|
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;
|
|
@@ -68,6 +84,9 @@ export interface TriggerDefinition {
|
|
|
68
84
|
readonly pollingSource?: PollingSource;
|
|
69
85
|
readonly workspaceName?: WorkspaceName;
|
|
70
86
|
readonly soulFile?: string;
|
|
87
|
+
readonly branchStrategy?: 'worktree' | 'none';
|
|
88
|
+
readonly baseBranch?: string;
|
|
89
|
+
readonly branchPrefix?: string;
|
|
71
90
|
}
|
|
72
91
|
export interface TriggerConfig {
|
|
73
92
|
readonly triggers: readonly TriggerDefinition[];
|