@exaudeus/workrail 3.25.0 → 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 (59) hide show
  1. package/dist/cli/commands/index.d.ts +5 -0
  2. package/dist/cli/commands/index.js +12 -1
  3. package/dist/cli/commands/worktrain-await.d.ts +35 -0
  4. package/dist/cli/commands/worktrain-await.js +207 -0
  5. package/dist/cli/commands/worktrain-inbox.d.ts +23 -0
  6. package/dist/cli/commands/worktrain-inbox.js +82 -0
  7. package/dist/cli/commands/worktrain-init.d.ts +23 -0
  8. package/dist/cli/commands/worktrain-init.js +338 -0
  9. package/dist/cli/commands/worktrain-spawn.d.ts +28 -0
  10. package/dist/cli/commands/worktrain-spawn.js +106 -0
  11. package/dist/cli/commands/worktrain-tell.d.ts +25 -0
  12. package/dist/cli/commands/worktrain-tell.js +32 -0
  13. package/dist/cli-worktrain.d.ts +2 -0
  14. package/dist/cli-worktrain.js +169 -0
  15. package/dist/cli.js +13 -3
  16. package/dist/config/config-file.d.ts +2 -0
  17. package/dist/config/config-file.js +55 -0
  18. package/dist/daemon/agent-loop.d.ts +90 -0
  19. package/dist/daemon/agent-loop.js +214 -0
  20. package/dist/daemon/pi-mono-loader.d.ts +0 -5
  21. package/dist/daemon/pi-mono-loader.js +0 -64
  22. package/dist/daemon/soul-template.d.ts +2 -0
  23. package/dist/daemon/soul-template.js +22 -0
  24. package/dist/daemon/workflow-runner.d.ts +24 -2
  25. package/dist/daemon/workflow-runner.js +235 -119
  26. package/dist/manifest.json +147 -51
  27. package/dist/mcp/output-schemas.d.ts +154 -154
  28. package/dist/mcp/transports/bridge-entry.js +20 -2
  29. package/dist/mcp/transports/bridge-events.d.ts +34 -0
  30. package/dist/mcp/transports/bridge-events.js +24 -0
  31. package/dist/mcp/transports/fatal-exit.d.ts +5 -0
  32. package/dist/mcp/transports/fatal-exit.js +82 -0
  33. package/dist/mcp/transports/http-entry.js +3 -0
  34. package/dist/mcp/transports/stdio-entry.js +3 -7
  35. package/dist/mcp/v2/tools.d.ts +7 -7
  36. package/dist/trigger/delivery-action.d.ts +37 -0
  37. package/dist/trigger/delivery-action.js +204 -0
  38. package/dist/trigger/delivery-client.d.ts +11 -0
  39. package/dist/trigger/delivery-client.js +27 -0
  40. package/dist/trigger/trigger-listener.d.ts +2 -0
  41. package/dist/trigger/trigger-listener.js +12 -2
  42. package/dist/trigger/trigger-router.d.ts +8 -2
  43. package/dist/trigger/trigger-router.js +164 -6
  44. package/dist/trigger/trigger-store.d.ts +11 -3
  45. package/dist/trigger/trigger-store.js +254 -13
  46. package/dist/trigger/types.d.ts +24 -0
  47. package/dist/trigger/types.js +4 -0
  48. package/dist/v2/durable-core/schemas/execution-snapshot/blocked-snapshot.d.ts +22 -22
  49. package/dist/v2/durable-core/schemas/execution-snapshot/execution-snapshot.v1.d.ts +114 -114
  50. package/dist/v2/durable-core/schemas/export-bundle/index.d.ts +454 -454
  51. package/dist/v2/durable-core/schemas/session/blockers.d.ts +14 -14
  52. package/dist/v2/durable-core/schemas/session/events.d.ts +93 -93
  53. package/dist/v2/durable-core/schemas/session/gaps.d.ts +2 -2
  54. package/dist/v2/durable-core/schemas/session/validation-event.d.ts +4 -4
  55. package/dist/v2/usecases/console-routes.js +33 -3
  56. package/package.json +6 -4
  57. package/spec/workflow-tags.json +1 -0
  58. package/workflows/classify-task-workflow.json +68 -0
  59. package/workflows/coding-task-workflow-agentic.lean.v2.json +43 -13
@@ -43,6 +43,7 @@ const express_1 = __importDefault(require("express"));
43
43
  const http = __importStar(require("node:http"));
44
44
  const trigger_store_js_1 = require("./trigger-store.js");
45
45
  const trigger_router_js_1 = require("./trigger-router.js");
46
+ const config_file_js_1 = require("../config/config-file.js");
46
47
  const workflow_runner_js_1 = require("../daemon/workflow-runner.js");
47
48
  const types_js_1 = require("./types.js");
48
49
  const DEFAULT_TRIGGER_PORT = 3200;
@@ -111,7 +112,10 @@ async function startTriggerListener(ctx, options) {
111
112
  if (!apiKey) {
112
113
  return { _kind: 'err', error: { kind: 'missing_api_key' } };
113
114
  }
114
- const configResult = await (0, trigger_store_js_1.loadTriggerConfigFromFile)(options.workspacePath, env);
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);
115
119
  let triggerIndex;
116
120
  if (configResult.kind === 'err') {
117
121
  if (configResult.error.kind === 'file_not_found') {
@@ -132,8 +136,14 @@ async function startTriggerListener(ctx, options) {
132
136
  triggerIndex = indexResult.value;
133
137
  console.log(`[TriggerListener] Loaded ${configResult.value.triggers.length} trigger(s) from triggers.yml`);
134
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;
135
145
  const runWorkflowFn = options.runWorkflowFn ?? workflow_runner_js_1.runWorkflow;
136
- const router = new trigger_router_js_1.TriggerRouter(triggerIndex, ctx, apiKey, runWorkflowFn);
146
+ const router = new trigger_router_js_1.TriggerRouter(triggerIndex, ctx, apiKey, runWorkflowFn, undefined, maxConcurrentSessions);
137
147
  const app = createTriggerApp(router);
138
148
  await (0, workflow_runner_js_1.runStartupRecovery)().catch((err) => {
139
149
  console.warn('[TriggerListener] Startup recovery encountered an unexpected error:', err instanceof Error ? err.message : String(err));
@@ -1,6 +1,7 @@
1
1
  import type { WorkflowTrigger, WorkflowRunResult } from '../daemon/workflow-runner.js';
2
2
  import type { V2ToolContext } from '../mcp/types.js';
3
3
  import type { TriggerDefinition, WebhookEvent } from './types.js';
4
+ import type { ExecFn } from './delivery-action.js';
4
5
  export type RouteError = {
5
6
  readonly kind: 'not_found';
6
7
  readonly triggerId: string;
@@ -18,14 +19,19 @@ export type RouteResult = {
18
19
  readonly error: RouteError;
19
20
  };
20
21
  export type RunWorkflowFn = (trigger: WorkflowTrigger, ctx: V2ToolContext, apiKey: string) => Promise<WorkflowRunResult>;
21
- export declare function interpolateGoalTemplate(template: string, staticGoal: string, payload: Readonly<Record<string, unknown>>): string;
22
+ export declare function interpolateGoalTemplate(template: string, staticGoal: string, payload: Readonly<Record<string, unknown>>, triggerId: string): string;
22
23
  export declare class TriggerRouter {
23
24
  private readonly index;
24
25
  private readonly ctx;
25
26
  private readonly apiKey;
26
27
  private readonly runWorkflowFn;
27
28
  private readonly queue;
28
- constructor(index: ReadonlyMap<string, TriggerDefinition>, ctx: V2ToolContext, apiKey: string, runWorkflowFn: RunWorkflowFn);
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;
29
35
  route(event: WebhookEvent): RouteResult;
30
36
  dispatch(workflowTrigger: WorkflowTrigger): string;
31
37
  listTriggers(): readonly TriggerDefinition[];
@@ -36,8 +36,13 @@ Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.TriggerRouter = void 0;
37
37
  exports.interpolateGoalTemplate = interpolateGoalTemplate;
38
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");
39
41
  const index_js_1 = require("../v2/infra/in-memory/keyed-async-queue/index.js");
40
- function interpolateGoalTemplate(template, staticGoal, payload) {
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) {
41
46
  const TOKEN_RE = /\{\{([^}]+)\}\}/g;
42
47
  const tokens = [];
43
48
  let match;
@@ -51,6 +56,8 @@ function interpolateGoalTemplate(template, staticGoal, payload) {
51
56
  for (const token of tokens) {
52
57
  const value = extractDotPath(payload, token);
53
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.`);
54
61
  return staticGoal;
55
62
  }
56
63
  resolved.set(token, String(value));
@@ -108,13 +115,97 @@ function validateHmac(rawBody, secret, headerValue) {
108
115
  }
109
116
  return crypto.timingSafeEqual(Buffer.from(expected, 'utf8'), Buffer.from(received, 'utf8'));
110
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;
111
184
  class TriggerRouter {
112
- constructor(index, ctx, apiKey, runWorkflowFn) {
185
+ constructor(index, ctx, apiKey, runWorkflowFn, execFn, maxConcurrentSessions) {
113
186
  this.index = index;
114
187
  this.ctx = ctx;
115
188
  this.apiKey = apiKey;
116
189
  this.runWorkflowFn = runWorkflowFn;
117
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;
118
209
  }
119
210
  route(event) {
120
211
  const trigger = this.index.get(event.triggerId);
@@ -141,7 +232,7 @@ class TriggerRouter {
141
232
  workflowContext = { payload: event.payload };
142
233
  }
143
234
  const goal = trigger.goalTemplate
144
- ? interpolateGoalTemplate(trigger.goalTemplate, trigger.goal, event.payload)
235
+ ? interpolateGoalTemplate(trigger.goalTemplate, trigger.goal, event.payload, trigger.id)
145
236
  : trigger.goal;
146
237
  const workflowTrigger = {
147
238
  workflowId: trigger.workflowId,
@@ -150,27 +241,94 @@ class TriggerRouter {
150
241
  context: workflowContext,
151
242
  ...(trigger.referenceUrls !== undefined ? { referenceUrls: trigger.referenceUrls } : {}),
152
243
  ...(trigger.agentConfig !== undefined ? { agentConfig: trigger.agentConfig } : {}),
244
+ ...(trigger.soulFile !== undefined ? { soulFile: trigger.soulFile } : {}),
153
245
  };
154
- void this.queue.enqueue(trigger.id, async () => {
155
- const result = await this.runWorkflowFn(workflowTrigger, this.ctx, this.apiKey);
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
+ }
156
282
  if (result._tag === 'success') {
157
283
  console.log(`[TriggerRouter] Workflow completed: triggerId=${trigger.id} ` +
158
284
  `workflowId=${trigger.workflowId} stopReason=${result.stopReason}`);
159
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
+ }
160
297
  else {
161
298
  console.log(`[TriggerRouter] Workflow failed: triggerId=${trigger.id} ` +
162
299
  `workflowId=${trigger.workflowId} error=${result.message} stopReason=${result.stopReason}`);
163
300
  }
301
+ await maybeRunDelivery(trigger.id, trigger, originalResult, this.execFn);
164
302
  });
165
303
  return { _tag: 'enqueued', triggerId: trigger.id };
166
304
  }
167
305
  dispatch(workflowTrigger) {
168
306
  void this.queue.enqueue(workflowTrigger.workflowId, async () => {
169
- const result = await this.runWorkflowFn(workflowTrigger, this.ctx, this.apiKey);
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
+ }
170
320
  if (result._tag === 'success') {
171
321
  console.log(`[TriggerRouter] Dispatch completed: workflowId=${workflowTrigger.workflowId} ` +
172
322
  `stopReason=${result.stopReason}`);
173
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
+ }
174
332
  else {
175
333
  console.log(`[TriggerRouter] Dispatch failed: workflowId=${workflowTrigger.workflowId} ` +
176
334
  `error=${result.message} stopReason=${result.stopReason}`);
@@ -1,5 +1,5 @@
1
1
  import type { Result } from '../runtime/result.js';
2
- import { type TriggerConfig, type TriggerDefinition } from './types.js';
2
+ import { type TriggerConfig, type TriggerDefinition, type WorkspaceConfig } from './types.js';
3
3
  export type TriggerStoreError = {
4
4
  readonly kind: 'parse_error';
5
5
  readonly message: string;
@@ -12,10 +12,18 @@ export type TriggerStoreError = {
12
12
  readonly kind: 'missing_field';
13
13
  readonly field: string;
14
14
  readonly triggerId: string;
15
+ } | {
16
+ readonly kind: 'invalid_field_value';
17
+ readonly field: string;
18
+ readonly triggerId: string;
15
19
  } | {
16
20
  readonly kind: 'unknown_provider';
17
21
  readonly provider: string;
18
22
  readonly triggerId: string;
23
+ } | {
24
+ readonly kind: 'unknown_workspace';
25
+ readonly workspaceName: string;
26
+ readonly triggerId: string;
19
27
  } | {
20
28
  readonly kind: 'file_not_found';
21
29
  readonly filePath: string;
@@ -26,6 +34,6 @@ export type TriggerStoreError = {
26
34
  readonly kind: 'duplicate_id';
27
35
  readonly triggerId: string;
28
36
  };
29
- export declare function loadTriggerConfig(yamlContent: string, env?: Record<string, string | undefined>): Result<TriggerConfig, TriggerStoreError>;
30
- export declare function loadTriggerConfigFromFile(workspacePath: string, env?: Record<string, string | undefined>): Promise<Result<TriggerConfig, TriggerStoreError>>;
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>>;
31
39
  export declare function buildTriggerIndex(config: TriggerConfig): Result<Map<string, TriggerDefinition>, TriggerStoreError>;