agents-relay 1.0.59 → 1.0.60

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
@@ -7,13 +7,24 @@ Agents Relay is a generic TypeScript runtime for durable asynchronous agent jobs
7
7
  For GitHub-backed work, create or adopt the PR through Agents Relay itself. This guarantees the durable job marker exists before any managed task is submitted.
8
8
 
9
9
  ~~~sh
10
+ cat > job.md <<'EOF'
11
+ # Goal
12
+
13
+ Standalone Job context. Put a Mermaid block first when a workflow/state/architecture diagram helps.
14
+ EOF
15
+
10
16
  npx agents-relay job create --repo OWNER/REPO --head feat/example --base main \
11
- --id job-1 --title "Objective" --body "PR description" --mode fixed
17
+ --id job-1 --title "Objective" --objective-file job.md --mode fixed
12
18
 
13
19
  # The orchestrator decides the initial batch separately
20
+ cat > task.md <<'EOF'
21
+ # Work
22
+
23
+ Implement this phase using the Job objective as context.
24
+ EOF
14
25
  npx agents-relay task create --repo OWNER/REPO --pr 12 --id job-1 \
15
26
  --task-id initial --adapter codex --provider openai --model MODEL --output task-pr \
16
- --input "Implement the objective and publish a terminal event."
27
+ --input-file task.md
17
28
 
18
29
  npx agents-relay task create --repo OWNER/REPO --pr 12 --id job-1 \
19
30
  --task-id child --adapter codex --provider openai --model MODEL --output task-pr \
@@ -38,6 +49,8 @@ npx agents-relay task update --repo OWNER/REPO --pr 12 --id job-1 --task-id chil
38
49
 
39
50
  Jobs separate durable lifecycle from derived runtime status. Durable lifecycle is `DRAFT | ACTIVE | COMPLETED | CANCELLED`: a newly created unscheduled Job is DRAFT, creating executable work activates it, and scheduled Jobs are ACTIVE because the scheduler is allowed to advance them. Runtime status (`IDLE`, `WAITING`, `QUEUED`, `RUNNING`, `BLOCKED`, `FAILED`, `REVIEWING`, `MERGE_PENDING`, `MERGED`, or `CANCELLED`) is computed from Tasks, Review, PR, and schedule facts and is never persisted as lifecycle truth.
40
51
 
52
+ Job objectives and Task inputs are Markdown. Prefer `--objective-file` / `--input-file` (use `-` for stdin) for durable long-form context; inline `--objective` / `--input` remain for short/simple text and are mutually exclusive with the file variants. When a diagram materially clarifies the work, put Mermaid first and then Markdown detail. A newly created Job must capture enough context to finish independently of the originating chat; its objective becomes the default PR body and objective updates keep the PR body synchronized.
53
+
41
54
  Jobs default to `fixed`. `job create` persists only the durable job and never
42
55
  creates executable tasks. The top-level orchestrator decides whether the user's
43
56
  request should start immediately and, if so, creates the first task batch
package/dist/cli.js CHANGED
@@ -64,15 +64,15 @@ Example:
64
64
 
65
65
  Required: --repo OWNER/REPO, --id JOB_ID, and either --head BRANCH or --pr NUMBER.
66
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.
67
+ Common: --base BRANCH (default: main), --title TEXT, --objective-file PATH|- (preferred), --objective MARKDOWN (short/simple only), --priority P0|P1|P2|P3, --mode fixed|autonomous, --no-auto-merge, --no-review. --objective and --objective-file are mutually exclusive. For a new --head Job, one is required and must contain enough standalone context to complete the Job without the originating chat/thread. Objective Markdown is also the default PR body; --body is an explicit override.
68
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.
69
69
 
70
70
  Example:
71
- npx agents-relay job create --repo OWNER/REPO --head feat/example --base main --id job-1 --title "Objective"`,
71
+ npx agents-relay job create --repo OWNER/REPO --head feat/example --base main --id job-1 --title "Objective" --objective-file job.md`,
72
72
  'job update': `Partially update mutable metadata on a healthy existing Job.
73
73
 
74
74
  Required: --repo OWNER/REPO, --pr NUMBER, and --id JOB_ID.
75
- Mutable: --title, --objective, --priority, --mode fixed|autonomous, --schedule, --auto-merge|--no-auto-merge, --review|--no-review, and continuation options.
75
+ Mutable: --title, --objective-file PATH|- (preferred) or --objective MARKDOWN, --priority, --mode fixed|autonomous, --schedule, --auto-merge|--no-auto-merge, --review|--no-review, and continuation options. Updating the objective also updates the PR body.
76
76
  Unspecified fields are preserved. Marker/binding corruption is never repaired implicitly; update fails with a precise diagnostic instead.
77
77
 
78
78
  Example:
@@ -109,16 +109,16 @@ Actions:
109
109
  cancel Cancel a Task`,
110
110
  'task create': `Create one durable Task under an existing Job.
111
111
 
112
- Required: normal Job storage options plus --input TEXT and --output task-pr|file. Codex/ChatGPT tasks also require --model MODEL.
112
+ Required: normal Job storage options plus --input-file PATH|- (preferred) or --input MARKDOWN, and --output task-pr|file. --input and --input-file are mutually exclusive. Codex/ChatGPT tasks also require --model MODEL.
113
113
  Common: --task-id ID, --adapter codex|chatgpt|orchestrator, --priority P0|P1|P2|P3, --story-size S|M|L, --deps a,b, --parent ID, --draft.\nTasks with dependencies start DRAFT; tasks without dependencies start QUEUED unless --draft is given.
114
114
  Directly-created Tasks are not wrapped in a synthetic Plan. Planner-generated Tasks are created internally by Relay from planner output.
115
115
 
116
116
  Example:
117
- npx agents-relay task create --repo OWNER/REPO --pr 12 --id job-1 --task-id build --adapter codex --provider openai --model MODEL --output task-pr --input "Implement the requested change."`,
117
+ npx agents-relay task create --repo OWNER/REPO --pr 12 --id job-1 --task-id build --adapter codex --provider openai --model MODEL --output task-pr --input-file task.md`,
118
118
  'task update': `Update mutable durable Task fields in place.
119
119
 
120
120
  Required: --task-id ID and the normal Job storage options.
121
- 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, --draft, --queue.
121
+ Mutable while not RUNNING/SUCCEEDED/CANCELLED: --adapter, --priority, --story-size, --input-file PATH|- or --input MARKDOWN, --project, --agent, --parent, --deps, --capabilities, --timeout, --max-attempts, --provider, --model, --profile, --reasoning, --cwd, --chatgpt-project, --chatgpt-tab-close-policy, --draft, --queue.
122
122
  Use --clear-routing to remove routing metadata.
123
123
 
124
124
  Example:
@@ -233,6 +233,23 @@ class ServiceEventBus {
233
233
  }
234
234
  function arg(args, name, fallback = '') { const index = args.indexOf(name); return index >= 0 ? args[index + 1] ?? fallback : fallback; }
235
235
  function hasArg(args, name) { return args.includes(name); }
236
+ async function readStdinText() {
237
+ const chunks = [];
238
+ for await (const chunk of process.stdin)
239
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));
240
+ return Buffer.concat(chunks).toString('utf8');
241
+ }
242
+ async function textOption(args, inlineFlag, fileFlag) {
243
+ if (hasArg(args, inlineFlag) && hasArg(args, fileFlag))
244
+ throw new Error(`${inlineFlag} and ${fileFlag} are mutually exclusive`);
245
+ if (hasArg(args, fileFlag)) {
246
+ const path = arg(args, fileFlag);
247
+ if (!path)
248
+ throw new Error(`${fileFlag} requires a path or - for stdin`);
249
+ return path === '-' ? readStdinText() : readFile(path, 'utf8');
250
+ }
251
+ return arg(args, inlineFlag);
252
+ }
236
253
  function integerArg(args, name, fallback) { if (!hasArg(args, name))
237
254
  return fallback; const value = Number(arg(args, name)); if (!Number.isInteger(value) || value < 0)
238
255
  throw new Error(`${name} must be a non-negative integer`); return value; }
@@ -328,15 +345,15 @@ export async function ensureManagedGitHubJob(client, repositoryName, trustedAuth
328
345
  await syncScheduleLabels(client, repositoryName, job);
329
346
  return { job: await store.load(options.id), pr };
330
347
  }
331
- function workerTaskFromArgs(job, args, now) {
348
+ async function workerTaskFromArgs(job, args, now) {
332
349
  const model = arg(args, '--model');
333
350
  const adapterValue = arg(args, '--adapter');
334
351
  const adapter = (adapterValue || 'codex');
335
352
  const outputKind = arg(args, '--output');
336
353
  const outputPath = arg(args, '--output-path');
337
- const input = arg(args, '--input');
338
- if (!input)
339
- throw new Error('task create requires --input');
354
+ const input = await textOption(args, '--input', '--input-file');
355
+ if (!input.trim())
356
+ throw new Error('task create requires --input-file PATH|- or --input MARKDOWN');
340
357
  if (!['codex', 'chatgpt', 'orchestrator'].includes(adapter))
341
358
  throw new Error('task create --adapter must be codex, chatgpt, or orchestrator');
342
359
  if (adapter !== 'orchestrator' && !model)
@@ -397,7 +414,7 @@ async function runTaskCommand(action, args) {
397
414
  if (['COMPLETED', 'CANCELLED'].includes(job.state))
398
415
  throw new Error(`Cannot create task on terminal job ${job.id} (${job.state})`);
399
416
  const now = new Date().toISOString();
400
- const task = workerTaskFromArgs(job, args, now);
417
+ const task = await workerTaskFromArgs(job, args, now);
401
418
  task.id = id;
402
419
  await store.appendTask(task);
403
420
  const refreshed = await store.load(job.id);
@@ -451,8 +468,8 @@ async function runTaskCommand(action, args) {
451
468
  if (!hasArg(args, '--timeout'))
452
469
  task.timeoutMs = taskOverallTimeoutMs(task.storySize, task.routing?.reasoning);
453
470
  }
454
- if (hasArg(args, '--input'))
455
- task.input = arg(args, '--input');
471
+ if (hasArg(args, '--input') || hasArg(args, '--input-file'))
472
+ task.input = await textOption(args, '--input', '--input-file');
456
473
  if (hasArg(args, '--project'))
457
474
  task.projectName = arg(args, '--project') || undefined;
458
475
  if (hasArg(args, '--agent'))
@@ -528,11 +545,15 @@ async function runJobCommand(action, args) {
528
545
  if (!['create', 'update', 'show', 'list', 'merge', 'cancel'].includes(action))
529
546
  throw new Error(usage);
530
547
  const repo = repository(args);
548
+ let createObjective;
531
549
  if (action === 'create') {
532
- 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', '--draft', '--queue'];
550
+ const taskOnlyFlags = ['--start', '--task-id', '--input', '--input-file', '--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', '--draft', '--queue'];
533
551
  const invalid = taskOnlyFlags.find(flag => hasArg(args, flag));
534
552
  if (invalid)
535
553
  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`);
554
+ createObjective = await textOption(args, '--objective', '--objective-file');
555
+ if (!arg(args, '--pr') && !createObjective.trim())
556
+ throw new Error('job create with --head requires --objective-file PATH|- or --objective MARKDOWN containing the complete standalone Job context');
536
557
  }
537
558
  const auth = await githubAuthContext(args);
538
559
  if (action === 'list') {
@@ -549,10 +570,11 @@ async function runJobCommand(action, args) {
549
570
  if (action === 'create') {
550
571
  if (!prNumber && !arg(args, '--head'))
551
572
  throw new Error('job create requires either --head or --pr');
573
+ const objective = createObjective ?? '';
552
574
  const result = await ensureManagedGitHubJob(auth.client, repo, auth.trustedAuthors, {
553
575
  id,
554
576
  title: arg(args, '--title') || undefined,
555
- objective: arg(args, '--objective') || undefined,
577
+ objective: objective || undefined,
556
578
  priority: priority(args),
557
579
  executionMode: requestedExecutionMode(args),
558
580
  schedule: scheduleFromArgs(args),
@@ -562,7 +584,7 @@ async function runJobCommand(action, args) {
562
584
  prNumber,
563
585
  head: arg(args, '--head') || undefined,
564
586
  base: arg(args, '--base', 'main'),
565
- body: arg(args, '--body') || undefined
587
+ body: arg(args, '--body') || objective || undefined
566
588
  });
567
589
  console.log(JSON.stringify({ repository: repo, pr: result.pr.number, job: result.job.id, executionMode: result.job.executionMode, state: result.job.state }, null, 2));
568
590
  return;
@@ -606,8 +628,13 @@ async function runJobCommand(action, args) {
606
628
  }
607
629
  if (hasArg(args, '--title'))
608
630
  job.title = arg(args, '--title');
609
- if (hasArg(args, '--objective'))
610
- job.objective = arg(args, '--objective');
631
+ if (hasArg(args, '--objective') || hasArg(args, '--objective-file')) {
632
+ const objective = await textOption(args, '--objective', '--objective-file');
633
+ if (!objective.trim())
634
+ throw new Error('Job objective cannot be empty');
635
+ job.objective = objective;
636
+ await store.updatePullRequestBody?.(objective);
637
+ }
611
638
  if (hasArg(args, '--priority'))
612
639
  job.priority = priority(args, job.priority ?? 'P2');
613
640
  if (hasArg(args, '--mode') || hasArg(args, '--execution-mode'))
package/dist/markers.js CHANGED
@@ -20,8 +20,30 @@ export function parseMarker(body, label) {
20
20
  }
21
21
  return null;
22
22
  }
23
- export function renderJobComment(job) { return `${marker(JOB_MARKER, { ...job, tasks: undefined })}\n# Agents Relay job: ${job.title}\nState: ${job.state}\nPR: #${job.prNumber}`; }
24
- export function renderTaskComment(task) { return `${marker(TASK_MARKER, task)}\n## Task ${task.id}\nState: ${task.state}\n${task.result?.summary ?? task.input}`; }
23
+ export function renderJobComment(job) {
24
+ const objective = job.objective?.trim() || '_No objective recorded._';
25
+ return `${marker(JOB_MARKER, { ...job, tasks: undefined })}
26
+ # Agents Relay job: ${job.title}
27
+
28
+ - **State:** ${job.state}
29
+ - **PR:** #${job.prNumber}
30
+
31
+ ## Objective
32
+
33
+ ${objective}`;
34
+ }
35
+ export function renderTaskComment(task) {
36
+ const body = task.result?.summary ?? task.input;
37
+ return `${marker(TASK_MARKER, task)}
38
+ ## Task: ${task.id}
39
+
40
+ - **State:** ${task.state}
41
+ - **Priority:** ${task.priority ?? 'P2'}
42
+
43
+ ### Work
44
+
45
+ ${body}`;
46
+ }
25
47
  export function parseJob(body) {
26
48
  const job = parseMarker(body, JOB_MARKER);
27
49
  if (!job)
package/dist/store.js CHANGED
@@ -66,6 +66,7 @@ export class InMemoryStore {
66
66
  }
67
67
  async saveTask(task) { this.values.set(`${TASK_MARKER}:${task.jobId ?? ''}:${task.id}`, renderTaskComment(task)); }
68
68
  async closePullRequest() { }
69
+ async updatePullRequestBody(_body) { }
69
70
  async headSha() { const jobs = [...this.values.values()].map(parseJob).filter((x) => x !== null); return jobs[0]?.headSha ?? jobs[0]?.review?.headSha ?? null; }
70
71
  async listAgents() { return [...this.values.values()].map(parseAgent).filter((x) => x !== null); }
71
72
  async saveAgent(agent) { this.values.set(`${AGENT_MARKER}:${agent.id}`, renderAgentComment(agent)); }
@@ -477,6 +478,9 @@ export class GitHubStore {
477
478
  async closePullRequest() {
478
479
  await this.client.request('repos/' + this.repository + '/pulls/' + this.prNumber, { method: 'PATCH', body: JSON.stringify({ state: 'closed' }) });
479
480
  }
481
+ async updatePullRequestBody(body) {
482
+ await this.client.request('repos/' + this.repository + '/pulls/' + this.prNumber, { method: 'PATCH', body: JSON.stringify({ body }) });
483
+ }
480
484
  async headSha() {
481
485
  const { owner, name } = this.repoParts();
482
486
  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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agents-relay",
3
- "version": "1.0.59",
3
+ "version": "1.0.60",
4
4
  "description": "Durable async agent jobs coordinated through GitHub pull requests",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,5 +1,9 @@
1
1
  # Agents Relay Orchestrator Workflows
2
2
 
3
+ ## Job context capture
4
+
5
+ Before `job create`, the orchestrator converts the relevant discussion into a standalone Markdown objective. Prefer `--objective-file`; if a diagram materially clarifies workflow/state/architecture/dependencies, put Mermaid first. The objective must be sufficient for a fresh orchestrator/agent to complete the Job without access to the original conversation. Creating the Job is the context-compaction boundary: important decisions must be copied into durable Job context before work is delegated.
6
+
3
7
  This contract defines orchestrator-owned exception workflows that sit above normal Agents Relay task/job execution.
4
8
 
5
9
  ## Hotfix workflow
@@ -2,6 +2,19 @@
2
2
 
3
3
  This contract is the shared protocol for every component that creates, schedules, reviews, or executes Agents Relay jobs and tasks.
4
4
 
5
+ ## Job and Task authoring format
6
+
7
+ Job objectives and Task inputs are **Markdown** and are durable execution context, not short labels. When the work has a workflow, state model, architecture, dependency graph, or relationship that is clearer visually, put a Mermaid block first and follow it with Markdown details. Otherwise use structured Markdown text directly.
8
+
9
+ For long-form content, use files instead of shell-quoted arguments:
10
+
11
+ - Job create/update: `--objective-file PATH` (or `--objective-file -` for stdin) is preferred. `--objective` is for short/simple content only. They are mutually exclusive.
12
+ - Task create/update: `--input-file PATH` (or `--input-file -` for stdin) is preferred. `--input` is for short/simple content only. They are mutually exclusive.
13
+
14
+ When creating a new Job from an active discussion, the orchestrator must consolidate **all context needed to finish the Job without the originating thread** into the Job objective before creation. Include, as applicable: goal, relevant background, decisions already made, requirements, constraints, lifecycle/workflow semantics, non-goals, implementation boundaries, acceptance criteria, delivery/review instructions, and any important known issues. Do not create a Job whose objective is only a one-line summary when the discussion contains material implementation context.
15
+
16
+ For a newly created PR (`job create --head`), standalone objective Markdown is required and becomes the default PR body. Updating the Job objective also updates the PR body. The trusted Job marker remains the machine-readable durable state, while the PR body and rendered Job comment expose the same objective to humans and agents.
17
+
5
18
  ## Task producers
6
19
 
7
20
  Any task producer — orchestrator, planner, or nested task-producing agent — must follow this contract.