agents-relay 1.0.34 → 1.0.36

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
@@ -117,7 +117,7 @@ Example:
117
117
  submit: `Queue a task for a managed job.
118
118
 
119
119
  Required: --repo OWNER/REPO, --pr NUMBER, --id JOB_ID, and --input TEXT.
120
- Required for worker tasks: --adapter codex|chatgpt, --model MODEL, and --output task-pr|file. File output also requires --output-path PATH.\nCommon: --task-id ID, --priority P0|P1|P2|P3, --story-size S|M|L, --max-attempts N. Story size defaults to M. Task producers follow the shared Task / Job Contract; prefer --story-size over raw --timeout. Model-backed execution requires --events nats in the running reconciler/daemon.
120
+ Required for worker tasks: --adapter codex|chatgpt|orchestrator and --output task-pr|file. Codex/ChatGPT also require --model MODEL. File output also requires --output-path PATH.\nCommon: --task-id ID, --priority P0|P1|P2|P3, --story-size S|M|L, --max-attempts N. Story size defaults to M. Task producers follow the shared Task / Job Contract; prefer --story-size over raw --timeout. Model-backed execution requires --events nats in the running reconciler/daemon.
121
121
 
122
122
  Example:
123
123
  npx agents-relay submit --repo OWNER/REPO --pr 12 --id job-1 --task-id build --adapter codex --provider openai --model gpt-5-6 --output task-pr --input "Run the required tests and report the result through events."`,
@@ -329,9 +329,9 @@ function workerTaskFromArgs(job, args, now, requireInput = false) {
329
329
  const outputKind = arg(args, '--output');
330
330
  const outputPath = arg(args, '--output-path');
331
331
  const input = arg(args, '--input');
332
- if (!['codex', 'chatgpt'].includes(adapter))
333
- throw new Error('submit adapter must be codex or chatgpt');
334
- if (!model)
332
+ if (!['codex', 'chatgpt', 'orchestrator'].includes(adapter))
333
+ throw new Error('submit adapter must be codex, chatgpt, or orchestrator');
334
+ if (adapter !== 'orchestrator' && !model)
335
335
  throw new Error('submit requires --model for codex/chatgpt tasks');
336
336
  if (requireInput && !input)
337
337
  throw new Error('job create --start requires --input');
@@ -339,10 +339,10 @@ function workerTaskFromArgs(job, args, now, requireInput = false) {
339
339
  throw new Error('submit requires --output task-pr|file');
340
340
  if (outputKind === 'file' && !outputPath)
341
341
  throw new Error('--output file requires --output-path');
342
- 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 };
342
+ const routing = adapter === 'orchestrator' ? undefined : { 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 };
343
343
  const closePolicy = hasArg(args, '--chatgpt-tab-close-policy') ? chatGptTabClosePolicy(args) : undefined;
344
344
  const size = storySize(args);
345
- const timeoutMs = hasArg(args, '--timeout') ? integerArg(args, '--timeout', taskOverallTimeoutMs(size, routing.reasoning)) : taskOverallTimeoutMs(size, routing.reasoning);
345
+ const timeoutMs = adapter === 'orchestrator' ? 0 : hasArg(args, '--timeout') ? integerArg(args, '--timeout', taskOverallTimeoutMs(size, routing?.reasoning)) : taskOverallTimeoutMs(size, routing?.reasoning);
346
346
  return {
347
347
  jobId: job.id,
348
348
  id: arg(args, '--task-id', 'initial'),
@@ -160,6 +160,14 @@ export class Reconciler {
160
160
  for (const task of scheduled.tasks.filter(t => t.state === 'READY').sort((a, b) => compareTaskPriority(a, b, scheduled.priority ?? 'P2')).slice(0, capacity)) {
161
161
  if (task.kind === 'planner')
162
162
  await this.launchPlanner(scheduled, task);
163
+ else if (task.adapter === 'orchestrator') {
164
+ transitionTask(task, 'RUNNING');
165
+ task.leaseOwner = null;
166
+ task.leaseExpiresAt = null;
167
+ task.executionId = null;
168
+ await this.store.saveTask(task);
169
+ await this.emit(eventFor(scheduled.id, task.id, task.parentTaskId, 'task.orchestrator_assigned', 'running', `Task ${task.id} assigned to orchestrator self`, 'orchestrator', { adapter: 'orchestrator' }, { agent: 'relay', component: 'agents-relay' }));
170
+ }
163
171
  else
164
172
  await this.launch(scheduled, task);
165
173
  }
@@ -213,7 +221,7 @@ export class Reconciler {
213
221
  return;
214
222
  const id = `final-review-${headSha.slice(0, 12)}`;
215
223
  const now = new Date().toISOString();
216
- 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 };
224
+ 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}. For review.changes_required, include every actionable finding in data.review.issues as an array of concise issue strings; Relay will create one durable fix task containing all findings. 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 };
217
225
  await this.store.appendTask(task);
218
226
  job.review = { outcome: 'pending', headSha, taskId: id, evidence: null, updatedAt: now };
219
227
  job.state = 'RUNNING';
@@ -254,8 +262,42 @@ export class Reconciler {
254
262
  task.updatedAt = job.review.updatedAt;
255
263
  await this.store.saveTask(task);
256
264
  }
265
+ if (outcome === 'changes_required') {
266
+ const rawIssues = Array.isArray(reviewData?.issues) ? reviewData.issues : [];
267
+ const issues = rawIssues.map(item => {
268
+ if (typeof item === 'string')
269
+ return item.trim();
270
+ if (item && typeof item === 'object') {
271
+ const value = item;
272
+ const summary = typeof value.summary === 'string' ? value.summary : typeof value.message === 'string' ? value.message : typeof value.title === 'string' ? value.title : '';
273
+ return summary.trim();
274
+ }
275
+ return '';
276
+ }).filter(Boolean);
277
+ if (issues.length === 0)
278
+ issues.push(event.message || 'Address the reviewer-requested changes');
279
+ const now = event.timestamp || new Date().toISOString();
280
+ const id = `review-fix-${headSha.slice(0, 8)}`;
281
+ if (!job.tasks.some(item => item.id === id)) {
282
+ const input = `Address all reviewer findings for PR #${job.prNumber} at ${headSha}:\n\n${issues.map((issue, index) => `${index + 1}. ${issue}`).join('\n')}`;
283
+ const fixTask = {
284
+ jobId: job.id, id, kind: 'work', priority: job.priority, storySize: 'M', projectName: job.title,
285
+ agentName: 'review-fix', parentTaskId: task.id, dependencies: [task.id], capabilities: ['implementation'],
286
+ adapter: 'codex', input, output: { kind: 'task_pr' },
287
+ routing: { provider: 'openai', model: 'gpt-5.6-luna', reasoning: 'medium', decidedBy: 'review-fix', decidedAt: now },
288
+ continuation: null, continuationDeliveredAt: null, state: 'QUEUED', attempt: 0, maxAttempts: 3,
289
+ leaseOwner: null, leaseExpiresAt: null, executionId: null, threadId: null, result: null, plannerResult: null,
290
+ error: null, timeoutMs: storySizeTimeoutMs('M'), createdAt: now, updatedAt: now
291
+ };
292
+ await this.store.appendTask(fixTask);
293
+ await this.emit(eventFor(job.id, id, task.id, 'review.fix_task_created', 'queued', `Created one fix task ${id} for ${issues.length} reviewer finding(s)`, 'orchestrator', { headSha, issues }, { agent: 'relay', component: 'agents-relay' }));
294
+ }
295
+ job.state = 'RUNNING';
296
+ }
257
297
  job.updatedAt = job.review.updatedAt;
258
298
  await this.store.saveJob(job);
299
+ if (outcome === 'changes_required')
300
+ this.detach(this.reconcile(job.id), job.id, task.id, 'review-fix reconciliation');
259
301
  if (outcome === 'approved' && job.autoMerge !== false && this.store.mergePullRequest) {
260
302
  await this.store.mergePullRequest(headSha);
261
303
  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' }));
@@ -675,7 +717,7 @@ export class Reconciler {
675
717
  async applyTerminalEvent(event) {
676
718
  const job = await this.store.load(event.job_id);
677
719
  const task = job.tasks.find(item => item.id === event.task_id);
678
- if (!task || !['codex', 'chatgpt'].includes(task.adapter))
720
+ if (!task || !['codex', 'chatgpt', 'orchestrator'].includes(task.adapter))
679
721
  return;
680
722
  if (task.state === 'FAILED' && this.isRecoverableWatchdogFailure(task) && this.isWorkerOwnedEvent(event)) {
681
723
  if (event.timestamp && new Date(event.timestamp).getTime() < new Date(task.updatedAt).getTime())
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agents-relay",
3
- "version": "1.0.34",
3
+ "version": "1.0.36",
4
4
  "description": "Durable async agent jobs coordinated through GitHub pull requests",
5
5
  "type": "module",
6
6
  "bin": {