@exaudeus/workrail 3.24.4 → 3.26.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 (82) hide show
  1. package/dist/cli/commands/index.d.ts +6 -0
  2. package/dist/cli/commands/index.js +14 -1
  3. package/dist/cli/commands/version.d.ts +6 -0
  4. package/dist/cli/commands/version.js +14 -0
  5. package/dist/cli/commands/worktrain-await.d.ts +35 -0
  6. package/dist/cli/commands/worktrain-await.js +207 -0
  7. package/dist/cli/commands/worktrain-inbox.d.ts +23 -0
  8. package/dist/cli/commands/worktrain-inbox.js +82 -0
  9. package/dist/cli/commands/worktrain-init.d.ts +23 -0
  10. package/dist/cli/commands/worktrain-init.js +338 -0
  11. package/dist/cli/commands/worktrain-spawn.d.ts +28 -0
  12. package/dist/cli/commands/worktrain-spawn.js +106 -0
  13. package/dist/cli/commands/worktrain-tell.d.ts +25 -0
  14. package/dist/cli/commands/worktrain-tell.js +32 -0
  15. package/dist/cli-worktrain.d.ts +2 -0
  16. package/dist/cli-worktrain.js +169 -0
  17. package/dist/cli.js +100 -0
  18. package/dist/config/config-file.d.ts +2 -0
  19. package/dist/config/config-file.js +55 -0
  20. package/dist/console/assets/index-8dh0Psu-.css +1 -0
  21. package/dist/console/assets/{index-TMfptYpQ.js → index-HhtarvD5.js} +10 -10
  22. package/dist/console/index.html +2 -2
  23. package/dist/daemon/agent-loop.d.ts +90 -0
  24. package/dist/daemon/agent-loop.js +214 -0
  25. package/dist/daemon/pi-mono-loader.d.ts +0 -0
  26. package/dist/daemon/pi-mono-loader.js +1 -0
  27. package/dist/daemon/soul-template.d.ts +2 -0
  28. package/dist/daemon/soul-template.js +22 -0
  29. package/dist/daemon/workflow-runner.d.ts +63 -0
  30. package/dist/daemon/workflow-runner.js +689 -0
  31. package/dist/infrastructure/session/HttpServer.js +2 -2
  32. package/dist/manifest.json +226 -50
  33. package/dist/mcp/handlers/v2-execution/start.d.ts +2 -1
  34. package/dist/mcp/handlers/v2-execution/start.js +4 -3
  35. package/dist/mcp/output-schemas.d.ts +154 -154
  36. package/dist/mcp/server.js +1 -1
  37. package/dist/mcp/transports/bridge-entry.js +20 -2
  38. package/dist/mcp/transports/bridge-events.d.ts +34 -0
  39. package/dist/mcp/transports/bridge-events.js +24 -0
  40. package/dist/mcp/transports/fatal-exit.d.ts +5 -0
  41. package/dist/mcp/transports/fatal-exit.js +82 -0
  42. package/dist/mcp/transports/http-entry.js +3 -0
  43. package/dist/mcp/transports/stdio-entry.js +3 -7
  44. package/dist/mcp/v2/tools.d.ts +7 -7
  45. package/dist/trigger/delivery-action.d.ts +37 -0
  46. package/dist/trigger/delivery-action.js +204 -0
  47. package/dist/trigger/delivery-client.d.ts +11 -0
  48. package/dist/trigger/delivery-client.js +27 -0
  49. package/dist/trigger/index.d.ts +5 -0
  50. package/dist/trigger/index.js +8 -0
  51. package/dist/trigger/trigger-listener.d.ts +32 -0
  52. package/dist/trigger/trigger-listener.js +176 -0
  53. package/dist/trigger/trigger-router.d.ts +38 -0
  54. package/dist/trigger/trigger-router.js +343 -0
  55. package/dist/trigger/trigger-store.d.ts +39 -0
  56. package/dist/trigger/trigger-store.js +698 -0
  57. package/dist/trigger/types.d.ts +70 -0
  58. package/dist/trigger/types.js +10 -0
  59. package/dist/v2/durable-core/schemas/execution-snapshot/blocked-snapshot.d.ts +22 -22
  60. package/dist/v2/durable-core/schemas/execution-snapshot/execution-snapshot.v1.d.ts +114 -114
  61. package/dist/v2/durable-core/schemas/export-bundle/index.d.ts +454 -454
  62. package/dist/v2/durable-core/schemas/session/blockers.d.ts +14 -14
  63. package/dist/v2/durable-core/schemas/session/events.d.ts +93 -93
  64. package/dist/v2/durable-core/schemas/session/gaps.d.ts +2 -2
  65. package/dist/v2/durable-core/schemas/session/validation-event.d.ts +4 -4
  66. package/dist/v2/infra/in-memory/daemon-registry/index.d.ts +14 -0
  67. package/dist/v2/infra/in-memory/daemon-registry/index.js +32 -0
  68. package/dist/v2/infra/in-memory/keyed-async-queue/index.d.ts +5 -0
  69. package/dist/v2/infra/in-memory/keyed-async-queue/index.js +32 -0
  70. package/dist/v2/usecases/console-routes.d.ts +3 -1
  71. package/dist/v2/usecases/console-routes.js +132 -1
  72. package/dist/v2/usecases/console-service.d.ts +2 -0
  73. package/dist/v2/usecases/console-service.js +18 -2
  74. package/dist/v2/usecases/console-types.d.ts +2 -0
  75. package/package.json +6 -2
  76. package/spec/workflow-tags.json +1 -0
  77. package/workflows/classify-task-workflow.json +68 -0
  78. package/workflows/coding-task-workflow-agentic.lean.v2.json +43 -13
  79. package/workflows/workflow-for-workflows.json +4 -2
  80. package/workflows/workflow-for-workflows.v2.json +4 -2
  81. package/dist/console/assets/index-BXRk3te_.css +0 -1
  82. package/workflows/rich-object-contribution.json +0 -258
@@ -0,0 +1,8 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.loadTriggerConfigFromFile = exports.loadTriggerConfig = exports.startTriggerListener = void 0;
4
+ var trigger_listener_js_1 = require("./trigger-listener.js");
5
+ Object.defineProperty(exports, "startTriggerListener", { enumerable: true, get: function () { return trigger_listener_js_1.startTriggerListener; } });
6
+ var trigger_store_js_1 = require("./trigger-store.js");
7
+ Object.defineProperty(exports, "loadTriggerConfig", { enumerable: true, get: function () { return trigger_store_js_1.loadTriggerConfig; } });
8
+ Object.defineProperty(exports, "loadTriggerConfigFromFile", { enumerable: true, get: function () { return trigger_store_js_1.loadTriggerConfigFromFile; } });
@@ -0,0 +1,32 @@
1
+ import 'reflect-metadata';
2
+ import express from 'express';
3
+ import type { V2ToolContext } from '../mcp/types.js';
4
+ import type { TriggerStoreError } from './trigger-store.js';
5
+ import { TriggerRouter, type RunWorkflowFn } from './trigger-router.js';
6
+ import type { WorkspaceConfig } from './types.js';
7
+ export type TriggerListenerError = TriggerStoreError | {
8
+ readonly kind: 'port_conflict';
9
+ readonly port: number;
10
+ } | {
11
+ readonly kind: 'feature_disabled';
12
+ } | {
13
+ readonly kind: 'missing_api_key';
14
+ };
15
+ export interface TriggerListenerHandle {
16
+ readonly port: number;
17
+ readonly router: TriggerRouter;
18
+ stop(): Promise<void>;
19
+ }
20
+ export interface StartTriggerListenerOptions {
21
+ readonly workspacePath: string;
22
+ readonly apiKey?: string;
23
+ readonly port?: number;
24
+ readonly env?: Record<string, string | undefined>;
25
+ readonly runWorkflowFn?: RunWorkflowFn;
26
+ readonly workspaces?: Readonly<Record<string, WorkspaceConfig>>;
27
+ }
28
+ export declare function createTriggerApp(router: TriggerRouter): express.Application;
29
+ export declare function startTriggerListener(ctx: V2ToolContext, options: StartTriggerListenerOptions): Promise<TriggerListenerHandle | null | {
30
+ readonly _kind: 'err';
31
+ readonly error: TriggerListenerError;
32
+ }>;
@@ -0,0 +1,176 @@
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
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.createTriggerApp = createTriggerApp;
40
+ exports.startTriggerListener = startTriggerListener;
41
+ require("reflect-metadata");
42
+ const express_1 = __importDefault(require("express"));
43
+ const http = __importStar(require("node:http"));
44
+ const trigger_store_js_1 = require("./trigger-store.js");
45
+ const trigger_router_js_1 = require("./trigger-router.js");
46
+ const config_file_js_1 = require("../config/config-file.js");
47
+ const workflow_runner_js_1 = require("../daemon/workflow-runner.js");
48
+ const types_js_1 = require("./types.js");
49
+ const DEFAULT_TRIGGER_PORT = 3200;
50
+ function createTriggerApp(router) {
51
+ const app = (0, express_1.default)();
52
+ app.use('/webhook', express_1.default.raw({ type: 'application/json', limit: '1mb' }));
53
+ app.get('/health', (_req, res) => {
54
+ res.status(200).json({ status: 'ok' });
55
+ });
56
+ app.post('/webhook/:triggerId', (req, res) => {
57
+ const triggerId = (0, types_js_1.asTriggerId)(req.params['triggerId'] ?? '');
58
+ if (!triggerId) {
59
+ res.status(400).json({ error: 'Missing triggerId' });
60
+ return;
61
+ }
62
+ const rawBody = Buffer.isBuffer(req.body) ? req.body : Buffer.from('');
63
+ let payload;
64
+ try {
65
+ const bodyStr = rawBody.toString('utf8');
66
+ if (bodyStr) {
67
+ payload = JSON.parse(bodyStr);
68
+ if (typeof payload !== 'object' || payload === null || Array.isArray(payload)) {
69
+ res.status(400).json({ error: 'Payload must be a JSON object' });
70
+ return;
71
+ }
72
+ }
73
+ else {
74
+ payload = {};
75
+ }
76
+ }
77
+ catch {
78
+ res.status(400).json({ error: 'Invalid JSON body' });
79
+ return;
80
+ }
81
+ const signature = req.headers['x-workrail-signature'];
82
+ const event = {
83
+ triggerId,
84
+ rawBody,
85
+ payload,
86
+ ...(signature !== undefined ? { signature } : {}),
87
+ };
88
+ const result = router.route(event);
89
+ if (result._tag === 'error') {
90
+ switch (result.error.kind) {
91
+ case 'not_found':
92
+ res.status(404).json({ error: `Unknown trigger: ${result.error.triggerId}` });
93
+ return;
94
+ case 'hmac_invalid':
95
+ res.status(401).json({ error: 'Invalid signature' });
96
+ return;
97
+ case 'payload_error':
98
+ res.status(400).json({ error: result.error.message });
99
+ return;
100
+ }
101
+ }
102
+ res.status(202).json({ status: 'accepted', triggerId });
103
+ });
104
+ return app;
105
+ }
106
+ async function startTriggerListener(ctx, options) {
107
+ const env = options.env ?? process.env;
108
+ if (env['WORKRAIL_TRIGGERS_ENABLED'] !== 'true') {
109
+ return null;
110
+ }
111
+ const apiKey = options.apiKey ?? env['ANTHROPIC_API_KEY'];
112
+ if (!apiKey) {
113
+ return { _kind: 'err', error: { kind: 'missing_api_key' } };
114
+ }
115
+ const workspaceResult = (0, config_file_js_1.loadWorkspacesFromConfigFile)();
116
+ const loadedWorkspaces = workspaceResult.kind === 'ok' ? workspaceResult.value : {};
117
+ const workspaces = options.workspaces ?? loadedWorkspaces;
118
+ const configResult = await (0, trigger_store_js_1.loadTriggerConfigFromFile)(options.workspacePath, env, workspaces);
119
+ let triggerIndex;
120
+ if (configResult.kind === 'err') {
121
+ if (configResult.error.kind === 'file_not_found') {
122
+ console.warn(`[TriggerListener] triggers.yml not found at ${options.workspacePath}. ` +
123
+ `Listener will start with 0 triggers. ` +
124
+ `Create triggers.yml in that directory to configure triggers.`);
125
+ triggerIndex = new Map();
126
+ }
127
+ else {
128
+ return { _kind: 'err', error: configResult.error };
129
+ }
130
+ }
131
+ else {
132
+ const indexResult = (0, trigger_store_js_1.buildTriggerIndex)(configResult.value);
133
+ if (indexResult.kind === 'err') {
134
+ return { _kind: 'err', error: indexResult.error };
135
+ }
136
+ triggerIndex = indexResult.value;
137
+ console.log(`[TriggerListener] Loaded ${configResult.value.triggers.length} trigger(s) from triggers.yml`);
138
+ }
139
+ const workrailConfig = (0, config_file_js_1.loadWorkrailConfigFile)();
140
+ const maxConcurrencyRaw = workrailConfig.kind === 'ok'
141
+ ? workrailConfig.value['maxConcurrentSessions']
142
+ : undefined;
143
+ const parsed = parseInt(maxConcurrencyRaw ?? '', 10);
144
+ const maxConcurrentSessions = !isNaN(parsed) ? parsed : undefined;
145
+ const runWorkflowFn = options.runWorkflowFn ?? workflow_runner_js_1.runWorkflow;
146
+ const router = new trigger_router_js_1.TriggerRouter(triggerIndex, ctx, apiKey, runWorkflowFn, undefined, maxConcurrentSessions);
147
+ const app = createTriggerApp(router);
148
+ await (0, workflow_runner_js_1.runStartupRecovery)().catch((err) => {
149
+ console.warn('[TriggerListener] Startup recovery encountered an unexpected error:', err instanceof Error ? err.message : String(err));
150
+ });
151
+ const portEnv = env['WORKRAIL_TRIGGER_PORT'];
152
+ const port = options.port ?? (portEnv ? parseInt(portEnv, 10) : DEFAULT_TRIGGER_PORT);
153
+ return new Promise((resolve) => {
154
+ const server = http.createServer(app);
155
+ server.on('error', (error) => {
156
+ if (error.code === 'EADDRINUSE') {
157
+ resolve({ _kind: 'err', error: { kind: 'port_conflict', port } });
158
+ }
159
+ else {
160
+ resolve({ _kind: 'err', error: { kind: 'io_error', message: error.message } });
161
+ }
162
+ });
163
+ server.listen(port, '127.0.0.1', () => {
164
+ const addr = server.address();
165
+ const actualPort = (addr && typeof addr === 'object') ? addr.port : port;
166
+ console.log(`[TriggerListener] Webhook server listening on port ${actualPort}`);
167
+ resolve({
168
+ port: actualPort,
169
+ router,
170
+ stop: () => new Promise((res, rej) => {
171
+ server.close((e) => (e ? rej(e) : res()));
172
+ }),
173
+ });
174
+ });
175
+ });
176
+ }
@@ -0,0 +1,38 @@
1
+ import type { WorkflowTrigger, WorkflowRunResult } from '../daemon/workflow-runner.js';
2
+ import type { V2ToolContext } from '../mcp/types.js';
3
+ import type { TriggerDefinition, WebhookEvent } from './types.js';
4
+ import type { ExecFn } from './delivery-action.js';
5
+ export type RouteError = {
6
+ readonly kind: 'not_found';
7
+ readonly triggerId: string;
8
+ } | {
9
+ readonly kind: 'hmac_invalid';
10
+ } | {
11
+ readonly kind: 'payload_error';
12
+ readonly message: string;
13
+ };
14
+ export type RouteResult = {
15
+ readonly _tag: 'enqueued';
16
+ readonly triggerId: string;
17
+ } | {
18
+ readonly _tag: 'error';
19
+ readonly error: RouteError;
20
+ };
21
+ export type RunWorkflowFn = (trigger: WorkflowTrigger, ctx: V2ToolContext, apiKey: string) => Promise<WorkflowRunResult>;
22
+ export declare function interpolateGoalTemplate(template: string, staticGoal: string, payload: Readonly<Record<string, unknown>>, triggerId: string): string;
23
+ export declare class TriggerRouter {
24
+ private readonly index;
25
+ private readonly ctx;
26
+ private readonly apiKey;
27
+ private readonly runWorkflowFn;
28
+ private readonly queue;
29
+ private readonly execFn;
30
+ private readonly semaphore;
31
+ private readonly _maxConcurrentSessions;
32
+ constructor(index: ReadonlyMap<string, TriggerDefinition>, ctx: V2ToolContext, apiKey: string, runWorkflowFn: RunWorkflowFn, execFn?: ExecFn, maxConcurrentSessions?: number);
33
+ get activeSessions(): number;
34
+ get maxConcurrentSessions(): number;
35
+ route(event: WebhookEvent): RouteResult;
36
+ dispatch(workflowTrigger: WorkflowTrigger): string;
37
+ listTriggers(): readonly TriggerDefinition[];
38
+ }
@@ -0,0 +1,343 @@
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.TriggerRouter = void 0;
37
+ exports.interpolateGoalTemplate = interpolateGoalTemplate;
38
+ const crypto = __importStar(require("node:crypto"));
39
+ const node_child_process_1 = require("node:child_process");
40
+ const node_util_1 = require("node:util");
41
+ const index_js_1 = require("../v2/infra/in-memory/keyed-async-queue/index.js");
42
+ const delivery_client_js_1 = require("./delivery-client.js");
43
+ const delivery_action_js_1 = require("./delivery-action.js");
44
+ const execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
45
+ function interpolateGoalTemplate(template, staticGoal, payload, triggerId) {
46
+ const TOKEN_RE = /\{\{([^}]+)\}\}/g;
47
+ const tokens = [];
48
+ let match;
49
+ while ((match = TOKEN_RE.exec(template)) !== null) {
50
+ if (match[1] !== undefined)
51
+ tokens.push(match[1]);
52
+ }
53
+ if (tokens.length === 0)
54
+ return template;
55
+ const resolved = new Map();
56
+ for (const token of tokens) {
57
+ const value = extractDotPath(payload, token);
58
+ if (value === undefined || value === null) {
59
+ console.warn(`[TriggerRouter] goalTemplate variable '${token}' not found in payload ` +
60
+ `for trigger '${triggerId}' (template: '${template}'). Falling back to static goal.`);
61
+ return staticGoal;
62
+ }
63
+ resolved.set(token, String(value));
64
+ }
65
+ return template.replace(TOKEN_RE, (_, token) => resolved.get(token) ?? staticGoal);
66
+ }
67
+ function extractDotPath(obj, rawPath) {
68
+ let path = rawPath.trim();
69
+ if (path.startsWith('$.'))
70
+ path = path.slice(2);
71
+ else if (path.startsWith('$'))
72
+ path = path.slice(1);
73
+ const segments = path.split('.');
74
+ let current = obj;
75
+ for (const segment of segments) {
76
+ if (segment.includes('[')) {
77
+ console.warn(`[TriggerRouter] contextMapping path "${rawPath}" contains array indexing ` +
78
+ `(segment: "${segment}"). Array indexing is not supported in MVP. ` +
79
+ `The extracted value will be undefined.`);
80
+ return undefined;
81
+ }
82
+ if (current === null || typeof current !== 'object') {
83
+ return undefined;
84
+ }
85
+ current = current[segment];
86
+ }
87
+ return current;
88
+ }
89
+ function applyContextMapping(payload, entries) {
90
+ const context = {};
91
+ for (const entry of entries) {
92
+ const value = extractDotPath(payload, entry.payloadPath);
93
+ if (value === undefined) {
94
+ if (entry.required) {
95
+ console.warn(`[TriggerRouter] Required contextMapping key "${entry.workflowContextKey}" ` +
96
+ `(path: "${entry.payloadPath}") resolved to undefined. ` +
97
+ `The workflow context will be missing this variable.`);
98
+ }
99
+ continue;
100
+ }
101
+ context[entry.workflowContextKey] = value;
102
+ }
103
+ return context;
104
+ }
105
+ function validateHmac(rawBody, secret, headerValue) {
106
+ const expected = crypto
107
+ .createHmac('sha256', secret)
108
+ .update(rawBody)
109
+ .digest('hex');
110
+ const received = headerValue.startsWith('sha256=')
111
+ ? headerValue.slice(7)
112
+ : headerValue;
113
+ if (expected.length !== received.length) {
114
+ return false;
115
+ }
116
+ return crypto.timingSafeEqual(Buffer.from(expected, 'utf8'), Buffer.from(received, 'utf8'));
117
+ }
118
+ async function maybeRunDelivery(triggerId, trigger, result, execFn) {
119
+ if (result._tag !== 'success')
120
+ return;
121
+ if (result.lastStepNotes === undefined) {
122
+ if (trigger.autoCommit === true) {
123
+ console.warn(`[TriggerRouter] Delivery skipped: triggerId=${triggerId} -- ` +
124
+ `lastStepNotes is absent (agent did not provide notes on the final step). ` +
125
+ `Ensure the workflow produces a JSON handoff block in its final step notes.`);
126
+ }
127
+ return;
128
+ }
129
+ if (trigger.autoCommit !== true)
130
+ return;
131
+ const parseResult = (0, delivery_action_js_1.parseHandoffArtifact)(result.lastStepNotes);
132
+ if (parseResult.kind === 'err') {
133
+ console.warn(`[TriggerRouter] Delivery skipped: triggerId=${triggerId} -- ` +
134
+ `handoff artifact not parseable: ${parseResult.error}. ` +
135
+ `Ensure the workflow's final step produces a JSON block with commitType, filesChanged, etc.`);
136
+ return;
137
+ }
138
+ const deliveryResult = await (0, delivery_action_js_1.runDelivery)(parseResult.value, trigger.workspacePath, { autoCommit: trigger.autoCommit, autoOpenPR: trigger.autoOpenPR }, execFn);
139
+ switch (deliveryResult._tag) {
140
+ case 'committed':
141
+ console.log(`[TriggerRouter] Delivery committed: triggerId=${triggerId} sha=${deliveryResult.sha}`);
142
+ break;
143
+ case 'pr_opened':
144
+ console.log(`[TriggerRouter] Delivery PR opened: triggerId=${triggerId} url=${deliveryResult.url}`);
145
+ break;
146
+ case 'skipped':
147
+ console.log(`[TriggerRouter] Delivery skipped: triggerId=${triggerId} reason=${deliveryResult.reason}`);
148
+ break;
149
+ case 'error':
150
+ console.warn(`[TriggerRouter] Delivery error: triggerId=${triggerId} phase=${deliveryResult.phase} ` +
151
+ `details=${deliveryResult.details}`);
152
+ break;
153
+ }
154
+ }
155
+ class Semaphore {
156
+ constructor(max) {
157
+ this.max = max;
158
+ this.active = 0;
159
+ this.waiters = [];
160
+ }
161
+ acquire() {
162
+ if (this.active < this.max) {
163
+ this.active++;
164
+ return Promise.resolve();
165
+ }
166
+ return new Promise((resolve) => {
167
+ this.waiters.push(resolve);
168
+ });
169
+ }
170
+ release() {
171
+ const next = this.waiters.shift();
172
+ if (next !== undefined) {
173
+ next();
174
+ }
175
+ else {
176
+ this.active--;
177
+ }
178
+ }
179
+ get activeCount() {
180
+ return this.active;
181
+ }
182
+ }
183
+ const DEFAULT_MAX_CONCURRENT_SESSIONS = 3;
184
+ class TriggerRouter {
185
+ constructor(index, ctx, apiKey, runWorkflowFn, execFn, maxConcurrentSessions) {
186
+ this.index = index;
187
+ this.ctx = ctx;
188
+ this.apiKey = apiKey;
189
+ this.runWorkflowFn = runWorkflowFn;
190
+ this.queue = new index_js_1.KeyedAsyncQueue();
191
+ this.execFn = execFn ?? execFileAsync;
192
+ const requested = maxConcurrentSessions ?? DEFAULT_MAX_CONCURRENT_SESSIONS;
193
+ const cap = Number.isNaN(requested) ? DEFAULT_MAX_CONCURRENT_SESSIONS : requested;
194
+ if (cap < 1) {
195
+ console.warn(`[TriggerRouter] maxConcurrentSessions must be >= 1; received ${cap}, clamping to 1.`);
196
+ this._maxConcurrentSessions = 1;
197
+ }
198
+ else {
199
+ this._maxConcurrentSessions = cap;
200
+ }
201
+ this.semaphore = new Semaphore(this._maxConcurrentSessions);
202
+ console.log(`[TriggerRouter] maxConcurrentSessions=${this._maxConcurrentSessions}`);
203
+ }
204
+ get activeSessions() {
205
+ return this.semaphore.activeCount;
206
+ }
207
+ get maxConcurrentSessions() {
208
+ return this._maxConcurrentSessions;
209
+ }
210
+ route(event) {
211
+ const trigger = this.index.get(event.triggerId);
212
+ if (!trigger) {
213
+ return {
214
+ _tag: 'error',
215
+ error: { kind: 'not_found', triggerId: event.triggerId },
216
+ };
217
+ }
218
+ if (trigger.hmacSecret) {
219
+ const signature = event.signature;
220
+ if (!signature) {
221
+ return { _tag: 'error', error: { kind: 'hmac_invalid' } };
222
+ }
223
+ if (!validateHmac(event.rawBody, trigger.hmacSecret, signature)) {
224
+ return { _tag: 'error', error: { kind: 'hmac_invalid' } };
225
+ }
226
+ }
227
+ let workflowContext;
228
+ if (trigger.contextMapping?.mappings.length) {
229
+ workflowContext = applyContextMapping(event.payload, trigger.contextMapping.mappings);
230
+ }
231
+ else {
232
+ workflowContext = { payload: event.payload };
233
+ }
234
+ const goal = trigger.goalTemplate
235
+ ? interpolateGoalTemplate(trigger.goalTemplate, trigger.goal, event.payload, trigger.id)
236
+ : trigger.goal;
237
+ const workflowTrigger = {
238
+ workflowId: trigger.workflowId,
239
+ goal,
240
+ workspacePath: trigger.workspacePath,
241
+ context: workflowContext,
242
+ ...(trigger.referenceUrls !== undefined ? { referenceUrls: trigger.referenceUrls } : {}),
243
+ ...(trigger.agentConfig !== undefined ? { agentConfig: trigger.agentConfig } : {}),
244
+ ...(trigger.soulFile !== undefined ? { soulFile: trigger.soulFile } : {}),
245
+ };
246
+ const queueKey = trigger.concurrencyMode === 'parallel'
247
+ ? `${trigger.id}:${crypto.randomUUID()}`
248
+ : trigger.id;
249
+ void this.queue.enqueue(queueKey, async () => {
250
+ if (this.semaphore.activeCount >= this._maxConcurrentSessions) {
251
+ console.warn(`[TriggerRouter] Concurrency limit reached ` +
252
+ `(${this.semaphore.activeCount}/${this._maxConcurrentSessions} active): ` +
253
+ `queuing dispatch for triggerId=${trigger.id}`);
254
+ }
255
+ await this.semaphore.acquire();
256
+ let result;
257
+ try {
258
+ result = await this.runWorkflowFn(workflowTrigger, this.ctx, this.apiKey);
259
+ }
260
+ finally {
261
+ this.semaphore.release();
262
+ }
263
+ const originalTag = result._tag;
264
+ const originalResult = result;
265
+ if (trigger.callbackUrl) {
266
+ const deliveryResult = await (0, delivery_client_js_1.post)(trigger.callbackUrl, result);
267
+ if (deliveryResult.kind === 'err') {
268
+ const deliveryError = deliveryResult.error.kind === 'http_error'
269
+ ? `HTTP ${deliveryResult.error.status}: ${deliveryResult.error.body}`
270
+ : deliveryResult.error.message;
271
+ console.error(`[TriggerRouter] Delivery failed: triggerId=${trigger.id} ` +
272
+ `callbackUrl=${trigger.callbackUrl} error=${deliveryError}`);
273
+ const deliveryFailed = {
274
+ _tag: 'delivery_failed',
275
+ workflowId: trigger.workflowId,
276
+ stopReason: result.stopReason,
277
+ deliveryError,
278
+ };
279
+ result = deliveryFailed;
280
+ }
281
+ }
282
+ if (result._tag === 'success') {
283
+ console.log(`[TriggerRouter] Workflow completed: triggerId=${trigger.id} ` +
284
+ `workflowId=${trigger.workflowId} stopReason=${result.stopReason}`);
285
+ }
286
+ else if (result._tag === 'delivery_failed') {
287
+ const outcomeLabel = originalTag === 'success'
288
+ ? 'Workflow succeeded but delivery failed'
289
+ : 'Workflow failed and delivery also failed';
290
+ console.log(`[TriggerRouter] ${outcomeLabel}: triggerId=${trigger.id} ` +
291
+ `workflowId=${trigger.workflowId} stopReason=${result.stopReason}`);
292
+ }
293
+ else if (result._tag === 'timeout') {
294
+ console.log(`[TriggerRouter] Workflow timed out: triggerId=${trigger.id} ` +
295
+ `workflowId=${trigger.workflowId} reason=${result.reason} message=${result.message}`);
296
+ }
297
+ else {
298
+ console.log(`[TriggerRouter] Workflow failed: triggerId=${trigger.id} ` +
299
+ `workflowId=${trigger.workflowId} error=${result.message} stopReason=${result.stopReason}`);
300
+ }
301
+ await maybeRunDelivery(trigger.id, trigger, originalResult, this.execFn);
302
+ });
303
+ return { _tag: 'enqueued', triggerId: trigger.id };
304
+ }
305
+ dispatch(workflowTrigger) {
306
+ void this.queue.enqueue(workflowTrigger.workflowId, async () => {
307
+ if (this.semaphore.activeCount >= this._maxConcurrentSessions) {
308
+ console.warn(`[TriggerRouter] Concurrency limit reached ` +
309
+ `(${this.semaphore.activeCount}/${this._maxConcurrentSessions} active): ` +
310
+ `queuing dispatch for workflowId=${workflowTrigger.workflowId}`);
311
+ }
312
+ await this.semaphore.acquire();
313
+ let result;
314
+ try {
315
+ result = await this.runWorkflowFn(workflowTrigger, this.ctx, this.apiKey);
316
+ }
317
+ finally {
318
+ this.semaphore.release();
319
+ }
320
+ if (result._tag === 'success') {
321
+ console.log(`[TriggerRouter] Dispatch completed: workflowId=${workflowTrigger.workflowId} ` +
322
+ `stopReason=${result.stopReason}`);
323
+ }
324
+ else if (result._tag === 'delivery_failed') {
325
+ console.log(`[TriggerRouter] Dispatch delivery failed: workflowId=${workflowTrigger.workflowId} ` +
326
+ `stopReason=${result.stopReason} deliveryError=${result.deliveryError}`);
327
+ }
328
+ else if (result._tag === 'timeout') {
329
+ console.log(`[TriggerRouter] Dispatch timed out: workflowId=${workflowTrigger.workflowId} ` +
330
+ `reason=${result.reason} message=${result.message}`);
331
+ }
332
+ else {
333
+ console.log(`[TriggerRouter] Dispatch failed: workflowId=${workflowTrigger.workflowId} ` +
334
+ `error=${result.message} stopReason=${result.stopReason}`);
335
+ }
336
+ });
337
+ return workflowTrigger.workflowId;
338
+ }
339
+ listTriggers() {
340
+ return [...this.index.values()];
341
+ }
342
+ }
343
+ exports.TriggerRouter = TriggerRouter;
@@ -0,0 +1,39 @@
1
+ import type { Result } from '../runtime/result.js';
2
+ import { type TriggerConfig, type TriggerDefinition, type WorkspaceConfig } from './types.js';
3
+ export type TriggerStoreError = {
4
+ readonly kind: 'parse_error';
5
+ readonly message: string;
6
+ readonly lineNumber?: number;
7
+ } | {
8
+ readonly kind: 'missing_secret';
9
+ readonly envVarName: string;
10
+ readonly triggerId: string;
11
+ } | {
12
+ readonly kind: 'missing_field';
13
+ readonly field: string;
14
+ readonly triggerId: string;
15
+ } | {
16
+ readonly kind: 'invalid_field_value';
17
+ readonly field: string;
18
+ readonly triggerId: string;
19
+ } | {
20
+ readonly kind: 'unknown_provider';
21
+ readonly provider: string;
22
+ readonly triggerId: string;
23
+ } | {
24
+ readonly kind: 'unknown_workspace';
25
+ readonly workspaceName: string;
26
+ readonly triggerId: string;
27
+ } | {
28
+ readonly kind: 'file_not_found';
29
+ readonly filePath: string;
30
+ } | {
31
+ readonly kind: 'io_error';
32
+ readonly message: string;
33
+ } | {
34
+ readonly kind: 'duplicate_id';
35
+ readonly triggerId: string;
36
+ };
37
+ export declare function loadTriggerConfig(yamlContent: string, env?: Record<string, string | undefined>, workspaces?: Readonly<Record<string, WorkspaceConfig>>): Result<TriggerConfig, TriggerStoreError>;
38
+ export declare function loadTriggerConfigFromFile(workspacePath: string, env?: Record<string, string | undefined>, workspaces?: Readonly<Record<string, WorkspaceConfig>>): Promise<Result<TriggerConfig, TriggerStoreError>>;
39
+ export declare function buildTriggerIndex(config: TriggerConfig): Result<Map<string, TriggerDefinition>, TriggerStoreError>;