agents-relay 1.0.26 → 1.0.28

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.js CHANGED
@@ -6,6 +6,7 @@ import { fileURLToPath } from 'node:url';
6
6
  import { randomUUID } from 'node:crypto';
7
7
  import { dashboardManagedJobs, InMemoryStore, GitHubStore, resolvePullRequest, listManagedGitHubJobs } from './store.js';
8
8
  import { githubAuthContext } from './github-auth.js';
9
+ import { taskOverallTimeoutMs } from './types.js';
9
10
  import { Reconciler } from './reconciler.js';
10
11
  import { CodexAdapter, ChatGptAdapter } from './adapters.js';
11
12
  import { CodexThreadContinuation, CommandContinuation, WebhookContinuation } from './continuation.js';
@@ -61,7 +62,7 @@ Example:
61
62
  'job create': `Create or reuse an open PR and persist one durable job marker.
62
63
 
63
64
  Required: --repo OWNER/REPO, --id JOB_ID, and --head BRANCH.
64
- Common: --base BRANCH (default: main), --title TEXT, --body TEXT, --mode fixed|autonomous.
65
+ Common: --base BRANCH (default: main), --title TEXT, --body TEXT, --mode fixed|autonomous, --no-auto-merge.
65
66
  With --start, also require --adapter codex|chatgpt, --model MODEL, --output task-pr|file,
66
67
  and --input TEXT. The initial task ID defaults to 'initial'; use --task-id to override it.
67
68
 
@@ -70,7 +71,7 @@ Example:
70
71
  'job adopt': `Adopt an existing PR as a durable job.
71
72
 
72
73
  Required: --repo OWNER/REPO, --pr NUMBER, and --id JOB_ID.
73
- Common: --title TEXT, --objective TEXT, --mode fixed|autonomous.
74
+ Common: --title TEXT, --objective TEXT, --mode fixed|autonomous, --no-auto-merge.
74
75
 
75
76
  Example:
76
77
  npx agents-relay job adopt --repo OWNER/REPO --pr 12 --id job-12 --title "Objective"`,
@@ -90,7 +91,7 @@ Actions:
90
91
  'task update': `Update mutable durable task fields in place.
91
92
 
92
93
  Required: --task-id ID and the normal job storage options.
93
- Mutable while not RUNNING/SUCCEEDED/CANCELLED: --adapter, --priority, --input, --project, --agent, --parent, --deps, --capabilities, --timeout, --max-attempts, --provider, --model, --profile, --reasoning, --cwd, --chatgpt-project, --chatgpt-tab-close-policy.
94
+ Mutable while not RUNNING/SUCCEEDED/CANCELLED: --adapter, --priority, --story-size, --input, --project, --agent, --parent, --deps, --capabilities, --timeout, --max-attempts, --provider, --model, --profile, --reasoning, --cwd, --chatgpt-project, --chatgpt-tab-close-policy.
94
95
  Use --clear-routing to remove routing metadata. A model update requires --model; provider defaults to the existing provider or openai.
95
96
 
96
97
  Example:
@@ -224,6 +225,8 @@ catch (error) {
224
225
  } }
225
226
  function priority(args, fallback = 'P2') { const value = arg(args, '--priority', fallback); if (!['P0', 'P1', 'P2', 'P3'].includes(value))
226
227
  throw new Error('--priority must be P0, P1, P2, or P3'); return value; }
228
+ function storySize(args, fallback = 'M') { const value = arg(args, '--story-size', fallback).toUpperCase(); if (!['S', 'M', 'L'].includes(value))
229
+ throw new Error('--story-size must be S, M, or L'); return value; }
227
230
  export function executionMode(args, fallback = 'fixed') { const value = arg(args, '--mode', arg(args, '--execution-mode', fallback)); if (value !== 'fixed' && value !== 'autonomous')
228
231
  throw new Error('--mode must be fixed or autonomous'); return value; }
229
232
  export function chatGptTabClosePolicy(args) {
@@ -247,7 +250,7 @@ export async function ensureManagedGitHubJob(client, repositoryName, trustedAuth
247
250
  if (!(error instanceof Error) || !error.message.includes('has no durable'))
248
251
  throw error;
249
252
  const now = new Date().toISOString();
250
- job = { id: options.id, title: options.title || pr.title || 'Agents Relay job', objective: options.objective || pr.body || undefined, description: pr.body || undefined, priority: options.priority ?? 'P2', executionMode: options.executionMode ?? 'fixed', prNumber: pr.number, repository: repositoryName, state: 'OPEN', continuation: options.continuation ?? null, createdAt: now, updatedAt: now, tasks: [] };
253
+ job = { id: options.id, title: options.title || pr.title || 'Agents Relay job', objective: options.objective || pr.body || undefined, description: pr.body || undefined, priority: options.priority ?? 'P2', executionMode: options.executionMode ?? 'fixed', autoMerge: options.autoMerge ?? true, prNumber: pr.number, repository: repositoryName, state: 'OPEN', continuation: options.continuation ?? null, createdAt: now, updatedAt: now, tasks: [] };
251
254
  }
252
255
  if (!job.description && pr.body)
253
256
  job.description = pr.body;
@@ -255,6 +258,8 @@ export async function ensureManagedGitHubJob(client, repositoryName, trustedAuth
255
258
  job.objective = options.objective;
256
259
  if (options.executionMode)
257
260
  job.executionMode = options.executionMode;
261
+ if (options.autoMerge !== undefined)
262
+ job.autoMerge = options.autoMerge;
258
263
  if (options.priority)
259
264
  job.priority = options.priority;
260
265
  else if (!job.priority)
@@ -283,10 +288,13 @@ function workerTaskFromArgs(job, args, now, requireInput = false) {
283
288
  throw new Error('--output file requires --output-path');
284
289
  const routing = { provider: arg(args, '--provider', 'openai'), model, profile: arg(args, '--profile') || undefined, reasoning: arg(args, '--reasoning') || undefined, cwd: arg(args, '--cwd') || undefined, projectId: arg(args, '--chatgpt-project') || undefined, decidedBy: 'cli', decidedAt: now };
285
290
  const closePolicy = hasArg(args, '--chatgpt-tab-close-policy') ? chatGptTabClosePolicy(args) : undefined;
291
+ const size = storySize(args);
292
+ const timeoutMs = hasArg(args, '--timeout') ? integerArg(args, '--timeout', taskOverallTimeoutMs(size, routing.reasoning)) : taskOverallTimeoutMs(size, routing.reasoning);
286
293
  return {
287
294
  jobId: job.id,
288
295
  id: arg(args, '--task-id', 'initial'),
289
296
  priority: priority(args, job.priority ?? 'P2'),
297
+ storySize: size,
290
298
  projectName: arg(args, '--project') || undefined,
291
299
  agentName: arg(args, '--agent') || undefined,
292
300
  parentTaskId: arg(args, '--parent') || null,
@@ -308,7 +316,7 @@ function workerTaskFromArgs(job, args, now, requireInput = false) {
308
316
  threadId: null,
309
317
  result: null,
310
318
  error: null,
311
- timeoutMs: Number(arg(args, '--timeout', '300000')),
319
+ timeoutMs,
312
320
  createdAt: now,
313
321
  updatedAt: now
314
322
  };
@@ -333,6 +341,11 @@ async function runTaskCommand(action, args) {
333
341
  }
334
342
  if (hasArg(args, '--priority'))
335
343
  task.priority = priority(args, task.priority ?? job.priority ?? 'P2');
344
+ if (hasArg(args, '--story-size')) {
345
+ task.storySize = storySize(args, task.storySize ?? 'M');
346
+ if (!hasArg(args, '--timeout'))
347
+ task.timeoutMs = taskOverallTimeoutMs(task.storySize, task.routing?.reasoning);
348
+ }
336
349
  if (hasArg(args, '--input'))
337
350
  task.input = arg(args, '--input');
338
351
  if (hasArg(args, '--project'))
@@ -369,6 +382,8 @@ async function runTaskCommand(action, args) {
369
382
  decidedBy: 'cli:update',
370
383
  decidedAt: new Date().toISOString()
371
384
  };
385
+ if (hasArg(args, '--reasoning') && !hasArg(args, '--timeout'))
386
+ task.timeoutMs = taskOverallTimeoutMs(task.storySize ?? 'M', task.routing.reasoning);
372
387
  }
373
388
  task.updatedAt = new Date().toISOString();
374
389
  await store.saveTask(task);
@@ -405,6 +420,7 @@ async function runJobCommand(action, args) {
405
420
  objective: arg(args, '--objective') || undefined,
406
421
  priority: priority(args),
407
422
  executionMode: requestedExecutionMode(args),
423
+ autoMerge: !hasArg(args, '--no-auto-merge'),
408
424
  continuation: continuation(args),
409
425
  prNumber,
410
426
  head: arg(args, '--head') || undefined,
@@ -518,7 +534,7 @@ async function main() {
518
534
  }
519
535
  if (command === 'init') {
520
536
  const now = new Date().toISOString();
521
- const job = { id: arg(args, '--id', randomUUID()), title: arg(args, '--title', 'Agents Relay job'), objective: arg(args, '--objective') || undefined, priority: priority(args), executionMode: executionMode(args), prNumber: Number(arg(args, '--pr', '0')), repository: arg(args, '--repo'), state: 'OPEN', continuation: continuation(args), createdAt: now, updatedAt: now, tasks: [] };
537
+ const job = { id: arg(args, '--id', randomUUID()), title: arg(args, '--title', 'Agents Relay job'), objective: arg(args, '--objective') || undefined, priority: priority(args), executionMode: executionMode(args), autoMerge: !hasArg(args, '--no-auto-merge'), prNumber: Number(arg(args, '--pr', '0')), repository: arg(args, '--repo'), state: 'OPEN', continuation: continuation(args), createdAt: now, updatedAt: now, tasks: [] };
522
538
  let store;
523
539
  let file = null;
524
540
  if (arg(args, '--repo') && arg(args, '--pr'))
package/dist/planner.js CHANGED
@@ -2,6 +2,7 @@ import { spawn } from 'node:child_process';
2
2
  import { readFile } from 'node:fs/promises';
3
3
  import { fileURLToPath } from 'node:url';
4
4
  import { dirname, join } from 'node:path';
5
+ import { taskOverallTimeoutMs } from './types.js';
5
6
  function record(value) {
6
7
  if (!value || typeof value !== 'object' || Array.isArray(value))
7
8
  throw new Error('Planner result must be an object');
@@ -83,17 +84,22 @@ export function parsePlannerResult(value) {
83
84
  if (output.kind === 'file' && (typeof output.path !== 'string' || output.path.length === 0))
84
85
  throw new Error(`Planner next_tasks[${index}].output.path is required for file output`);
85
86
  const priority = task.priority === undefined ? undefined : normalizePlannerPriority(task.priority, `next_tasks[${index}].priority`);
87
+ const sizeValue = task.storySize === undefined ? 'M' : String(task.storySize).toUpperCase();
88
+ if (!['S', 'M', 'L'].includes(sizeValue))
89
+ throw new Error('Planner next_tasks[' + index + '].storySize must be S, M, or L');
90
+ const storySize = sizeValue;
86
91
  const maxAttempts = task.maxAttempts === undefined ? 3 : task.maxAttempts;
87
- const timeoutMs = task.timeoutMs === undefined ? 300000 : task.timeoutMs;
92
+ const route = routing(task.routing, 'next_tasks[' + index + '].routing');
93
+ const timeoutMs = task.timeoutMs === undefined ? taskOverallTimeoutMs(storySize, route?.reasoning) : task.timeoutMs;
88
94
  if (typeof maxAttempts !== 'number' || !Number.isInteger(maxAttempts) || maxAttempts < 1)
89
95
  throw new Error(`Planner next_tasks[${index}].maxAttempts is invalid`);
90
96
  if (typeof timeoutMs !== 'number' || !Number.isFinite(timeoutMs) || timeoutMs < 0)
91
97
  throw new Error(`Planner next_tasks[${index}].timeoutMs is invalid`);
92
98
  return {
93
- id, input: task.input, priority,
99
+ id, input: task.input, priority, storySize,
94
100
  projectName: optionalString(task.projectName, 'projectName'), agentName: optionalString(task.agentName, 'agentName'),
95
101
  dependencies: stringArray(task.dependencies, 'dependencies'), capabilities: stringArray(task.capabilities, 'capabilities'),
96
- adapter: adapter, output: output, routing: routing(task.routing, `next_tasks[${index}].routing`),
102
+ adapter: adapter, output: output, routing: route,
97
103
  continuation: task.continuation, maxAttempts, timeoutMs
98
104
  };
99
105
  });
@@ -1,4 +1,5 @@
1
1
  import { randomUUID } from 'node:crypto';
2
+ import { storySizeTimeoutMs, taskOverallTimeoutMs } from './types.js';
2
3
  import { schedule, transitionTask } from './scheduler.js';
3
4
  import { LaunchFailure, StartAckTimeout } from './adapters.js';
4
5
  import { eventFor } from './events.js';
@@ -203,7 +204,7 @@ export class Reconciler {
203
204
  return;
204
205
  const id = `final-review-${headSha.slice(0, 12)}`;
205
206
  const now = new Date().toISOString();
206
- const task = { jobId: job.id, id, kind: 'work', priority: job.priority, projectName: job.title, agentName: 'reviewer', parentTaskId: null, dependencies: job.tasks.filter(item => item.agentName !== 'reviewer' && !this.isSupersededTask(item)).map(item => item.id), capabilities: ['review'], adapter: this.options.reviewer?.adapter ?? 'codex', input: `Review PR #${job.prNumber} at head SHA ${headSha}. Objective: ${job.objective ?? job.description ?? job.title}. First publish task.started as the worker start acknowledgement with data.worker_owned=true, then verify the deterministic pre-review gates have passed and inspect the completed work. Emit review.started, then exactly one of review.approved, review.changes_required, or review.blocked with data.review.headSha set to ${headSha}. Do not modify code and do not merge.`, output: { kind: 'task_pr' }, routing: this.options.reviewer?.routing, continuation: null, continuationDeliveredAt: null, reviewHeadSha: headSha, state: 'QUEUED', attempt: 0, maxAttempts: this.options.reviewer?.maxAttempts ?? 2, leaseOwner: null, leaseExpiresAt: null, executionId: null, threadId: null, result: null, plannerResult: null, error: null, timeoutMs: this.options.reviewer?.timeoutMs ?? 300000, createdAt: now, updatedAt: now };
207
+ const task = { jobId: job.id, id, kind: 'work', priority: job.priority, storySize: 'M', projectName: job.title, agentName: 'reviewer', parentTaskId: null, dependencies: job.tasks.filter(item => item.agentName !== 'reviewer' && !this.isSupersededTask(item)).map(item => item.id), capabilities: ['review'], adapter: this.options.reviewer?.adapter ?? 'codex', input: `Review PR #${job.prNumber} at head SHA ${headSha}. Objective: ${job.objective ?? job.description ?? job.title}. First publish task.started as the worker start acknowledgement with data.worker_owned=true, then verify the deterministic pre-review gates have passed and inspect the completed work. Emit review.started, then exactly one of review.approved, review.changes_required, or review.blocked with data.review.headSha set to ${headSha}. Do not modify code. When auto-merge is enabled, Relay will merge this exact reviewed head after review.approved.`, output: { kind: 'task_pr' }, routing: this.options.reviewer?.routing, continuation: null, continuationDeliveredAt: null, reviewHeadSha: headSha, state: 'QUEUED', attempt: 0, maxAttempts: this.options.reviewer?.maxAttempts ?? 2, leaseOwner: null, leaseExpiresAt: null, executionId: null, threadId: null, result: null, plannerResult: null, error: null, timeoutMs: this.options.reviewer?.timeoutMs ?? storySizeTimeoutMs('M'), createdAt: now, updatedAt: now };
207
208
  await this.store.appendTask(task);
208
209
  job.review = { outcome: 'pending', headSha, taskId: id, evidence: null, updatedAt: now };
209
210
  job.state = 'RUNNING';
@@ -246,6 +247,10 @@ export class Reconciler {
246
247
  }
247
248
  job.updatedAt = job.review.updatedAt;
248
249
  await this.store.saveJob(job);
250
+ if (outcome === 'approved' && job.autoMerge !== false && this.store.mergePullRequest) {
251
+ await this.store.mergePullRequest(headSha);
252
+ await this.emit(eventFor(job.id, task.id, task.parentTaskId, 'review.merged', 'succeeded', 'Reviewer-approved PR #' + job.prNumber + ' merged', 'user', { headSha, autoMerge: true }, { agent: 'reviewer', component: 'agents-relay' }));
253
+ }
249
254
  }
250
255
  async recoverLostExecutions(job, now) {
251
256
  for (const task of job.tasks.filter(item => item.state === 'RUNNING')) {
@@ -297,7 +302,7 @@ export class Reconciler {
297
302
  id = `planner-${number}`;
298
303
  }
299
304
  const now = new Date().toISOString();
300
- const task = { jobId: job.id, id, kind: 'planner', agentName: 'planner', parentTaskId: latest?.id ?? null, dependencies: [], capabilities: ['planner'], adapter: 'orchestrator', input: plannerInput({ job, tasks: job.tasks, objective: job.objective ?? job.description ?? job.title, previousPlannerTaskId: latest?.id ?? null }), continuation: null, continuationDeliveredAt: null, state: 'QUEUED', attempt: 0, maxAttempts: 3, leaseOwner: null, leaseExpiresAt: null, executionId: null, threadId: null, result: null, plannerResult: null, error: null, timeoutMs: this.options.plannerTimeoutMs ?? 300000, createdAt: now, updatedAt: now };
305
+ const task = { jobId: job.id, id, kind: 'planner', storySize: 'M', agentName: 'planner', parentTaskId: latest?.id ?? null, dependencies: [], capabilities: ['planner'], adapter: 'orchestrator', input: plannerInput({ job, tasks: job.tasks, objective: job.objective ?? job.description ?? job.title, previousPlannerTaskId: latest?.id ?? null }), continuation: null, continuationDeliveredAt: null, state: 'QUEUED', attempt: 0, maxAttempts: 3, leaseOwner: null, leaseExpiresAt: null, executionId: null, threadId: null, result: null, plannerResult: null, error: null, timeoutMs: this.options.plannerTimeoutMs ?? storySizeTimeoutMs('M'), createdAt: now, updatedAt: now };
301
306
  await this.store.appendTask(task);
302
307
  await this.emit(eventFor(job.id, task.id, task.parentTaskId, 'planner.created', 'queued', `Planner task ${task.id} created`, 'orchestrator'));
303
308
  return this.store.load(job.id);
@@ -492,7 +497,7 @@ export class Reconciler {
492
497
  }
493
498
  plannerChild(job, planner, spec) {
494
499
  const now = new Date().toISOString();
495
- return { jobId: job.id, id: spec.id, kind: 'work', priority: spec.priority ?? job.priority, projectName: spec.projectName, agentName: spec.agentName, parentTaskId: planner.id, dependencies: spec.dependencies ?? [], capabilities: spec.capabilities ?? [], adapter: spec.adapter, input: spec.input, output: spec.output, routing: spec.routing, continuation: spec.continuation ?? null, continuationDeliveredAt: null, state: 'QUEUED', attempt: 0, maxAttempts: spec.maxAttempts ?? 3, leaseOwner: null, leaseExpiresAt: null, executionId: null, threadId: null, result: null, plannerResult: null, error: null, timeoutMs: spec.timeoutMs ?? 300000, createdAt: now, updatedAt: now };
500
+ return { jobId: job.id, id: spec.id, kind: 'work', priority: spec.priority ?? job.priority, storySize: spec.storySize ?? 'M', projectName: spec.projectName, agentName: spec.agentName, parentTaskId: planner.id, dependencies: spec.dependencies ?? [], capabilities: spec.capabilities ?? [], adapter: spec.adapter, input: spec.input, output: spec.output, routing: spec.routing, continuation: spec.continuation ?? null, continuationDeliveredAt: null, state: 'QUEUED', attempt: 0, maxAttempts: spec.maxAttempts ?? 3, leaseOwner: null, leaseExpiresAt: null, executionId: null, threadId: null, result: null, plannerResult: null, error: null, timeoutMs: spec.timeoutMs ?? taskOverallTimeoutMs(spec.storySize ?? 'M', spec.routing?.reasoning), createdAt: now, updatedAt: now };
496
501
  }
497
502
  clearExecution(taskId, executionId) {
498
503
  const live = this.live.get(taskId);
@@ -693,5 +698,5 @@ export class Reconciler {
693
698
  } }
694
699
  }
695
700
  function samePlannerChildDefinition(left, right) {
696
- return JSON.stringify({ id: left.id, input: left.input, output: left.output, priority: left.priority, projectName: left.projectName, agentName: left.agentName, dependencies: left.dependencies, capabilities: left.capabilities, adapter: left.adapter, routing: left.routing, continuation: left.continuation ?? null, maxAttempts: left.maxAttempts, timeoutMs: left.timeoutMs }) === JSON.stringify({ id: right.id, input: right.input, output: right.output, priority: right.priority, projectName: right.projectName, agentName: right.agentName, dependencies: right.dependencies, capabilities: right.capabilities, adapter: right.adapter, routing: right.routing, continuation: right.continuation ?? null, maxAttempts: right.maxAttempts, timeoutMs: right.timeoutMs });
701
+ return JSON.stringify({ id: left.id, input: left.input, output: left.output, priority: left.priority, storySize: left.storySize ?? 'M', projectName: left.projectName, agentName: left.agentName, dependencies: left.dependencies, capabilities: left.capabilities, adapter: left.adapter, routing: left.routing, continuation: left.continuation ?? null, maxAttempts: left.maxAttempts, timeoutMs: left.timeoutMs }) === JSON.stringify({ id: right.id, input: right.input, output: right.output, priority: right.priority, storySize: right.storySize ?? 'M', projectName: right.projectName, agentName: right.agentName, dependencies: right.dependencies, capabilities: right.capabilities, adapter: right.adapter, routing: right.routing, continuation: right.continuation ?? null, maxAttempts: right.maxAttempts, timeoutMs: right.timeoutMs });
697
702
  }
package/dist/store.js CHANGED
@@ -81,7 +81,7 @@ function immutableTaskDefinition(task) {
81
81
  reasoning: task.routing.reasoning, cwd: task.routing.cwd, projectId: task.routing.projectId
82
82
  } : undefined;
83
83
  return {
84
- jobId: task.jobId, id: task.id, priority: task.priority, projectName: task.projectName, agentName: task.agentName,
84
+ jobId: task.jobId, id: task.id, priority: task.priority, storySize: task.storySize ?? 'M', projectName: task.projectName, agentName: task.agentName,
85
85
  kind: task.kind ?? 'work', parentTaskId: task.parentTaskId, dependencies: task.dependencies, capabilities: task.capabilities, adapter: task.adapter,
86
86
  input: task.input, output: task.output, routing, chatgptTabClosePolicy: task.chatgptTabClosePolicy ?? 'after-start', continuation: task.continuation ?? null, maxAttempts: task.maxAttempts, timeoutMs: task.timeoutMs
87
87
  };
@@ -461,6 +461,17 @@ export class GitHubStore {
461
461
  return 'MERGED';
462
462
  return String(pr.state ?? '').toUpperCase() === 'CLOSED' ? 'CLOSED' : 'OPEN';
463
463
  }
464
+ async mergePullRequest(expectedHeadSha) {
465
+ const payload = { merge_method: 'squash' };
466
+ if (expectedHeadSha)
467
+ payload.sha = expectedHeadSha;
468
+ const result = await this.client.request('repos/' + this.repository + '/pulls/' + this.prNumber + '/merge', {
469
+ method: 'PUT',
470
+ body: JSON.stringify(payload),
471
+ });
472
+ if (result.merged !== true)
473
+ throw new Error(typeof result.message === 'string' ? result.message : 'Pull request #' + this.prNumber + ' was not merged');
474
+ }
464
475
  async headSha() {
465
476
  const { owner, name } = this.repoParts();
466
477
  const data = await this.graphql('query($owner:String!,$name:String!,$number:Int!){repository(owner:$owner,name:$name){pullRequest(number:$number){headRefOid}}}', { owner, name, number: this.prNumber });
package/dist/types.js CHANGED
@@ -1,3 +1,6 @@
1
+ export const STORY_SIZE_TIMEOUT_MS = { S: 120000, M: 300000, L: 900000 };
2
+ export function storySizeTimeoutMs(size) { return STORY_SIZE_TIMEOUT_MS[size]; }
3
+ export function taskOverallTimeoutMs(size, reasoning) { return storySizeTimeoutMs(size) * (reasoning?.toLowerCase() === 'high' ? 2 : 1); }
1
4
  const jobTransitions = { OPEN: ['RUNNING', 'WAITING', 'BLOCKED', 'COMPLETED', 'CANCELLED'], RUNNING: ['OPEN', 'WAITING', 'BLOCKED', 'COMPLETED', 'FAILED', 'CANCELLED'], WAITING: ['RUNNING', 'BLOCKED', 'COMPLETED', 'CANCELLED'], BLOCKED: ['RUNNING', 'CANCELLED'], COMPLETED: ['RUNNING', 'CANCELLED'], FAILED: ['RUNNING', 'CANCELLED'], CANCELLED: ['RUNNING'] };
2
5
  const taskTransitions = { QUEUED: ['READY', 'BLOCKED', 'CANCELLED'], READY: ['RUNNING', 'BLOCKED', 'FAILED', 'CANCELLED'], RUNNING: ['WAITING', 'BLOCKED', 'SUCCEEDED', 'FAILED', 'CANCELLED', 'READY'], WAITING: ['READY', 'BLOCKED', 'CANCELLED'], BLOCKED: ['READY', 'CANCELLED'], SUCCEEDED: [], FAILED: ['READY', 'CANCELLED'], CANCELLED: [] };
3
6
  export function assertJobTransition(from, to) { if (from !== to && !jobTransitions[from].includes(to))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agents-relay",
3
- "version": "1.0.26",
3
+ "version": "1.0.28",
4
4
  "description": "Durable async agent jobs coordinated through GitHub pull requests",
5
5
  "type": "module",
6
6
  "bin": {