agents-relay 1.0.49 → 1.0.50

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/README.md CHANGED
@@ -20,7 +20,7 @@ npx agents-relay submit --repo OWNER/REPO --pr 12 --id job-1 \
20
20
  --input "Implement the described child task and publish a terminal event."
21
21
 
22
22
  npx agents-relay reconcile --repo OWNER/REPO --pr 12 --id job-1
23
- npx agents-relay status --repo OWNER/REPO --pr 12 --id job-1
23
+ npx agents-relay job show --repo OWNER/REPO --pr 12 --id job-1
24
24
  ~~~
25
25
 
26
26
  Mutable queued/retryable task fields can be changed in place without replacing the durable task identity. `task update` rejects `RUNNING`, `SUCCEEDED`, and `CANCELLED` tasks so active execution and terminal history cannot be rewritten. For example:
@@ -57,19 +57,21 @@ operational contract.
57
57
 
58
58
  job create first resolves an existing open PR with the same head/base, then creates it only when needed. Re-running it is idempotent: it reuses the PR and maintains exactly one trusted agents-relay:job:v1 marker. Job creation is always create-only. Task execution options belong to `submit`, and the orchestrator may submit zero, one, or multiple initial tasks according to user intent.
59
59
 
60
- To adopt an existing unmanaged PR, including a PR with no comments:
60
+ To create a Job on an existing unmanaged PR, use the same `job create` action with `--pr` instead of `--head`:
61
61
 
62
62
  ~~~sh
63
- npx agents-relay job adopt --repo OWNER/REPO --pr 6 --id job-6 --title "Existing objective"
63
+ npx agents-relay job create --repo OWNER/REPO --pr 6 --id job-6 --title "Existing objective"
64
64
  ~~~
65
65
 
66
- Use job repair with the same arguments to repair duplicate same-job markers. A conflicting marker for another job is rejected rather than overwritten.
66
+ Use `job update` for partial mutable metadata changes on an already healthy Job marker/binding:
67
67
 
68
68
  ~~~sh
69
- npx agents-relay job repair --repo OWNER/REPO --pr 6 --id job-6
69
+ npx agents-relay job update --repo OWNER/REPO --pr 6 --id job-6 --priority P0 --no-auto-merge
70
70
  ~~~
71
71
 
72
- Managed GitHub submit/status/reconcile/retry/cancel/serve commands require the durable job marker. If it is missing, the CLI fails with a message directing the caller to job adopt. Orchestrators do not need raw gh PR/comment commands: Agents Relay uses GhApiClient and GitHubStore internally.
72
+ `job update` validates the trusted Job marker first. Missing, malformed, duplicated, mismatched, or conflicting bindings fail with a precise diagnostic and are not repaired implicitly. The orchestrator handles exceptional repair explicitly according to the Job contract, then retries the update. `job show` reads one Job and `job list` lists managed Jobs in a repository.
73
+
74
+ Managed GitHub submit/reconcile/retry/cancel/serve commands also require a durable Job marker. Orchestrators do not need raw GitHub commands for normal Job creation or metadata updates: Agents Relay uses its authenticated GitHub client internally.
73
75
 
74
76
  The original init command remains supported for backward compatibility:
75
77
 
package/dist/cli.js CHANGED
@@ -19,7 +19,7 @@ import { extendRetryBudget, parseJobSchedule } from './scheduler.js';
19
19
  import { dueBuckets, processDueScheduledJobs, syncScheduleLabels } from './scheduled-jobs.js';
20
20
  import { resolveExecutionContext } from './execution-context.js';
21
21
  import { defaultWorkspaceRoot, resolveManagedProjectPath } from './workspace.js';
22
- const usage = 'agents-relay <job|task|context|init|submit|record|agent-register|agent-discover|status|reconcile|tick|retry|cancel|serve> [options]';
22
+ const usage = 'agents-relay <job|task|context|init|submit|record|agent-register|agent-discover|reconcile|tick|retry|cancel|serve> [options]';
23
23
  const help = {
24
24
  root: `Agents Relay coordinates durable asynchronous agent jobs through GitHub pull requests.
25
25
 
@@ -27,7 +27,7 @@ Usage:
27
27
  npx agents-relay <command> [options]
28
28
 
29
29
  Commands:
30
- job create|adopt|repair Create or repair a durable GitHub-backed job
30
+ job create|update|show|list Manage durable GitHub-backed jobs
31
31
  task update Update mutable durable task fields in place
32
32
  context resolve Resolve shared project instructions, skills, agents, and runtimes
33
33
  init Create a local demo job or initialize a GitHub job
@@ -35,7 +35,6 @@ Commands:
35
35
  record Backfill a completed task into durable history
36
36
  agent-register Register an agent role and its capabilities
37
37
  agent-discover Find registered agents matching hard filters
38
- status Show the durable job state
39
38
  reconcile Run reconciliation once
40
39
  tick Run the indexed autonomous scheduler heartbeat
41
40
  retry|cancel Change a task state
@@ -43,52 +42,53 @@ Commands:
43
42
 
44
43
  Run 'npx agents-relay <command> --help' for command options and examples.
45
44
  Use 'node dist/cli.js' only for explicit local development/demo mode.`,
46
- job: `Manage a GitHub-backed durable job.
45
+ job: `Manage GitHub-backed durable jobs.
47
46
 
48
47
  Usage:
49
- npx agents-relay job <create|adopt|repair> [options]
48
+ npx agents-relay job <create|update|show|list> [options]
50
49
 
51
50
  Actions:
52
- create Find or create an open PR, then persist its job marker
53
- adopt Attach a job marker to an existing PR
54
- repair Repair duplicate markers for the same job
51
+ create Create a durable Job, optionally binding an existing unmanaged PR
52
+ update Partially update mutable metadata on a healthy existing Job
53
+ show Show one durable Job
54
+ list List managed Jobs in a repository
55
55
 
56
56
  Common options:
57
- --repo OWNER/REPO GitHub repository (required)
58
- --id JOB_ID Durable job identifier (required)
59
- --pr NUMBER Existing PR number (required for adopt/repair)
60
- --title TEXT Job title
61
- --objective TEXT Objective for autonomous mode
62
- --mode fixed|autonomous Execution mode (default: fixed)
63
- --schedule CRON Autonomous schedule; minute resolution is 10 minutes
64
- --schedule-timezone TZ IANA timezone for --schedule (default: UTC)
65
- --schedule-next-run ISO Optional first run (must match the cron expression)
66
- --missed-run-policy run|skip Run each missed slot or skip to the next future slot
67
- --schedule-concurrency skip|queue Skip or retain the slot while work is active
57
+ --repo OWNER/REPO GitHub repository
58
+ --id JOB_ID Durable job identifier
59
+ --pr NUMBER Existing PR number
68
60
 
69
61
  Example:
70
- npx agents-relay job create --repo OWNER/REPO --head feat/example --base main --id job-1`,
71
- 'job create': `Create or reuse an open PR and persist one durable job marker.
62
+ npx agents-relay job show --repo OWNER/REPO --pr 12 --id job-1`,
63
+ 'job create': `Create a durable Job and its trusted marker.
72
64
 
73
- Required: --repo OWNER/REPO, --id JOB_ID, and --head BRANCH.
74
- Common: --base BRANCH (default: main), --title TEXT, --body TEXT, --mode fixed|autonomous, --no-auto-merge, --no-review.
75
- Job creation never creates executable tasks. The orchestrator decides the initial task batch separately with submit.
65
+ Required: --repo OWNER/REPO, --id JOB_ID, and either --head BRANCH or --pr NUMBER.
66
+ With --head, reuse/create the PR for that head. With --pr, bind an existing unmanaged PR.
67
+ Common: --base BRANCH (default: main), --title TEXT, --body TEXT, --objective TEXT, --priority P0|P1|P2|P3, --mode fixed|autonomous, --no-auto-merge, --no-review.
68
+ Job creation never creates executable tasks. The orchestrator decides the initial task batch separately. Re-running create against an already healthy Job is idempotent and does not mutate its metadata.
76
69
 
77
70
  Example:
78
71
  npx agents-relay job create --repo OWNER/REPO --head feat/example --base main --id job-1 --title "Objective"`,
79
- 'job adopt': `Adopt an existing PR as a durable job.
72
+ 'job update': `Partially update mutable metadata on a healthy existing Job.
80
73
 
81
74
  Required: --repo OWNER/REPO, --pr NUMBER, and --id JOB_ID.
82
- Common: --title TEXT, --objective TEXT, --mode fixed|autonomous, --no-auto-merge.
75
+ Mutable: --title, --objective, --priority, --mode fixed|autonomous, --schedule, --auto-merge|--no-auto-merge, --review|--no-review, and continuation options.
76
+ Unspecified fields are preserved. Marker/binding corruption is never repaired implicitly; update fails with a precise diagnostic instead.
83
77
 
84
78
  Example:
85
- npx agents-relay job adopt --repo OWNER/REPO --pr 12 --id job-12 --title "Objective"`,
86
- 'job repair': `Repair duplicate durable markers for an existing job.
79
+ npx agents-relay job update --repo OWNER/REPO --pr 12 --id job-1 --priority P0 --no-auto-merge`,
80
+ 'job show': `Show one durable Job.
87
81
 
88
82
  Required: --repo OWNER/REPO, --pr NUMBER, and --id JOB_ID.
89
83
 
90
84
  Example:
91
- npx agents-relay job repair --repo OWNER/REPO --pr 12 --id job-12`,
85
+ npx agents-relay job show --repo OWNER/REPO --pr 12 --id job-1`,
86
+ 'job list': `List managed Jobs in a repository.
87
+
88
+ Required: --repo OWNER/REPO.
89
+
90
+ Example:
91
+ npx agents-relay job list --repo OWNER/REPO`,
92
92
  task: `Manage a durable task.
93
93
 
94
94
  Usage:
@@ -133,12 +133,6 @@ Common: --task-id ID, --commit SHA, --project NAME.
133
133
 
134
134
  Example:
135
135
  npx agents-relay record --file .agents-relay.json --summary "Built release" --commit abc123`,
136
- status: `Show the durable state for a job.
137
-
138
- Required: --file PATH for local demo mode, or --repo OWNER/REPO --pr NUMBER --id JOB_ID.
139
-
140
- Example:
141
- npx agents-relay status --repo OWNER/REPO --pr 12 --id job-1`,
142
136
  reconcile: `Reconcile a job once, launching ready tasks and persisting results.
143
137
 
144
138
  Required: --file PATH for local demo mode, or --repo OWNER/REPO --pr NUMBER --id JOB_ID.
@@ -287,34 +281,36 @@ export async function ensureManagedGitHubJob(client, repositoryName, trustedAuth
287
281
  throw new Error('--id is required for managed GitHub jobs');
288
282
  const pr = await resolvePullRequest(client, repositoryName, { prNumber: options.prNumber, head: options.head, base: options.base, title: options.title, body: options.body });
289
283
  const store = new GitHubStore(client, repositoryName, pr.number, trustedAuthors);
290
- let job;
291
284
  try {
292
- job = await store.load(options.id);
285
+ const existing = await store.load(options.id);
286
+ if (existing.schedule)
287
+ await syncScheduleLabels(client, repositoryName, existing);
288
+ return { job: existing, pr };
293
289
  }
294
290
  catch (error) {
295
- if (!(error instanceof Error) || !error.message.includes('has no durable'))
291
+ if (!(error instanceof Error) || !error.message.startsWith('JOB_MARKER_MISSING:'))
296
292
  throw error;
297
- const now = new Date().toISOString();
298
- 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, reviewEnabled: options.reviewEnabled ?? true, prNumber: pr.number, repository: repositoryName, state: 'OPEN', continuation: options.continuation ?? null, createdAt: now, updatedAt: now, tasks: [] };
299
- }
300
- if (!job.description && pr.body)
301
- job.description = pr.body;
302
- if (options.objective)
303
- job.objective = options.objective;
304
- if (options.executionMode)
305
- job.executionMode = options.executionMode;
293
+ }
294
+ const now = new Date().toISOString();
295
+ const job = {
296
+ id: options.id,
297
+ title: options.title || pr.title || 'Agents Relay job',
298
+ objective: options.objective || pr.body || undefined,
299
+ description: pr.body || undefined,
300
+ priority: options.priority ?? 'P2',
301
+ executionMode: options.executionMode ?? 'fixed',
302
+ autoMerge: options.autoMerge ?? true,
303
+ reviewEnabled: options.reviewEnabled ?? true,
304
+ prNumber: pr.number,
305
+ repository: repositoryName,
306
+ state: 'OPEN',
307
+ continuation: options.continuation ?? null,
308
+ createdAt: now,
309
+ updatedAt: now,
310
+ tasks: []
311
+ };
306
312
  if (options.schedule)
307
313
  job.schedule = options.schedule;
308
- if (options.autoMerge !== undefined)
309
- job.autoMerge = options.autoMerge;
310
- if (options.reviewEnabled !== undefined)
311
- job.reviewEnabled = options.reviewEnabled;
312
- if (options.priority)
313
- job.priority = options.priority;
314
- else if (!job.priority)
315
- job.priority = 'P2';
316
- if (!job.title && pr.title)
317
- job.title = pr.title;
318
314
  await store.saveJob(job);
319
315
  if (job.schedule)
320
316
  await syncScheduleLabels(client, repositoryName, job);
@@ -474,43 +470,90 @@ async function runContextCommand(action, args) {
474
470
  console.log(JSON.stringify(context, null, 2));
475
471
  }
476
472
  async function runJobCommand(action, args) {
477
- if (!['create', 'adopt', 'repair'].includes(action))
473
+ if (!['create', 'update', 'show', 'list'].includes(action))
478
474
  throw new Error(usage);
479
475
  const repo = repository(args);
480
- const id = arg(args, '--id');
481
- if (!id)
482
- throw new Error('--id is required for managed GitHub jobs');
483
476
  if (action === 'create') {
484
477
  const taskOnlyFlags = ['--start', '--task-id', '--input', '--adapter', '--provider', '--model', '--profile', '--reasoning', '--story-size', '--project', '--agent', '--parent', '--deps', '--capabilities', '--attachments', '--max-attempts', '--timeout', '--cwd', '--chatgpt-project', '--chatgpt-tab-close-policy', '--output', '--output-path'];
485
478
  const invalid = taskOnlyFlags.find(flag => hasArg(args, flag));
486
479
  if (invalid)
487
- throw new Error(`job create does not accept task execution option ${invalid}; create the job first, then let the orchestrator submit the initial task batch`);
480
+ throw new Error(`job create does not accept task execution option ${invalid}; create the job first, then let the orchestrator create the initial plan/task batch`);
488
481
  }
489
482
  const auth = await githubAuthContext(args);
490
- const client = auth.client;
491
- const trustedAuthors = auth.trustedAuthors;
483
+ if (action === 'list') {
484
+ console.log(JSON.stringify(await listManagedGitHubJobs(auth.client, repo, auth.trustedAuthors), null, 2));
485
+ return;
486
+ }
487
+ const id = arg(args, '--id');
488
+ if (!id)
489
+ throw new Error('--id is required for managed GitHub jobs');
492
490
  const prValue = arg(args, '--pr');
493
491
  const prNumber = prValue ? Number(prValue) : undefined;
494
492
  if (prValue && (!Number.isInteger(prNumber) || (prNumber ?? 0) <= 0))
495
493
  throw new Error('--pr must be a positive integer');
496
- if ((action === 'adopt' || action === 'repair') && !prNumber)
494
+ if (action === 'create') {
495
+ if (!prNumber && !arg(args, '--head'))
496
+ throw new Error('job create requires either --head or --pr');
497
+ const result = await ensureManagedGitHubJob(auth.client, repo, auth.trustedAuthors, {
498
+ id,
499
+ title: arg(args, '--title') || undefined,
500
+ objective: arg(args, '--objective') || undefined,
501
+ priority: priority(args),
502
+ executionMode: requestedExecutionMode(args),
503
+ schedule: scheduleFromArgs(args),
504
+ autoMerge: !hasArg(args, '--no-auto-merge'),
505
+ reviewEnabled: !hasArg(args, '--no-review'),
506
+ continuation: continuation(args),
507
+ prNumber,
508
+ head: arg(args, '--head') || undefined,
509
+ base: arg(args, '--base', 'main'),
510
+ body: arg(args, '--body') || undefined
511
+ });
512
+ console.log(JSON.stringify({ repository: repo, pr: result.pr.number, job: result.job.id, executionMode: result.job.executionMode, state: result.job.state }, null, 2));
513
+ return;
514
+ }
515
+ if (!prNumber)
497
516
  throw new Error(`job ${action} requires --pr`);
498
- const result = await ensureManagedGitHubJob(client, repo, trustedAuthors, {
499
- id,
500
- title: arg(args, '--title') || undefined,
501
- objective: arg(args, '--objective') || undefined,
502
- priority: priority(args),
503
- executionMode: requestedExecutionMode(args),
504
- schedule: scheduleFromArgs(args),
505
- autoMerge: !hasArg(args, '--no-auto-merge'),
506
- reviewEnabled: !hasArg(args, '--no-review'),
507
- continuation: continuation(args),
508
- prNumber,
509
- head: arg(args, '--head') || undefined,
510
- base: arg(args, '--base', 'main'),
511
- body: arg(args, '--body') || undefined
512
- });
513
- console.log(JSON.stringify({ repository: repo, pr: result.pr.number, job: result.job.id, executionMode: result.job.executionMode, state: result.job.state }, null, 2));
517
+ const store = new GitHubStore(auth.client, repo, prNumber, auth.trustedAuthors);
518
+ const job = await store.load(id);
519
+ if (action === 'show') {
520
+ console.log(JSON.stringify(job, null, 2));
521
+ return;
522
+ }
523
+ await store.validateJobBinding(id);
524
+ if (hasArg(args, '--title'))
525
+ job.title = arg(args, '--title');
526
+ if (hasArg(args, '--objective'))
527
+ job.objective = arg(args, '--objective');
528
+ if (hasArg(args, '--priority'))
529
+ job.priority = priority(args, job.priority ?? 'P2');
530
+ if (hasArg(args, '--mode') || hasArg(args, '--execution-mode'))
531
+ job.executionMode = executionMode(args, job.executionMode ?? 'fixed');
532
+ if (hasArg(args, '--schedule'))
533
+ job.schedule = scheduleFromArgs(args);
534
+ if (hasArg(args, '--auto-merge') && hasArg(args, '--no-auto-merge'))
535
+ throw new Error('choose only one of --auto-merge or --no-auto-merge');
536
+ if (hasArg(args, '--auto-merge'))
537
+ job.autoMerge = true;
538
+ if (hasArg(args, '--no-auto-merge'))
539
+ job.autoMerge = false;
540
+ if (hasArg(args, '--review') && hasArg(args, '--no-review'))
541
+ throw new Error('choose only one of --review or --no-review');
542
+ if (hasArg(args, '--review'))
543
+ job.reviewEnabled = true;
544
+ if (hasArg(args, '--no-review'))
545
+ job.reviewEnabled = false;
546
+ if (hasArg(args, '--continue-kind') || hasArg(args, '--continue-target')) {
547
+ const next = continuation(args);
548
+ if (!next)
549
+ throw new Error('--continue-kind and --continue-target must be supplied together');
550
+ job.continuation = next;
551
+ }
552
+ job.updatedAt = new Date().toISOString();
553
+ await store.saveJob(job);
554
+ if (hasArg(args, '--schedule') && job.schedule)
555
+ await syncScheduleLabels(auth.client, repo, job);
556
+ console.log(JSON.stringify(await store.load(id), null, 2));
514
557
  }
515
558
  export function runtimePlanner(args) { const plannerCommand = arg([...args], '--planner-command'); const modelRuntime = arg([...args], '--codex', 'codex'); return plannerCommand ? new CommandObjectivePlanner(plannerCommand) : new AgentObjectivePlanner(modelRuntime); }
516
559
  export function runtimeReviewer() { return { requirements: { capabilities: ['model'] } }; }
package/dist/store.js CHANGED
@@ -485,7 +485,7 @@ export class GitHubStore {
485
485
  const jobs = comments.map(x => parseJob(x.body)).filter((x) => x !== null && typeof x.id === 'string' && x.id.length > 0);
486
486
  const job = jobs.find(x => x.id === jobId);
487
487
  if (!job)
488
- throw new Error(`Managed GitHub job ${jobId} has no durable ${JOB_MARKER} marker in PR #${this.prNumber}; run agents-relay job adopt --repo ${this.repository} --pr ${this.prNumber} --id ${jobId}`);
488
+ throw new Error(`JOB_MARKER_MISSING: Managed GitHub job ${jobId} has no durable ${JOB_MARKER} marker in PR #${this.prNumber}`);
489
489
  const soleTrustedJob = jobs.length === 1;
490
490
  job.tasks = comments.map(comment => {
491
491
  const task = parseTask(comment.body);
@@ -496,6 +496,25 @@ export class GitHubStore {
496
496
  }).filter((x) => x !== null).filter(task => isTaskForJob(task, jobId, soleTrustedJob));
497
497
  return job;
498
498
  }
499
+ async validateJobBinding(jobId) {
500
+ const comments = (await this.list()).filter(comment => this.isTrusted(comment));
501
+ const markerComments = comments.filter(comment => comment.body.includes(JOB_MARKER));
502
+ const parsed = markerComments.map(comment => parseJob(comment.body));
503
+ const malformed = parsed.filter(job => job === null).length;
504
+ if (malformed > 0)
505
+ throw new Error(`JOB_MARKER_MALFORMED: PR #${this.prNumber} contains ${malformed} trusted ${JOB_MARKER} marker(s) that cannot be parsed`);
506
+ const jobs = parsed.filter((job) => job !== null);
507
+ const matches = jobs.filter(job => job.id === jobId);
508
+ if (matches.length === 0) {
509
+ if (jobs.length > 0)
510
+ throw new Error(`JOB_ID_MISMATCH: PR #${this.prNumber} is bound to ${jobs.map(job => job.id).join(', ')}, not ${jobId}`);
511
+ throw new Error(`JOB_MARKER_MISSING: Managed GitHub job ${jobId} has no durable ${JOB_MARKER} marker in PR #${this.prNumber}`);
512
+ }
513
+ if (matches.length > 1)
514
+ throw new Error(`JOB_MARKER_DUPLICATED: PR #${this.prNumber} contains ${matches.length} trusted ${JOB_MARKER} markers for job ${jobId}`);
515
+ if (jobs.length > 1)
516
+ throw new Error(`JOB_BINDING_CONFLICT: PR #${this.prNumber} contains trusted ${JOB_MARKER} markers for multiple job ids`);
517
+ }
499
518
  async listAgents() { return (await this.list()).filter(comment => this.isTrusted(comment)).map(x => parseAgent(x.body)).filter((x) => x !== null); }
500
519
  async comment(body) {
501
520
  const pr = await this.prNode();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agents-relay",
3
- "version": "1.0.49",
3
+ "version": "1.0.50",
4
4
  "description": "Durable async agent jobs coordinated through GitHub pull requests",
5
5
  "type": "module",
6
6
  "bin": {