@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.
Files changed (77) hide show
  1. package/dist/cli/commands/index.d.ts +1 -0
  2. package/dist/cli/commands/index.js +3 -1
  3. package/dist/cli/commands/worktrain-await.js +11 -9
  4. package/dist/cli/commands/worktrain-daemon-install.d.ts +35 -0
  5. package/dist/cli/commands/worktrain-daemon-install.js +291 -0
  6. package/dist/cli/commands/worktrain-daemon.d.ts +31 -0
  7. package/dist/cli/commands/worktrain-daemon.js +272 -0
  8. package/dist/cli/commands/worktrain-spawn.js +11 -9
  9. package/dist/cli-worktrain.js +329 -0
  10. package/dist/cli.js +1 -22
  11. package/dist/console/standalone-console.d.ts +28 -0
  12. package/dist/console/standalone-console.js +142 -0
  13. package/dist/{console/assets/index-Cb_LO718.js → console-ui/assets/index-BuJFLLfY.js} +1 -1
  14. package/dist/{console → console-ui}/index.html +1 -1
  15. package/dist/daemon/agent-loop.d.ts +26 -0
  16. package/dist/daemon/agent-loop.js +39 -1
  17. package/dist/daemon/daemon-events.d.ts +47 -1
  18. package/dist/daemon/workflow-runner.d.ts +3 -2
  19. package/dist/daemon/workflow-runner.js +205 -41
  20. package/dist/infrastructure/session/HttpServer.js +133 -34
  21. package/dist/manifest.json +118 -62
  22. package/dist/mcp/output-schemas.d.ts +30 -30
  23. package/dist/mcp/transports/bridge-events.d.ts +4 -0
  24. package/dist/mcp/transports/fatal-exit.js +4 -0
  25. package/dist/mcp/transports/http-entry.js +2 -0
  26. package/dist/mcp/transports/stdio-entry.js +26 -6
  27. package/dist/mcp/v2/tools.d.ts +4 -4
  28. package/dist/trigger/adapters/github-poller.d.ts +44 -0
  29. package/dist/trigger/adapters/github-poller.js +190 -0
  30. package/dist/trigger/adapters/gitlab-poller.d.ts +27 -0
  31. package/dist/trigger/adapters/gitlab-poller.js +81 -0
  32. package/dist/trigger/index.d.ts +4 -1
  33. package/dist/trigger/index.js +5 -1
  34. package/dist/trigger/polled-event-store.d.ts +22 -0
  35. package/dist/trigger/polled-event-store.js +173 -0
  36. package/dist/trigger/polling-scheduler.d.ts +20 -0
  37. package/dist/trigger/polling-scheduler.js +249 -0
  38. package/dist/trigger/trigger-listener.d.ts +3 -0
  39. package/dist/trigger/trigger-listener.js +47 -3
  40. package/dist/trigger/trigger-store.js +114 -33
  41. package/dist/trigger/types.d.ts +17 -1
  42. package/dist/v2/durable-core/schemas/export-bundle/index.d.ts +224 -224
  43. package/dist/v2/durable-core/schemas/session/events.d.ts +42 -42
  44. package/dist/v2/durable-core/schemas/session/manifest.d.ts +6 -6
  45. package/dist/v2/durable-core/schemas/session/validation-event.d.ts +2 -2
  46. package/dist/v2/durable-core/tokens/payloads.d.ts +52 -52
  47. package/dist/v2/usecases/console-routes.js +3 -3
  48. package/dist/v2/usecases/console-service.js +133 -9
  49. package/dist/v2/usecases/console-types.d.ts +7 -0
  50. package/docs/design/daemon-conversation-logging-plan.md +98 -0
  51. package/docs/design/daemon-conversation-logging-review.md +55 -0
  52. package/docs/design/daemon-conversation-logging.md +129 -0
  53. package/docs/design/github-polling-adapter-design-candidates.md +226 -0
  54. package/docs/design/github-polling-adapter-design-review-findings.md +131 -0
  55. package/docs/design/github-polling-adapter-implementation-plan.md +284 -0
  56. package/docs/design/implementation_plan.md +192 -0
  57. package/docs/design/workflow-id-validation-at-startup.md +146 -0
  58. package/docs/design/workflow-id-validation-design-review.md +87 -0
  59. package/docs/design/workflow-id-validation-implementation-plan.md +185 -0
  60. package/docs/design/worktrain-system-prompt-report-issue-candidates.md +135 -0
  61. package/docs/design/worktrain-system-prompt-report-issue-design-review.md +73 -0
  62. package/docs/ideas/backlog.md +361 -0
  63. package/package.json +1 -1
  64. package/workflows/architecture-scalability-audit.json +1 -1
  65. package/workflows/bug-investigation.agentic.v2.json +3 -3
  66. package/workflows/coding-task-workflow-agentic.json +32 -32
  67. package/workflows/coding-task-workflow-agentic.lean.v2.json +1 -1
  68. package/workflows/coding-task-workflow-agentic.v2.json +7 -7
  69. package/workflows/mr-review-workflow.agentic.v2.json +21 -12
  70. package/workflows/personal-learning-materials-creation-branched.json +2 -2
  71. package/workflows/production-readiness-audit.json +1 -1
  72. package/workflows/relocation-workflow-us.json +2 -2
  73. package/workflows/ui-ux-design-workflow.json +14 -14
  74. package/workflows/workflow-for-workflows.json +3 -3
  75. package/workflows/workflow-for-workflows.v2.json +2 -2
  76. package/workflows/wr.discovery.json +1 -1
  77. /package/dist/{console → console-ui}/assets/index-8dh0Psu-.css +0 -0
@@ -164,18 +164,18 @@ export declare const V2ResumeSessionInput: z.ZodObject<{
164
164
  sameWorkspaceOnly: z.ZodOptional<z.ZodBoolean>;
165
165
  }, "strict", z.ZodTypeAny, {
166
166
  workspacePath: string;
167
- query?: string | undefined;
168
- runId?: string | undefined;
169
167
  sessionId?: string | undefined;
168
+ runId?: string | undefined;
170
169
  gitBranch?: string | undefined;
170
+ query?: string | undefined;
171
171
  gitHeadSha?: string | undefined;
172
172
  sameWorkspaceOnly?: boolean | undefined;
173
173
  }, {
174
174
  workspacePath: string;
175
- query?: string | undefined;
176
- runId?: string | undefined;
177
175
  sessionId?: string | undefined;
176
+ runId?: string | undefined;
178
177
  gitBranch?: string | undefined;
178
+ query?: string | undefined;
179
179
  gitHeadSha?: string | undefined;
180
180
  sameWorkspaceOnly?: boolean | undefined;
181
181
  }>;
@@ -0,0 +1,44 @@
1
+ import type { GitHubPollingSource } from '../types.js';
2
+ import type { Result } from '../../runtime/result.js';
3
+ export interface GitHubLabel {
4
+ readonly name: string;
5
+ }
6
+ export interface GitHubIssue {
7
+ readonly id: number;
8
+ readonly number: number;
9
+ readonly title: string;
10
+ readonly html_url: string;
11
+ readonly updated_at: string;
12
+ readonly state: string;
13
+ readonly user?: {
14
+ readonly login?: string;
15
+ };
16
+ readonly labels?: readonly GitHubLabel[];
17
+ }
18
+ export interface GitHubPR {
19
+ readonly id: number;
20
+ readonly number: number;
21
+ readonly title: string;
22
+ readonly html_url: string;
23
+ readonly updated_at: string;
24
+ readonly state: string;
25
+ readonly user?: {
26
+ readonly login?: string;
27
+ };
28
+ readonly draft?: boolean;
29
+ readonly labels?: readonly GitHubLabel[];
30
+ }
31
+ export type GitHubPollError = {
32
+ readonly kind: 'http_error';
33
+ readonly status: number;
34
+ readonly message: string;
35
+ } | {
36
+ readonly kind: 'network_error';
37
+ readonly message: string;
38
+ } | {
39
+ readonly kind: 'parse_error';
40
+ readonly message: string;
41
+ };
42
+ export type FetchFn = (url: string, init: RequestInit) => Promise<Response>;
43
+ export declare function pollGitHubIssues(source: GitHubPollingSource, since: string, fetchFn?: FetchFn): Promise<Result<GitHubIssue[], GitHubPollError>>;
44
+ export declare function pollGitHubPRs(source: GitHubPollingSource, since: string, fetchFn?: FetchFn): Promise<Result<GitHubPR[], GitHubPollError>>;
@@ -0,0 +1,190 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.pollGitHubIssues = pollGitHubIssues;
4
+ exports.pollGitHubPRs = pollGitHubPRs;
5
+ const result_js_1 = require("../../runtime/result.js");
6
+ function checkRateLimit(response) {
7
+ const remainingHeader = response.headers.get('X-RateLimit-Remaining');
8
+ const resetHeader = response.headers.get('X-RateLimit-Reset');
9
+ if (remainingHeader === null)
10
+ return true;
11
+ const remaining = parseInt(remainingHeader, 10);
12
+ if (isNaN(remaining) || remaining >= 100)
13
+ return true;
14
+ const resetTs = parseInt(resetHeader ?? '0', 10);
15
+ const resetAt = resetTs > 0 ? new Date(resetTs * 1000).toISOString() : 'unknown';
16
+ console.warn(`[GitHubPoller] Rate limit low: remaining=${remaining}, resets at ${resetAt}. ` +
17
+ `Skipping poll cycle to avoid exhaustion.`);
18
+ return false;
19
+ }
20
+ async function pollGitHubIssues(source, since, fetchFn = globalThis.fetch) {
21
+ const [owner, repo] = source.repo.split('/');
22
+ const url = new URL(`https://api.github.com/repos/${owner}/${repo}/issues`);
23
+ url.searchParams.set('state', 'open');
24
+ url.searchParams.set('since', since);
25
+ url.searchParams.set('sort', 'updated');
26
+ url.searchParams.set('direction', 'desc');
27
+ url.searchParams.set('per_page', '100');
28
+ if (source.labelFilter.length > 0) {
29
+ url.searchParams.set('labels', source.labelFilter.join(','));
30
+ }
31
+ let response;
32
+ try {
33
+ response = await fetchFn(url.toString(), {
34
+ headers: {
35
+ 'Authorization': `Bearer ${source.token}`,
36
+ 'Accept': 'application/vnd.github+json',
37
+ 'X-GitHub-Api-Version': '2022-11-28',
38
+ },
39
+ });
40
+ }
41
+ catch (e) {
42
+ return (0, result_js_1.err)({
43
+ kind: 'network_error',
44
+ message: e instanceof Error ? e.message : String(e),
45
+ });
46
+ }
47
+ if (!response.ok) {
48
+ return (0, result_js_1.err)({
49
+ kind: 'http_error',
50
+ status: response.status,
51
+ message: `GitHub API returned HTTP ${response.status}: ${response.statusText}`,
52
+ });
53
+ }
54
+ if (!checkRateLimit(response)) {
55
+ return (0, result_js_1.ok)([]);
56
+ }
57
+ let raw;
58
+ try {
59
+ raw = await response.json();
60
+ }
61
+ catch (e) {
62
+ return (0, result_js_1.err)({
63
+ kind: 'parse_error',
64
+ message: `Failed to parse GitHub Issues API response: ${e instanceof Error ? e.message : String(e)}`,
65
+ });
66
+ }
67
+ if (!Array.isArray(raw)) {
68
+ return (0, result_js_1.err)({
69
+ kind: 'parse_error',
70
+ message: `Expected array from GitHub Issues API, got: ${typeof raw}`,
71
+ });
72
+ }
73
+ const issues = [];
74
+ for (const item of raw) {
75
+ if (isGitHubIssueShape(item)) {
76
+ issues.push(item);
77
+ }
78
+ }
79
+ return (0, result_js_1.ok)(applyIssueFilters(issues, source));
80
+ }
81
+ async function pollGitHubPRs(source, since, fetchFn = globalThis.fetch) {
82
+ const [owner, repo] = source.repo.split('/');
83
+ const url = new URL(`https://api.github.com/repos/${owner}/${repo}/pulls`);
84
+ url.searchParams.set('state', 'open');
85
+ url.searchParams.set('sort', 'updated');
86
+ url.searchParams.set('direction', 'desc');
87
+ url.searchParams.set('per_page', '100');
88
+ let response;
89
+ try {
90
+ response = await fetchFn(url.toString(), {
91
+ headers: {
92
+ 'Authorization': `Bearer ${source.token}`,
93
+ 'Accept': 'application/vnd.github+json',
94
+ 'X-GitHub-Api-Version': '2022-11-28',
95
+ },
96
+ });
97
+ }
98
+ catch (e) {
99
+ return (0, result_js_1.err)({
100
+ kind: 'network_error',
101
+ message: e instanceof Error ? e.message : String(e),
102
+ });
103
+ }
104
+ if (!response.ok) {
105
+ return (0, result_js_1.err)({
106
+ kind: 'http_error',
107
+ status: response.status,
108
+ message: `GitHub API returned HTTP ${response.status}: ${response.statusText}`,
109
+ });
110
+ }
111
+ if (!checkRateLimit(response)) {
112
+ return (0, result_js_1.ok)([]);
113
+ }
114
+ let raw;
115
+ try {
116
+ raw = await response.json();
117
+ }
118
+ catch (e) {
119
+ return (0, result_js_1.err)({
120
+ kind: 'parse_error',
121
+ message: `Failed to parse GitHub PRs API response: ${e instanceof Error ? e.message : String(e)}`,
122
+ });
123
+ }
124
+ if (!Array.isArray(raw)) {
125
+ return (0, result_js_1.err)({
126
+ kind: 'parse_error',
127
+ message: `Expected array from GitHub PRs API, got: ${typeof raw}`,
128
+ });
129
+ }
130
+ const prs = [];
131
+ for (const item of raw) {
132
+ if (isGitHubPRShape(item)) {
133
+ prs.push(item);
134
+ }
135
+ }
136
+ const filtered = prs.filter(pr => pr.updated_at > since);
137
+ return (0, result_js_1.ok)(applyPRFilters(filtered, source));
138
+ }
139
+ function applyIssueFilters(issues, source) {
140
+ return issues.filter(issue => {
141
+ if (source.excludeAuthors.length > 0) {
142
+ const login = issue.user?.login;
143
+ if (!login || source.excludeAuthors.includes(login))
144
+ return false;
145
+ }
146
+ if (source.notLabels.length > 0 && issue.labels) {
147
+ const labelNames = issue.labels.map(l => l.name);
148
+ if (source.notLabels.some(nl => labelNames.includes(nl)))
149
+ return false;
150
+ }
151
+ return true;
152
+ });
153
+ }
154
+ function applyPRFilters(prs, source) {
155
+ return prs.filter(pr => {
156
+ if (source.excludeAuthors.length > 0) {
157
+ const login = pr.user?.login;
158
+ if (!login || source.excludeAuthors.includes(login))
159
+ return false;
160
+ }
161
+ if (source.notLabels.length > 0 && pr.labels) {
162
+ const labelNames = pr.labels.map(l => l.name);
163
+ if (source.notLabels.some(nl => labelNames.includes(nl)))
164
+ return false;
165
+ }
166
+ return true;
167
+ });
168
+ }
169
+ function isGitHubIssueShape(item) {
170
+ if (typeof item !== 'object' || item === null)
171
+ return false;
172
+ const obj = item;
173
+ return (typeof obj['id'] === 'number' &&
174
+ typeof obj['number'] === 'number' &&
175
+ typeof obj['title'] === 'string' &&
176
+ typeof obj['html_url'] === 'string' &&
177
+ typeof obj['updated_at'] === 'string' &&
178
+ typeof obj['state'] === 'string');
179
+ }
180
+ function isGitHubPRShape(item) {
181
+ if (typeof item !== 'object' || item === null)
182
+ return false;
183
+ const obj = item;
184
+ return (typeof obj['id'] === 'number' &&
185
+ typeof obj['number'] === 'number' &&
186
+ typeof obj['title'] === 'string' &&
187
+ typeof obj['html_url'] === 'string' &&
188
+ typeof obj['updated_at'] === 'string' &&
189
+ typeof obj['state'] === 'string');
190
+ }
@@ -0,0 +1,27 @@
1
+ import type { GitLabPollingSource } from '../types.js';
2
+ import type { Result } from '../../runtime/result.js';
3
+ export interface GitLabMR {
4
+ readonly id: number;
5
+ readonly iid: number;
6
+ readonly title: string;
7
+ readonly web_url: string;
8
+ readonly updated_at: string;
9
+ readonly state: string;
10
+ readonly author?: {
11
+ readonly username?: string;
12
+ readonly name?: string;
13
+ };
14
+ }
15
+ export type GitLabPollError = {
16
+ readonly kind: 'http_error';
17
+ readonly status: number;
18
+ readonly message: string;
19
+ } | {
20
+ readonly kind: 'network_error';
21
+ readonly message: string;
22
+ } | {
23
+ readonly kind: 'parse_error';
24
+ readonly message: string;
25
+ };
26
+ export type FetchFn = (url: string, init: RequestInit) => Promise<Response>;
27
+ export declare function pollGitLabMRs(source: GitLabPollingSource, updatedAfter: string, fetchFn?: FetchFn): Promise<Result<GitLabMR[], GitLabPollError>>;
@@ -0,0 +1,81 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.pollGitLabMRs = pollGitLabMRs;
4
+ const result_js_1 = require("../../runtime/result.js");
5
+ async function pollGitLabMRs(source, updatedAfter, fetchFn = globalThis.fetch) {
6
+ const encodedProjectId = encodeURIComponent(source.projectId);
7
+ const baseUrl = source.baseUrl.replace(/\/$/, '');
8
+ const url = new URL(`${baseUrl}/api/v4/projects/${encodedProjectId}/merge_requests`);
9
+ url.searchParams.set('state', 'opened');
10
+ url.searchParams.set('updated_after', updatedAfter);
11
+ url.searchParams.set('per_page', '100');
12
+ let response;
13
+ try {
14
+ response = await fetchFn(url.toString(), {
15
+ headers: {
16
+ 'PRIVATE-TOKEN': source.token,
17
+ 'Content-Type': 'application/json',
18
+ },
19
+ });
20
+ }
21
+ catch (e) {
22
+ return (0, result_js_1.err)({
23
+ kind: 'network_error',
24
+ message: e instanceof Error ? e.message : String(e),
25
+ });
26
+ }
27
+ if (!response.ok) {
28
+ return (0, result_js_1.err)({
29
+ kind: 'http_error',
30
+ status: response.status,
31
+ message: `GitLab API returned HTTP ${response.status}: ${response.statusText}`,
32
+ });
33
+ }
34
+ let raw;
35
+ try {
36
+ raw = await response.json();
37
+ }
38
+ catch (e) {
39
+ return (0, result_js_1.err)({
40
+ kind: 'parse_error',
41
+ message: `Failed to parse GitLab API response: ${e instanceof Error ? e.message : String(e)}`,
42
+ });
43
+ }
44
+ if (!Array.isArray(raw)) {
45
+ return (0, result_js_1.err)({
46
+ kind: 'parse_error',
47
+ message: `Expected array from GitLab MR API, got: ${typeof raw}`,
48
+ });
49
+ }
50
+ const mrs = [];
51
+ for (const item of raw) {
52
+ if (isGitLabMRShape(item)) {
53
+ mrs.push(item);
54
+ }
55
+ }
56
+ const filtered = applyEventFilter(mrs, source.events);
57
+ return (0, result_js_1.ok)(filtered);
58
+ }
59
+ function applyEventFilter(mrs, events) {
60
+ if (events.length === 0)
61
+ return mrs;
62
+ const includeOpened = events.includes('merge_request.opened');
63
+ const includeUpdated = events.includes('merge_request.updated');
64
+ if (!includeOpened && !includeUpdated) {
65
+ return mrs;
66
+ }
67
+ if (includeUpdated)
68
+ return mrs;
69
+ return mrs.filter(mr => mr.state === 'opened');
70
+ }
71
+ function isGitLabMRShape(item) {
72
+ if (typeof item !== 'object' || item === null)
73
+ return false;
74
+ const obj = item;
75
+ return (typeof obj['id'] === 'number' &&
76
+ typeof obj['iid'] === 'number' &&
77
+ typeof obj['title'] === 'string' &&
78
+ typeof obj['web_url'] === 'string' &&
79
+ typeof obj['updated_at'] === 'string' &&
80
+ typeof obj['state'] === 'string');
81
+ }
@@ -2,4 +2,7 @@ export { startTriggerListener } from './trigger-listener.js';
2
2
  export type { TriggerListenerHandle, TriggerListenerError, StartTriggerListenerOptions } from './trigger-listener.js';
3
3
  export { loadTriggerConfig, loadTriggerConfigFromFile } from './trigger-store.js';
4
4
  export type { TriggerStoreError } from './trigger-store.js';
5
- export type { TriggerId, TriggerDefinition, TriggerConfig, TriggerSource, WebhookEvent, ContextMapping, ContextMappingEntry, } from './types.js';
5
+ export type { TriggerId, TriggerDefinition, TriggerConfig, TriggerSource, WebhookEvent, ContextMapping, ContextMappingEntry, GitLabPollingSource, GitHubPollingSource, PollingSource, } from './types.js';
6
+ export { PolledEventStore } from './polled-event-store.js';
7
+ export type { PolledEventState, PolledEventStoreError } from './polled-event-store.js';
8
+ export { PollingScheduler } from './polling-scheduler.js';
@@ -1,8 +1,12 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.loadTriggerConfigFromFile = exports.loadTriggerConfig = exports.startTriggerListener = void 0;
3
+ exports.PollingScheduler = exports.PolledEventStore = exports.loadTriggerConfigFromFile = exports.loadTriggerConfig = exports.startTriggerListener = void 0;
4
4
  var trigger_listener_js_1 = require("./trigger-listener.js");
5
5
  Object.defineProperty(exports, "startTriggerListener", { enumerable: true, get: function () { return trigger_listener_js_1.startTriggerListener; } });
6
6
  var trigger_store_js_1 = require("./trigger-store.js");
7
7
  Object.defineProperty(exports, "loadTriggerConfig", { enumerable: true, get: function () { return trigger_store_js_1.loadTriggerConfig; } });
8
8
  Object.defineProperty(exports, "loadTriggerConfigFromFile", { enumerable: true, get: function () { return trigger_store_js_1.loadTriggerConfigFromFile; } });
9
+ var polled_event_store_js_1 = require("./polled-event-store.js");
10
+ Object.defineProperty(exports, "PolledEventStore", { enumerable: true, get: function () { return polled_event_store_js_1.PolledEventStore; } });
11
+ var polling_scheduler_js_1 = require("./polling-scheduler.js");
12
+ Object.defineProperty(exports, "PollingScheduler", { enumerable: true, get: function () { return polling_scheduler_js_1.PollingScheduler; } });
@@ -0,0 +1,22 @@
1
+ import type { Result } from '../runtime/result.js';
2
+ import type { TriggerId } from './types.js';
3
+ export interface PolledEventState {
4
+ readonly processedIds: readonly string[];
5
+ readonly lastPollAt: string;
6
+ }
7
+ export type PolledEventStoreError = {
8
+ readonly kind: 'io_error';
9
+ readonly message: string;
10
+ } | {
11
+ readonly kind: 'write_error';
12
+ readonly message: string;
13
+ };
14
+ export declare class PolledEventStore {
15
+ private readonly env;
16
+ constructor(env?: Record<string, string | undefined>);
17
+ load(triggerId: TriggerId): Promise<Result<PolledEventState, PolledEventStoreError>>;
18
+ save(triggerId: TriggerId, state: PolledEventState): Promise<Result<void, PolledEventStoreError>>;
19
+ filterNew(triggerId: TriggerId, candidateIds: readonly string[]): Promise<Result<string[], PolledEventStoreError>>;
20
+ record(triggerId: TriggerId, newIds: readonly string[], lastPollAt: string): Promise<Result<void, PolledEventStoreError>>;
21
+ getLastPollAt(triggerId: TriggerId): Promise<string>;
22
+ }
@@ -0,0 +1,173 @@
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.PolledEventStore = void 0;
37
+ const fs = __importStar(require("node:fs/promises"));
38
+ const path = __importStar(require("node:path"));
39
+ const os = __importStar(require("node:os"));
40
+ const node_crypto_1 = require("node:crypto");
41
+ const result_js_1 = require("../runtime/result.js");
42
+ const MAX_PROCESSED_IDS = 500;
43
+ function polledEventsDir(env = process.env) {
44
+ const workrailHome = env['WORKRAIL_HOME'] ?? path.join(os.homedir(), '.workrail');
45
+ return path.join(workrailHome, 'polled-events');
46
+ }
47
+ function stateFilePath(triggerId, env = process.env) {
48
+ const safeId = String(triggerId).replace(/[^a-zA-Z0-9_-]/g, '_');
49
+ return path.join(polledEventsDir(env), `${safeId}.json`);
50
+ }
51
+ class PolledEventStore {
52
+ constructor(env = process.env) {
53
+ this.env = env;
54
+ }
55
+ async load(triggerId) {
56
+ const filePath = stateFilePath(triggerId, this.env);
57
+ let raw;
58
+ try {
59
+ raw = await fs.readFile(filePath, 'utf8');
60
+ }
61
+ catch (e) {
62
+ const error = e;
63
+ if (error.code === 'ENOENT') {
64
+ return (0, result_js_1.ok)(freshState());
65
+ }
66
+ return (0, result_js_1.err)({ kind: 'io_error', message: error.message ?? String(e) });
67
+ }
68
+ try {
69
+ const parsed = JSON.parse(raw);
70
+ if (isValidState(parsed)) {
71
+ return (0, result_js_1.ok)({
72
+ processedIds: parsed.processedIds,
73
+ lastPollAt: parsed.lastPollAt,
74
+ });
75
+ }
76
+ console.warn(`[PolledEventStore] State file for trigger '${triggerId}' has unexpected schema. ` +
77
+ `Starting fresh with lastPollAt=now to prevent burst firing.`);
78
+ return (0, result_js_1.ok)(freshState());
79
+ }
80
+ catch {
81
+ console.warn(`[PolledEventStore] Could not parse state file for trigger '${triggerId}'. ` +
82
+ `Starting fresh with lastPollAt=now to prevent burst firing.`);
83
+ return (0, result_js_1.ok)(freshState());
84
+ }
85
+ }
86
+ async save(triggerId, state) {
87
+ const filePath = stateFilePath(triggerId, this.env);
88
+ const dir = path.dirname(filePath);
89
+ try {
90
+ await fs.mkdir(dir, { recursive: true });
91
+ }
92
+ catch (e) {
93
+ return (0, result_js_1.err)({ kind: 'write_error', message: `Failed to create directory ${dir}: ${String(e)}` });
94
+ }
95
+ const pruned = state.processedIds.length > MAX_PROCESSED_IDS
96
+ ? state.processedIds.slice(state.processedIds.length - MAX_PROCESSED_IDS)
97
+ : state.processedIds;
98
+ const serialized = JSON.stringify({ processedIds: pruned, lastPollAt: state.lastPollAt }, null, 2);
99
+ const tmpPath = `${filePath}.${(0, node_crypto_1.randomUUID)()}.tmp`;
100
+ try {
101
+ await fs.writeFile(tmpPath, serialized, 'utf8');
102
+ const fh = await fs.open(tmpPath, 'r+');
103
+ try {
104
+ await fh.sync();
105
+ }
106
+ finally {
107
+ await fh.close();
108
+ }
109
+ await fs.rename(tmpPath, filePath);
110
+ const dirFh = await fs.open(dir, 'r');
111
+ try {
112
+ await dirFh.sync();
113
+ }
114
+ finally {
115
+ await dirFh.close();
116
+ }
117
+ return (0, result_js_1.ok)(undefined);
118
+ }
119
+ catch (e) {
120
+ await fs.unlink(tmpPath).catch(() => undefined);
121
+ return (0, result_js_1.err)({ kind: 'write_error', message: `Failed to save state for trigger '${triggerId}': ${String(e)}` });
122
+ }
123
+ }
124
+ async filterNew(triggerId, candidateIds) {
125
+ if (candidateIds.length === 0)
126
+ return (0, result_js_1.ok)([]);
127
+ const stateResult = await this.load(triggerId);
128
+ if (stateResult.kind === 'err')
129
+ return stateResult;
130
+ const processed = new Set(stateResult.value.processedIds);
131
+ const newIds = candidateIds.filter(id => !processed.has(id));
132
+ return (0, result_js_1.ok)(newIds);
133
+ }
134
+ async record(triggerId, newIds, lastPollAt) {
135
+ if (newIds.length === 0) {
136
+ const stateResult = await this.load(triggerId);
137
+ if (stateResult.kind === 'err')
138
+ return stateResult;
139
+ return this.save(triggerId, { ...stateResult.value, lastPollAt });
140
+ }
141
+ const stateResult = await this.load(triggerId);
142
+ if (stateResult.kind === 'err')
143
+ return stateResult;
144
+ const existing = stateResult.value.processedIds;
145
+ const combined = [...existing, ...newIds];
146
+ return this.save(triggerId, { processedIds: combined, lastPollAt });
147
+ }
148
+ async getLastPollAt(triggerId) {
149
+ const stateResult = await this.load(triggerId);
150
+ if (stateResult.kind === 'err')
151
+ return new Date().toISOString();
152
+ return stateResult.value.lastPollAt;
153
+ }
154
+ }
155
+ exports.PolledEventStore = PolledEventStore;
156
+ function freshState() {
157
+ return {
158
+ processedIds: [],
159
+ lastPollAt: new Date().toISOString(),
160
+ };
161
+ }
162
+ function isValidState(value) {
163
+ if (typeof value !== 'object' || value === null)
164
+ return false;
165
+ const obj = value;
166
+ if (!Array.isArray(obj['processedIds']))
167
+ return false;
168
+ if (typeof obj['lastPollAt'] !== 'string')
169
+ return false;
170
+ if (!obj['processedIds'].every(id => typeof id === 'string'))
171
+ return false;
172
+ return true;
173
+ }
@@ -0,0 +1,20 @@
1
+ import type { TriggerDefinition } from './types.js';
2
+ import type { TriggerRouter } from './trigger-router.js';
3
+ import type { PolledEventStore } from './polled-event-store.js';
4
+ import { type FetchFn } from './adapters/gitlab-poller.js';
5
+ export declare class PollingScheduler {
6
+ private readonly triggers;
7
+ private readonly router;
8
+ private readonly store;
9
+ private readonly fetchFn?;
10
+ private readonly intervals;
11
+ private readonly polling;
12
+ constructor(triggers: readonly TriggerDefinition[], router: TriggerRouter, store: PolledEventStore, fetchFn?: FetchFn | undefined);
13
+ start(): void;
14
+ stop(): void;
15
+ private runPollCycle;
16
+ private doPoll;
17
+ private doPollGitLab;
18
+ private doPollGitHub;
19
+ private dispatchAndRecord;
20
+ }