@exaudeus/workrail 3.32.0 → 3.33.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/cli/commands/index.d.ts +1 -0
- package/dist/cli/commands/index.js +3 -1
- package/dist/cli/commands/worktrain-await.js +11 -9
- package/dist/cli/commands/worktrain-daemon-install.d.ts +35 -0
- package/dist/cli/commands/worktrain-daemon-install.js +291 -0
- package/dist/cli/commands/worktrain-daemon.d.ts +31 -0
- package/dist/cli/commands/worktrain-daemon.js +272 -0
- package/dist/cli/commands/worktrain-spawn.js +11 -9
- package/dist/cli-worktrain.js +329 -0
- package/dist/cli.js +1 -22
- package/dist/console/standalone-console.d.ts +28 -0
- package/dist/console/standalone-console.js +142 -0
- package/dist/{console/assets/index-Cb_LO718.js → console-ui/assets/index-BuJFLLfY.js} +1 -1
- package/dist/{console → console-ui}/index.html +1 -1
- package/dist/daemon/agent-loop.d.ts +26 -0
- package/dist/daemon/agent-loop.js +39 -1
- package/dist/daemon/daemon-events.d.ts +47 -1
- package/dist/daemon/workflow-runner.d.ts +3 -2
- package/dist/daemon/workflow-runner.js +205 -41
- package/dist/infrastructure/session/HttpServer.js +133 -34
- package/dist/manifest.json +118 -62
- package/dist/mcp/output-schemas.d.ts +30 -30
- package/dist/mcp/transports/bridge-events.d.ts +4 -0
- package/dist/mcp/transports/fatal-exit.js +4 -0
- package/dist/mcp/transports/http-entry.js +2 -0
- package/dist/mcp/transports/stdio-entry.js +26 -6
- package/dist/mcp/v2/tools.d.ts +4 -4
- package/dist/trigger/adapters/github-poller.d.ts +44 -0
- package/dist/trigger/adapters/github-poller.js +190 -0
- package/dist/trigger/adapters/gitlab-poller.d.ts +27 -0
- package/dist/trigger/adapters/gitlab-poller.js +81 -0
- package/dist/trigger/index.d.ts +4 -1
- package/dist/trigger/index.js +5 -1
- package/dist/trigger/polled-event-store.d.ts +22 -0
- package/dist/trigger/polled-event-store.js +173 -0
- package/dist/trigger/polling-scheduler.d.ts +20 -0
- package/dist/trigger/polling-scheduler.js +249 -0
- package/dist/trigger/trigger-listener.d.ts +3 -0
- package/dist/trigger/trigger-listener.js +47 -3
- package/dist/trigger/trigger-store.js +114 -33
- package/dist/trigger/types.d.ts +17 -1
- package/dist/v2/durable-core/schemas/export-bundle/index.d.ts +224 -224
- package/dist/v2/durable-core/schemas/session/events.d.ts +42 -42
- package/dist/v2/durable-core/schemas/session/manifest.d.ts +6 -6
- package/dist/v2/durable-core/schemas/session/validation-event.d.ts +2 -2
- package/dist/v2/durable-core/tokens/payloads.d.ts +52 -52
- package/dist/v2/usecases/console-routes.js +3 -3
- package/dist/v2/usecases/console-service.js +133 -9
- package/dist/v2/usecases/console-types.d.ts +7 -0
- package/docs/design/daemon-conversation-logging-plan.md +98 -0
- package/docs/design/daemon-conversation-logging-review.md +55 -0
- package/docs/design/daemon-conversation-logging.md +129 -0
- package/docs/design/github-polling-adapter-design-candidates.md +226 -0
- package/docs/design/github-polling-adapter-design-review-findings.md +131 -0
- package/docs/design/github-polling-adapter-implementation-plan.md +284 -0
- package/docs/design/implementation_plan.md +192 -0
- package/docs/design/workflow-id-validation-at-startup.md +146 -0
- package/docs/design/workflow-id-validation-design-review.md +87 -0
- package/docs/design/workflow-id-validation-implementation-plan.md +185 -0
- package/docs/design/worktrain-system-prompt-report-issue-candidates.md +135 -0
- package/docs/design/worktrain-system-prompt-report-issue-design-review.md +73 -0
- package/docs/ideas/backlog.md +361 -0
- package/package.json +1 -1
- package/workflows/architecture-scalability-audit.json +1 -1
- package/workflows/bug-investigation.agentic.v2.json +3 -3
- package/workflows/coding-task-workflow-agentic.json +32 -32
- package/workflows/coding-task-workflow-agentic.lean.v2.json +1 -1
- package/workflows/coding-task-workflow-agentic.v2.json +7 -7
- package/workflows/mr-review-workflow.agentic.v2.json +21 -12
- package/workflows/personal-learning-materials-creation-branched.json +2 -2
- package/workflows/production-readiness-audit.json +1 -1
- package/workflows/relocation-workflow-us.json +2 -2
- package/workflows/ui-ux-design-workflow.json +14 -14
- package/workflows/workflow-for-workflows.json +3 -3
- package/workflows/workflow-for-workflows.v2.json +2 -2
- package/workflows/wr.discovery.json +1 -1
- /package/dist/{console → console-ui}/assets/index-8dh0Psu-.css +0 -0
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PollingScheduler = void 0;
|
|
4
|
+
const gitlab_poller_js_1 = require("./adapters/gitlab-poller.js");
|
|
5
|
+
const github_poller_js_1 = require("./adapters/github-poller.js");
|
|
6
|
+
function isPollingTrigger(trigger) {
|
|
7
|
+
return trigger.pollingSource !== undefined;
|
|
8
|
+
}
|
|
9
|
+
class PollingScheduler {
|
|
10
|
+
constructor(triggers, router, store, fetchFn) {
|
|
11
|
+
this.triggers = triggers;
|
|
12
|
+
this.router = router;
|
|
13
|
+
this.store = store;
|
|
14
|
+
this.fetchFn = fetchFn;
|
|
15
|
+
this.intervals = new Map();
|
|
16
|
+
this.polling = new Map();
|
|
17
|
+
}
|
|
18
|
+
start() {
|
|
19
|
+
const pollingTriggers = this.triggers.filter(isPollingTrigger);
|
|
20
|
+
if (pollingTriggers.length === 0) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
console.log(`[PollingScheduler] Starting polling for ${pollingTriggers.length} trigger(s)`);
|
|
24
|
+
for (const trigger of pollingTriggers) {
|
|
25
|
+
if (this.intervals.has(trigger.id)) {
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
const intervalMs = trigger.pollingSource.pollIntervalSeconds * 1000;
|
|
29
|
+
this.polling.set(trigger.id, false);
|
|
30
|
+
const firstPollTimeout = setTimeout(() => {
|
|
31
|
+
void this.runPollCycle(trigger);
|
|
32
|
+
}, 5000);
|
|
33
|
+
const handle = setInterval(() => {
|
|
34
|
+
void this.runPollCycle(trigger);
|
|
35
|
+
}, intervalMs);
|
|
36
|
+
this.intervals.set(trigger.id, handle);
|
|
37
|
+
this.intervals.set(`${trigger.id}__first`, firstPollTimeout);
|
|
38
|
+
console.log(`[PollingScheduler] Started polling trigger '${trigger.id}' ` +
|
|
39
|
+
`(provider: ${trigger.provider}, interval: ${trigger.pollingSource.pollIntervalSeconds}s)`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
stop() {
|
|
43
|
+
for (const [id, handle] of this.intervals) {
|
|
44
|
+
clearInterval(handle);
|
|
45
|
+
this.intervals.delete(id);
|
|
46
|
+
}
|
|
47
|
+
console.log('[PollingScheduler] All polling loops stopped.');
|
|
48
|
+
}
|
|
49
|
+
async runPollCycle(trigger) {
|
|
50
|
+
const triggerId = trigger.id;
|
|
51
|
+
if (this.polling.get(triggerId)) {
|
|
52
|
+
console.warn(`[PollingScheduler] Skipping poll cycle for trigger '${triggerId}' -- ` +
|
|
53
|
+
`previous cycle is still running. Consider increasing pollIntervalSeconds.`);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
this.polling.set(triggerId, true);
|
|
57
|
+
try {
|
|
58
|
+
await this.doPoll(trigger);
|
|
59
|
+
}
|
|
60
|
+
catch (e) {
|
|
61
|
+
console.warn(`[PollingScheduler] Unexpected error in poll cycle for trigger '${triggerId}':`, e instanceof Error ? e.message : String(e));
|
|
62
|
+
}
|
|
63
|
+
finally {
|
|
64
|
+
this.polling.set(triggerId, false);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
async doPoll(trigger) {
|
|
68
|
+
const triggerId = trigger.id;
|
|
69
|
+
const pollStartAt = new Date().toISOString();
|
|
70
|
+
const lastPollAt = await this.store.getLastPollAt(triggerId);
|
|
71
|
+
switch (trigger.pollingSource.provider) {
|
|
72
|
+
case 'gitlab_poll':
|
|
73
|
+
await this.doPollGitLab(trigger, triggerId, pollStartAt, lastPollAt, trigger.pollingSource);
|
|
74
|
+
break;
|
|
75
|
+
case 'github_issues_poll':
|
|
76
|
+
await this.doPollGitHub(trigger, triggerId, pollStartAt, lastPollAt, trigger.pollingSource, 'issues');
|
|
77
|
+
break;
|
|
78
|
+
case 'github_prs_poll':
|
|
79
|
+
await this.doPollGitHub(trigger, triggerId, pollStartAt, lastPollAt, trigger.pollingSource, 'prs');
|
|
80
|
+
break;
|
|
81
|
+
default: {
|
|
82
|
+
const _exhaustive = trigger.pollingSource;
|
|
83
|
+
console.warn(`[PollingScheduler] Unknown provider '${String(_exhaustive.provider)}' ` +
|
|
84
|
+
`for trigger '${triggerId}'. Skipping cycle.`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
async doPollGitLab(trigger, triggerId, pollStartAt, lastPollAt, source) {
|
|
89
|
+
const pollResult = await (0, gitlab_poller_js_1.pollGitLabMRs)(source, lastPollAt, this.fetchFn);
|
|
90
|
+
if (pollResult.kind === 'err') {
|
|
91
|
+
console.warn(`[PollingScheduler] GitLab poll failed for trigger '${triggerId}': ` +
|
|
92
|
+
`${pollResult.error.kind}: ${pollResult.error.message}. ` +
|
|
93
|
+
`Skipping this cycle, will retry at next interval.`);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const mrs = pollResult.value;
|
|
97
|
+
await this.dispatchAndRecord(trigger, triggerId, pollStartAt, mrs.map(mr => String(mr.id)), (id) => {
|
|
98
|
+
const mr = mrs.find(m => String(m.id) === id);
|
|
99
|
+
return mr ? buildGitLabWorkflowTrigger(trigger, mr) : null;
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
async doPollGitHub(trigger, triggerId, pollStartAt, lastPollAt, source, kind) {
|
|
103
|
+
let pollResult;
|
|
104
|
+
if (kind === 'issues') {
|
|
105
|
+
pollResult = await (0, github_poller_js_1.pollGitHubIssues)(source, lastPollAt, this.fetchFn);
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
pollResult = await (0, github_poller_js_1.pollGitHubPRs)(source, lastPollAt, this.fetchFn);
|
|
109
|
+
}
|
|
110
|
+
if (pollResult.kind === 'err') {
|
|
111
|
+
console.warn(`[PollingScheduler] GitHub ${kind} poll failed for trigger '${triggerId}': ` +
|
|
112
|
+
`${pollResult.error.kind}: ${pollResult.error.message}. ` +
|
|
113
|
+
`Skipping this cycle, will retry at next interval.`);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
const items = pollResult.value;
|
|
117
|
+
await this.dispatchAndRecord(trigger, triggerId, pollStartAt, items.map(item => String(item.id)), (id) => {
|
|
118
|
+
const item = items.find(i => String(i.id) === id);
|
|
119
|
+
return item ? buildGitHubWorkflowTrigger(trigger, item) : null;
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
async dispatchAndRecord(trigger, triggerId, pollStartAt, candidateIds, buildTrigger) {
|
|
123
|
+
if (candidateIds.length === 0) {
|
|
124
|
+
await this.store.record(triggerId, [], pollStartAt);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
const filterResult = await this.store.filterNew(triggerId, candidateIds);
|
|
128
|
+
if (filterResult.kind === 'err') {
|
|
129
|
+
console.warn(`[PollingScheduler] Failed to read event store for trigger '${triggerId}': ` +
|
|
130
|
+
`${filterResult.error.message}. Skipping dispatch to avoid duplicates.`);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
const newIds = filterResult.value;
|
|
134
|
+
if (newIds.length === 0) {
|
|
135
|
+
await this.store.record(triggerId, [], pollStartAt);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
for (const newId of newIds) {
|
|
139
|
+
const workflowTrigger = buildTrigger(newId);
|
|
140
|
+
if (!workflowTrigger)
|
|
141
|
+
continue;
|
|
142
|
+
this.router.dispatch(workflowTrigger);
|
|
143
|
+
}
|
|
144
|
+
const recordResult = await this.store.record(triggerId, newIds, pollStartAt);
|
|
145
|
+
if (recordResult.kind === 'err') {
|
|
146
|
+
console.warn(`[PollingScheduler] Failed to record processed events for trigger '${triggerId}': ` +
|
|
147
|
+
`${recordResult.error.message}. Events may be re-dispatched on the next cycle.`);
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
console.log(`[PollingScheduler] Dispatched ${newIds.length} new event(s) for trigger '${triggerId}'`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
exports.PollingScheduler = PollingScheduler;
|
|
155
|
+
function buildGitLabWorkflowTrigger(trigger, mr) {
|
|
156
|
+
const context = {
|
|
157
|
+
mrId: mr.id,
|
|
158
|
+
mrIid: mr.iid,
|
|
159
|
+
mrTitle: mr.title,
|
|
160
|
+
mrUrl: mr.web_url,
|
|
161
|
+
mrUpdatedAt: mr.updated_at,
|
|
162
|
+
...(mr.author?.username ? { mrAuthorUsername: mr.author.username } : {}),
|
|
163
|
+
};
|
|
164
|
+
const goal = interpolateGoalFromPayload(trigger, {
|
|
165
|
+
id: mr.id,
|
|
166
|
+
iid: mr.iid,
|
|
167
|
+
title: mr.title,
|
|
168
|
+
web_url: mr.web_url,
|
|
169
|
+
updated_at: mr.updated_at,
|
|
170
|
+
state: mr.state,
|
|
171
|
+
author: mr.author ?? {},
|
|
172
|
+
});
|
|
173
|
+
return {
|
|
174
|
+
workflowId: trigger.workflowId,
|
|
175
|
+
goal,
|
|
176
|
+
workspacePath: trigger.workspacePath,
|
|
177
|
+
context,
|
|
178
|
+
...(trigger.referenceUrls !== undefined ? { referenceUrls: trigger.referenceUrls } : {}),
|
|
179
|
+
...(trigger.agentConfig !== undefined ? { agentConfig: trigger.agentConfig } : {}),
|
|
180
|
+
...(trigger.soulFile !== undefined ? { soulFile: trigger.soulFile } : {}),
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
function buildGitHubWorkflowTrigger(trigger, item) {
|
|
184
|
+
const context = {
|
|
185
|
+
itemId: item.id,
|
|
186
|
+
itemNumber: item.number,
|
|
187
|
+
itemTitle: item.title,
|
|
188
|
+
itemUrl: item.html_url,
|
|
189
|
+
itemUpdatedAt: item.updated_at,
|
|
190
|
+
...(item.user?.login ? { itemAuthorLogin: item.user.login } : {}),
|
|
191
|
+
};
|
|
192
|
+
const goal = interpolateGoalFromPayload(trigger, {
|
|
193
|
+
id: item.id,
|
|
194
|
+
number: item.number,
|
|
195
|
+
title: item.title,
|
|
196
|
+
html_url: item.html_url,
|
|
197
|
+
updated_at: item.updated_at,
|
|
198
|
+
state: item.state,
|
|
199
|
+
user: item.user ?? {},
|
|
200
|
+
});
|
|
201
|
+
return {
|
|
202
|
+
workflowId: trigger.workflowId,
|
|
203
|
+
goal,
|
|
204
|
+
workspacePath: trigger.workspacePath,
|
|
205
|
+
context,
|
|
206
|
+
...(trigger.referenceUrls !== undefined ? { referenceUrls: trigger.referenceUrls } : {}),
|
|
207
|
+
...(trigger.agentConfig !== undefined ? { agentConfig: trigger.agentConfig } : {}),
|
|
208
|
+
...(trigger.soulFile !== undefined ? { soulFile: trigger.soulFile } : {}),
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
function interpolateGoalFromPayload(trigger, payload) {
|
|
212
|
+
const template = trigger.goalTemplate;
|
|
213
|
+
if (!template)
|
|
214
|
+
return trigger.goal;
|
|
215
|
+
const TOKEN_RE = /\{\{([^}]+)\}\}/g;
|
|
216
|
+
const tokens = [];
|
|
217
|
+
let match;
|
|
218
|
+
while ((match = TOKEN_RE.exec(template)) !== null) {
|
|
219
|
+
if (match[1] !== undefined)
|
|
220
|
+
tokens.push(match[1]);
|
|
221
|
+
}
|
|
222
|
+
if (tokens.length === 0)
|
|
223
|
+
return template;
|
|
224
|
+
const resolved = new Map();
|
|
225
|
+
for (const token of tokens) {
|
|
226
|
+
const value = extractDotPath(payload, token);
|
|
227
|
+
if (value === undefined || value === null) {
|
|
228
|
+
return trigger.goal;
|
|
229
|
+
}
|
|
230
|
+
resolved.set(token, String(value));
|
|
231
|
+
}
|
|
232
|
+
return template.replace(/\{\{([^}]+)\}\}/g, (_, token) => resolved.get(token) ?? trigger.goal);
|
|
233
|
+
}
|
|
234
|
+
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('.');
|
|
241
|
+
let current = obj;
|
|
242
|
+
for (const segment of segments) {
|
|
243
|
+
if (segment.includes('[') || current === null || typeof current !== 'object') {
|
|
244
|
+
return undefined;
|
|
245
|
+
}
|
|
246
|
+
current = current[segment];
|
|
247
|
+
}
|
|
248
|
+
return current;
|
|
249
|
+
}
|
|
@@ -5,6 +5,7 @@ import type { TriggerStoreError } from './trigger-store.js';
|
|
|
5
5
|
import { TriggerRouter, type RunWorkflowFn } from './trigger-router.js';
|
|
6
6
|
import type { WorkspaceConfig } from './types.js';
|
|
7
7
|
import type { DaemonEventEmitter } from '../daemon/daemon-events.js';
|
|
8
|
+
import type { FetchFn } from './adapters/gitlab-poller.js';
|
|
8
9
|
export type TriggerListenerError = TriggerStoreError | {
|
|
9
10
|
readonly kind: 'port_conflict';
|
|
10
11
|
readonly port: number;
|
|
@@ -26,6 +27,8 @@ export interface StartTriggerListenerOptions {
|
|
|
26
27
|
readonly runWorkflowFn?: RunWorkflowFn;
|
|
27
28
|
readonly workspaces?: Readonly<Record<string, WorkspaceConfig>>;
|
|
28
29
|
readonly emitter?: DaemonEventEmitter;
|
|
30
|
+
readonly fetchFn?: FetchFn;
|
|
31
|
+
readonly getWorkflowByIdFn?: (id: string) => Promise<boolean>;
|
|
29
32
|
}
|
|
30
33
|
export declare function createTriggerApp(router: TriggerRouter): express.Application;
|
|
31
34
|
export declare function startTriggerListener(ctx: V2ToolContext, options: StartTriggerListenerOptions): Promise<TriggerListenerHandle | null | {
|
|
@@ -46,6 +46,8 @@ const trigger_router_js_1 = require("./trigger-router.js");
|
|
|
46
46
|
const config_file_js_1 = require("../config/config-file.js");
|
|
47
47
|
const workflow_runner_js_1 = require("../daemon/workflow-runner.js");
|
|
48
48
|
const types_js_1 = require("./types.js");
|
|
49
|
+
const polling_scheduler_js_1 = require("./polling-scheduler.js");
|
|
50
|
+
const polled_event_store_js_1 = require("./polled-event-store.js");
|
|
49
51
|
const DEFAULT_TRIGGER_PORT = 3200;
|
|
50
52
|
function createTriggerApp(router) {
|
|
51
53
|
const app = (0, express_1.default)();
|
|
@@ -136,6 +138,39 @@ async function startTriggerListener(ctx, options) {
|
|
|
136
138
|
triggerIndex = indexResult.value;
|
|
137
139
|
console.log(`[TriggerListener] Loaded ${configResult.value.triggers.length} trigger(s) from triggers.yml`);
|
|
138
140
|
}
|
|
141
|
+
const getWorkflowByIdFn = options.getWorkflowByIdFn
|
|
142
|
+
?? (ctx.workflowService
|
|
143
|
+
? async (id) => (await ctx.workflowService.getWorkflowById(id)) !== null
|
|
144
|
+
: undefined);
|
|
145
|
+
if (getWorkflowByIdFn) {
|
|
146
|
+
const unknownTriggerIds = [];
|
|
147
|
+
for (const [triggerId, trigger] of triggerIndex) {
|
|
148
|
+
let found;
|
|
149
|
+
try {
|
|
150
|
+
found = await getWorkflowByIdFn(trigger.workflowId);
|
|
151
|
+
}
|
|
152
|
+
catch (e) {
|
|
153
|
+
found = false;
|
|
154
|
+
console.warn(`[TriggerListener] Error validating workflowId '${trigger.workflowId}' for trigger '${triggerId}': ` +
|
|
155
|
+
(e instanceof Error ? e.message : String(e)));
|
|
156
|
+
}
|
|
157
|
+
if (!found) {
|
|
158
|
+
unknownTriggerIds.push(triggerId);
|
|
159
|
+
console.warn(`[TriggerListener] Skipping trigger '${triggerId}': workflowId '${trigger.workflowId}' was not found. ` +
|
|
160
|
+
`Fix the workflowId in triggers.yml and restart the daemon.`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
for (const id of unknownTriggerIds) {
|
|
164
|
+
triggerIndex.delete(id);
|
|
165
|
+
}
|
|
166
|
+
if (unknownTriggerIds.length > 0) {
|
|
167
|
+
console.warn(`[TriggerListener] Skipped ${unknownTriggerIds.length} trigger(s) with unknown workflowId(s). ` +
|
|
168
|
+
`${triggerIndex.size} trigger(s) will be active.`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
console.log(`[TriggerListener] workflowId validation skipped (no resolver provided).`);
|
|
173
|
+
}
|
|
139
174
|
const workrailConfig = (0, config_file_js_1.loadWorkrailConfigFile)();
|
|
140
175
|
const maxConcurrencyRaw = workrailConfig.kind === 'ok'
|
|
141
176
|
? workrailConfig.value['maxConcurrentSessions']
|
|
@@ -145,6 +180,10 @@ async function startTriggerListener(ctx, options) {
|
|
|
145
180
|
const runWorkflowFn = options.runWorkflowFn ?? workflow_runner_js_1.runWorkflow;
|
|
146
181
|
const router = new trigger_router_js_1.TriggerRouter(triggerIndex, ctx, apiKey, runWorkflowFn, undefined, maxConcurrentSessions, options.emitter);
|
|
147
182
|
const app = createTriggerApp(router);
|
|
183
|
+
const allTriggers = [...triggerIndex.values()];
|
|
184
|
+
const polledEventStore = new polled_event_store_js_1.PolledEventStore(env);
|
|
185
|
+
const pollingScheduler = new polling_scheduler_js_1.PollingScheduler(allTriggers, router, polledEventStore, options.fetchFn);
|
|
186
|
+
pollingScheduler.start();
|
|
148
187
|
await (0, workflow_runner_js_1.runStartupRecovery)().catch((err) => {
|
|
149
188
|
console.warn('[TriggerListener] Startup recovery encountered an unexpected error:', err instanceof Error ? err.message : String(err));
|
|
150
189
|
});
|
|
@@ -154,9 +193,11 @@ async function startTriggerListener(ctx, options) {
|
|
|
154
193
|
const server = http.createServer(app);
|
|
155
194
|
server.on('error', (error) => {
|
|
156
195
|
if (error.code === 'EADDRINUSE') {
|
|
196
|
+
pollingScheduler.stop();
|
|
157
197
|
resolve({ _kind: 'err', error: { kind: 'port_conflict', port } });
|
|
158
198
|
}
|
|
159
199
|
else {
|
|
200
|
+
pollingScheduler.stop();
|
|
160
201
|
resolve({ _kind: 'err', error: { kind: 'io_error', message: error.message } });
|
|
161
202
|
}
|
|
162
203
|
});
|
|
@@ -172,9 +213,12 @@ async function startTriggerListener(ctx, options) {
|
|
|
172
213
|
resolve({
|
|
173
214
|
port: actualPort,
|
|
174
215
|
router,
|
|
175
|
-
stop:
|
|
176
|
-
|
|
177
|
-
|
|
216
|
+
stop: async () => {
|
|
217
|
+
pollingScheduler.stop();
|
|
218
|
+
return new Promise((res, rej) => {
|
|
219
|
+
server.close((e) => (e ? rej(e) : res()));
|
|
220
|
+
});
|
|
221
|
+
},
|
|
178
222
|
});
|
|
179
223
|
});
|
|
180
224
|
});
|
|
@@ -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']);
|
|
44
|
+
const SUPPORTED_PROVIDERS = new Set(['generic', 'gitlab_poll', 'github_issues_poll', 'github_prs_poll']);
|
|
45
45
|
function unquoteYamlScalar(raw) {
|
|
46
46
|
const s = raw.trim();
|
|
47
47
|
if ((s.startsWith('"') && s.endsWith('"')) ||
|
|
@@ -304,6 +304,18 @@ function parseTriggersYaml(content) {
|
|
|
304
304
|
case 'projectId':
|
|
305
305
|
source.projectId = srcValueResult.value;
|
|
306
306
|
break;
|
|
307
|
+
case 'repo':
|
|
308
|
+
source.repo = srcValueResult.value;
|
|
309
|
+
break;
|
|
310
|
+
case 'excludeAuthors':
|
|
311
|
+
source.excludeAuthors = srcValueResult.value;
|
|
312
|
+
break;
|
|
313
|
+
case 'notLabels':
|
|
314
|
+
source.notLabels = srcValueResult.value;
|
|
315
|
+
break;
|
|
316
|
+
case 'labelFilter':
|
|
317
|
+
source.labelFilter = srcValueResult.value;
|
|
318
|
+
break;
|
|
307
319
|
case 'token':
|
|
308
320
|
source.token = srcValueResult.value;
|
|
309
321
|
break;
|
|
@@ -577,54 +589,123 @@ function validateAndResolveTrigger(raw, env, workspaces = {}) {
|
|
|
577
589
|
console.warn(`[TriggerStore] Warning: trigger "${rawId}" has autoOpenPR: true but autoCommit is not true. ` +
|
|
578
590
|
`A PR requires a commit -- delivery will be skipped unless autoCommit is also set to true.`);
|
|
579
591
|
}
|
|
592
|
+
function parsePollIntervalSeconds(raw2, triggerId2) {
|
|
593
|
+
const intervalRaw = raw2.pollIntervalSeconds?.trim();
|
|
594
|
+
if (!intervalRaw)
|
|
595
|
+
return (0, result_js_1.ok)(60);
|
|
596
|
+
const asNumber = Number(intervalRaw);
|
|
597
|
+
if (!Number.isInteger(asNumber) || asNumber <= 0) {
|
|
598
|
+
return (0, result_js_1.err)({
|
|
599
|
+
kind: 'invalid_field_value',
|
|
600
|
+
field: `source.pollIntervalSeconds (must be a positive integer, got: ${intervalRaw})`,
|
|
601
|
+
triggerId: triggerId2,
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
return (0, result_js_1.ok)(asNumber);
|
|
605
|
+
}
|
|
580
606
|
let pollingSource;
|
|
581
|
-
|
|
607
|
+
const isPollingProvider = provider === 'gitlab_poll' ||
|
|
608
|
+
provider === 'github_issues_poll' ||
|
|
609
|
+
provider === 'github_prs_poll';
|
|
610
|
+
if (isPollingProvider) {
|
|
582
611
|
if (!raw.source) {
|
|
583
612
|
return (0, result_js_1.err)({ kind: 'missing_field', field: 'source', triggerId: rawId });
|
|
584
613
|
}
|
|
585
614
|
const src = raw.source;
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
];
|
|
589
|
-
for (const field of requiredSourceFields) {
|
|
590
|
-
if (!src[field]?.trim()) {
|
|
591
|
-
return (0, result_js_1.err)({ kind: 'missing_field', field: `source.${field}`, triggerId: rawId });
|
|
592
|
-
}
|
|
615
|
+
if (!src.token?.trim()) {
|
|
616
|
+
return (0, result_js_1.err)({ kind: 'missing_field', field: 'source.token', triggerId: rawId });
|
|
593
617
|
}
|
|
594
|
-
const
|
|
595
|
-
const tokenResult = resolveSecret(tokenRaw, rawId, env);
|
|
618
|
+
const tokenResult = resolveSecret(src.token.trim(), rawId, env);
|
|
596
619
|
if (tokenResult.kind === 'err')
|
|
597
620
|
return tokenResult;
|
|
598
|
-
|
|
599
|
-
|
|
621
|
+
if (!src.events?.trim()) {
|
|
622
|
+
return (0, result_js_1.err)({ kind: 'missing_field', field: 'source.events', triggerId: rawId });
|
|
623
|
+
}
|
|
624
|
+
const events = src.events.trim().split(/\s+/).filter(Boolean);
|
|
600
625
|
if (events.length === 0) {
|
|
601
626
|
return (0, result_js_1.err)({ kind: 'missing_field', field: 'source.events (empty)', triggerId: rawId });
|
|
602
627
|
}
|
|
603
|
-
const
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
});
|
|
628
|
+
const intervalResult = parsePollIntervalSeconds(src, rawId);
|
|
629
|
+
if (intervalResult.kind === 'err')
|
|
630
|
+
return intervalResult;
|
|
631
|
+
const pollIntervalSeconds = intervalResult.value;
|
|
632
|
+
if (provider === 'gitlab_poll') {
|
|
633
|
+
if (!src.baseUrl?.trim()) {
|
|
634
|
+
return (0, result_js_1.err)({ kind: 'missing_field', field: 'source.baseUrl', triggerId: rawId });
|
|
635
|
+
}
|
|
636
|
+
if (!src.projectId?.trim()) {
|
|
637
|
+
return (0, result_js_1.err)({ kind: 'missing_field', field: 'source.projectId', triggerId: rawId });
|
|
638
|
+
}
|
|
639
|
+
const KNOWN_MR_EVENT_TYPES = new Set([
|
|
640
|
+
'merge_request.opened',
|
|
641
|
+
'merge_request.updated',
|
|
642
|
+
'merge_request.merged',
|
|
643
|
+
'merge_request.closed',
|
|
644
|
+
]);
|
|
645
|
+
for (const event of events) {
|
|
646
|
+
if (!KNOWN_MR_EVENT_TYPES.has(event)) {
|
|
647
|
+
console.warn(`[TriggerStore] Unknown polling event type '${event}' for trigger '${rawId}' -- ` +
|
|
648
|
+
`will match all open MRs as fallback`);
|
|
649
|
+
}
|
|
650
|
+
else if (event === 'merge_request.merged' || event === 'merge_request.closed') {
|
|
651
|
+
console.warn(`[TriggerStore] Event type '${event}' for trigger '${rawId}' cannot be observed ` +
|
|
652
|
+
`with state=opened polling (GitLab only returns open MRs). ` +
|
|
653
|
+
`Use a webhook trigger for merge/close events.`);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
pollingSource = {
|
|
657
|
+
provider: 'gitlab_poll',
|
|
658
|
+
baseUrl: src.baseUrl.trim(),
|
|
659
|
+
projectId: src.projectId.trim(),
|
|
660
|
+
token: tokenResult.value,
|
|
661
|
+
events,
|
|
662
|
+
pollIntervalSeconds,
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
else {
|
|
666
|
+
if (!src.repo?.trim()) {
|
|
667
|
+
return (0, result_js_1.err)({ kind: 'missing_field', field: 'source.repo', triggerId: rawId });
|
|
668
|
+
}
|
|
669
|
+
const KNOWN_GITHUB_ISSUE_EVENTS = new Set(['issues.opened', 'issues.updated']);
|
|
670
|
+
const KNOWN_GITHUB_PR_EVENTS = new Set(['pull_request.opened', 'pull_request.updated']);
|
|
671
|
+
const knownEvents = provider === 'github_issues_poll' ? KNOWN_GITHUB_ISSUE_EVENTS : KNOWN_GITHUB_PR_EVENTS;
|
|
672
|
+
for (const event of events) {
|
|
673
|
+
if (!knownEvents.has(event)) {
|
|
674
|
+
console.warn(`[TriggerStore] Unknown GitHub polling event type '${event}' for trigger '${rawId}' -- ` +
|
|
675
|
+
`will match all items as fallback`);
|
|
676
|
+
}
|
|
613
677
|
}
|
|
614
|
-
|
|
678
|
+
const excludeAuthors = src.excludeAuthors?.trim()
|
|
679
|
+
? src.excludeAuthors.trim().split(/\s+/).filter(Boolean)
|
|
680
|
+
: [];
|
|
681
|
+
const notLabels = src.notLabels?.trim()
|
|
682
|
+
? src.notLabels.trim().split(/\s+/).filter(Boolean)
|
|
683
|
+
: [];
|
|
684
|
+
const labelFilter = src.labelFilter?.trim()
|
|
685
|
+
? src.labelFilter.trim().split(/\s+/).filter(Boolean)
|
|
686
|
+
: [];
|
|
687
|
+
if (excludeAuthors.length === 0) {
|
|
688
|
+
console.warn(`[TriggerStore] WARNING: trigger '${rawId}' has provider='${provider}' but ` +
|
|
689
|
+
`excludeAuthors is not set. If WorkTrain creates issues/PRs under a bot account, ` +
|
|
690
|
+
`omitting excludeAuthors will cause infinite self-review loops. ` +
|
|
691
|
+
`Set excludeAuthors to your WorkTrain bot account login (e.g. "worktrain-bot").`);
|
|
692
|
+
}
|
|
693
|
+
pollingSource = {
|
|
694
|
+
provider: provider,
|
|
695
|
+
repo: src.repo.trim(),
|
|
696
|
+
token: tokenResult.value,
|
|
697
|
+
events,
|
|
698
|
+
pollIntervalSeconds,
|
|
699
|
+
excludeAuthors,
|
|
700
|
+
notLabels,
|
|
701
|
+
labelFilter,
|
|
702
|
+
};
|
|
615
703
|
}
|
|
616
|
-
pollingSource = {
|
|
617
|
-
baseUrl: src.baseUrl.trim(),
|
|
618
|
-
projectId: src.projectId.trim(),
|
|
619
|
-
token: tokenResult.value,
|
|
620
|
-
events,
|
|
621
|
-
pollIntervalSeconds,
|
|
622
|
-
};
|
|
623
704
|
}
|
|
624
705
|
else if (raw.source) {
|
|
625
706
|
console.warn(`[TriggerStore] WARNING: trigger '${rawId}' has provider='${provider}' but also ` +
|
|
626
|
-
`defines a source: block. The source: block is only used for
|
|
627
|
-
`It will be ignored for this trigger.`);
|
|
707
|
+
`defines a source: block. The source: block is only used for polling providers ` +
|
|
708
|
+
`(gitlab_poll, github_issues_poll, github_prs_poll). It will be ignored for this trigger.`);
|
|
628
709
|
}
|
|
629
710
|
const trigger = {
|
|
630
711
|
id: (0, types_js_1.asTriggerId)(rawId),
|
package/dist/trigger/types.d.ts
CHANGED
|
@@ -25,6 +25,22 @@ export interface GitLabPollingSource {
|
|
|
25
25
|
readonly events: readonly string[];
|
|
26
26
|
readonly pollIntervalSeconds: number;
|
|
27
27
|
}
|
|
28
|
+
export interface GitHubPollingSource {
|
|
29
|
+
readonly repo: string;
|
|
30
|
+
readonly token: string;
|
|
31
|
+
readonly events: readonly string[];
|
|
32
|
+
readonly pollIntervalSeconds: number;
|
|
33
|
+
readonly excludeAuthors: readonly string[];
|
|
34
|
+
readonly notLabels: readonly string[];
|
|
35
|
+
readonly labelFilter: readonly string[];
|
|
36
|
+
}
|
|
37
|
+
export type PollingSource = (GitLabPollingSource & {
|
|
38
|
+
readonly provider: 'gitlab_poll';
|
|
39
|
+
}) | (GitHubPollingSource & {
|
|
40
|
+
readonly provider: 'github_issues_poll';
|
|
41
|
+
}) | (GitHubPollingSource & {
|
|
42
|
+
readonly provider: 'github_prs_poll';
|
|
43
|
+
});
|
|
28
44
|
export interface TriggerDefinition {
|
|
29
45
|
readonly id: TriggerId;
|
|
30
46
|
readonly provider: string;
|
|
@@ -49,7 +65,7 @@ export interface TriggerDefinition {
|
|
|
49
65
|
readonly workflowId?: string;
|
|
50
66
|
readonly goal?: string;
|
|
51
67
|
};
|
|
52
|
-
readonly pollingSource?:
|
|
68
|
+
readonly pollingSource?: PollingSource;
|
|
53
69
|
readonly workspaceName?: WorkspaceName;
|
|
54
70
|
readonly soulFile?: string;
|
|
55
71
|
}
|