agents-relay 1.0.50 → 1.0.51

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
@@ -11,11 +11,11 @@ npx agents-relay job create --repo OWNER/REPO --head feat/example --base main \
11
11
  --id job-1 --title "Objective" --body "PR description" --mode fixed
12
12
 
13
13
  # The orchestrator decides the initial batch separately
14
- npx agents-relay submit --repo OWNER/REPO --pr 12 --id job-1 \
14
+ npx agents-relay task create --repo OWNER/REPO --pr 12 --id job-1 \
15
15
  --task-id initial --adapter codex --provider openai --model MODEL --output task-pr \
16
16
  --input "Implement the objective and publish a terminal event."
17
17
 
18
- npx agents-relay submit --repo OWNER/REPO --pr 12 --id job-1 \
18
+ npx agents-relay task create --repo OWNER/REPO --pr 12 --id job-1 \
19
19
  --task-id child --adapter codex --provider openai --model MODEL --output task-pr \
20
20
  --input "Implement the described child task and publish a terminal event."
21
21
 
@@ -33,9 +33,8 @@ npx agents-relay task update --repo OWNER/REPO --pr 12 --id job-1 --task-id chil
33
33
 
34
34
  Jobs default to `fixed`. `job create` persists only the durable job and never
35
35
  creates executable tasks. The top-level orchestrator decides whether the user's
36
- request should start immediately and, if so, submits the first task batch
37
- separately. That submit path records the orchestrator planning decision used as
38
- the provenance root for those tasks. A zero-task autonomous job remains idle;
36
+ request should start immediately and, if so, creates the first task batch
37
+ separately. Direct orchestrator-created tasks are persisted directly and are not wrapped in a synthetic Plan. A zero-task autonomous job remains idle;
39
38
  reconciliation does not invent its first planner. After an autonomous job has
40
39
  been explicitly started, later planning rounds may extend the work. Fixed jobs
41
40
  never grow another planning round.
@@ -55,7 +54,7 @@ A repository heartbeat invokes `tick` at 10-minute resolution. See
55
54
  [Scheduled autonomous jobs](docs/scheduled-autonomous-jobs.md) for the exact
56
55
  operational contract.
57
56
 
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.
57
+ 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 `task create`, and the orchestrator may create zero, one, or multiple initial Tasks according to user intent.
59
58
 
60
59
  To create a Job on an existing unmanaged PR, use the same `job create` action with `--pr` instead of `--head`:
61
60
 
@@ -71,7 +70,7 @@ npx agents-relay job update --repo OWNER/REPO --pr 6 --id job-6 --priority P0 --
71
70
 
72
71
  `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
72
 
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
+ Managed GitHub task create/update/show/list/retry/cancel and reconcile/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.
75
74
 
76
75
  The original init command remains supported for backward compatibility:
77
76
 
@@ -91,7 +90,7 @@ For explicit local demo/test mode:
91
90
  npm install
92
91
  npm run build
93
92
  node dist/cli.js init --file .agents-relay.json --title "Dogfood" --id demo
94
- node dist/cli.js submit --file .agents-relay.json --task-id hello --adapter codex --provider openai --model MODEL --output task-pr --input "Complete the described task"
93
+ node dist/cli.js task create --file .agents-relay.json --task-id hello --adapter codex --provider openai --model MODEL --output task-pr --input "Complete the described task"
95
94
  node dist/cli.js reconcile --file .agents-relay.json
96
95
  npx agents-relay serve --file .agents-relay.json
97
96
  ~~~
@@ -143,7 +142,7 @@ For Codex and ChatGPT, **events are authoritative task state**. Event publicatio
143
142
  The ChatGPT adapter uses the `chatgpt-browser-worker` path selected by the shared execution-context resolver; Relay does not bundle its own copy. The resolver applies project-local > Neo shared > global `~/.agents` precedence at execution time, so durable tasks store names/intent rather than stale absolute skill paths. It opens a fresh Temporary Chat tab, uses the account defaults, submits the prompt, verifies acceptance, and applies `--chatgpt-tab-close-policy` (or `AGENTS_RELAY_CHATGPT_TAB_CLOSE_POLICY`): `after-start` by default, `never` for test/debug, or `after-terminal` after a new exact-task terminal event. It never closes a user tab and does not poll for the assistant response, resume/reopen a thread, or use conversation text as the result channel. Any observed ChatGPT thread ID is diagnostic only.
144
143
 
145
144
  ~~~sh
146
- npx agents-relay submit --repo OWNER/REPO --pr 5 --id job-1 \
145
+ npx agents-relay task create --repo OWNER/REPO --pr 5 --id job-1 \
147
146
  --task-id research-ui --adapter chatgpt \
148
147
  --capabilities model,chatgpt,browser-harness \
149
148
  --provider openai --model MODEL \
@@ -153,4 +152,4 @@ npx agents-relay submit --repo OWNER/REPO --pr 5 --id job-1 \
153
152
 
154
153
  GitHub PR state is authoritative for terminal lifecycle: a merged PR reconciles its managed job to `COMPLETED`; a closed, unmerged PR reconciles to `CANCELLED` and cannot launch queued work. The dashboard exposes `/api/jobs` and shows repository-wide managed PR state beside each durable Agents Relay job state so stale markers are visible instead of being mistaken for current truth.
155
154
 
156
- For work that was completed outside the relay but must be represented truthfully in durable history, use `npx agents-relay record --task-id <id> --summary <summary> --commit <sha>`. Recorded tasks use the `orchestrator` adapter and terminal `SUCCEEDED` state; they do not pretend a shell/model worker executed the work. This is a repair/backfill mechanism—normal work should be submitted before execution. Managed jobs also retain the GitHub PR body as the job description for dashboard display.
155
+ For work that was completed outside the relay but must be represented truthfully in durable history, use `npx agents-relay record --task-id <id> --summary <summary> --commit <sha>`. Recorded tasks use the `orchestrator` adapter and terminal `SUCCEEDED` state; they do not pretend a shell/model worker executed the work. This is a repair/backfill mechanism—normal work should be created as a durable Task before execution. Managed jobs also retain the GitHub PR body as the job description for dashboard display.
package/dist/cli.js CHANGED
@@ -14,12 +14,12 @@ import { NatsEventBus, eventFor } from './events.js';
14
14
  import { serveDashboard } from './dashboard.js';
15
15
  import { codexAndZaiUsageRegistry } from './usage.js';
16
16
  import { discoverAgents } from './registry.js';
17
- import { AgentObjectivePlanner, CommandObjectivePlanner, orchestratorPlanningDecision } from './planner.js';
17
+ import { AgentObjectivePlanner, CommandObjectivePlanner } from './planner.js';
18
18
  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|reconcile|tick|retry|cancel|serve> [options]';
22
+ const usage = 'agents-relay <job|task|context|init|record|agent-register|agent-discover|reconcile|tick|serve> [options]';
23
23
  const help = {
24
24
  root: `Agents Relay coordinates durable asynchronous agent jobs through GitHub pull requests.
25
25
 
@@ -28,16 +28,14 @@ Usage:
28
28
 
29
29
  Commands:
30
30
  job create|update|show|list Manage durable GitHub-backed jobs
31
- task update Update mutable durable task fields in place
31
+ task create|update|show|list|retry|cancel Manage durable tasks
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
34
- submit Queue a task for a managed job
35
34
  record Backfill a completed task into durable history
36
35
  agent-register Register an agent role and its capabilities
37
36
  agent-discover Find registered agents matching hard filters
38
37
  reconcile Run reconciliation once
39
38
  tick Run the indexed autonomous scheduler heartbeat
40
- retry|cancel Change a task state
41
39
  serve Run the dashboard and reconciliation service
42
40
 
43
41
  Run 'npx agents-relay <command> --help' for command options and examples.
@@ -89,21 +87,46 @@ Required: --repo OWNER/REPO.
89
87
 
90
88
  Example:
91
89
  npx agents-relay job list --repo OWNER/REPO`,
92
- task: `Manage a durable task.
90
+ task: `Manage durable tasks.
93
91
 
94
92
  Usage:
95
- npx agents-relay task update [options]
93
+ npx agents-relay task <create|update|show|list|retry|cancel> [options]
96
94
 
97
95
  Actions:
98
- update Update mutable task fields without changing task identity/history`,
99
- 'task update': `Update mutable durable task fields in place.
96
+ create Create one executable Task under an existing Job
97
+ update Update mutable Task fields
98
+ show Show one Task
99
+ list List Tasks for a Job
100
+ retry Retry a failed or blocked Task
101
+ cancel Cancel a Task`,
102
+ 'task create': `Create one executable Task under an existing Job.
103
+
104
+ Required: normal Job storage options plus --input TEXT and --output task-pr|file. Codex/ChatGPT tasks also require --model MODEL.
105
+ Common: --task-id ID, --adapter codex|chatgpt|orchestrator, --priority P0|P1|P2|P3, --story-size S|M|L, --deps a,b, --parent ID.
106
+ Directly-created Tasks are not wrapped in a synthetic Plan. Planner-generated Tasks are created internally by Relay from planner output.
100
107
 
101
- Required: --task-id ID and the normal job storage options.
108
+ Example:
109
+ 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."`,
110
+ 'task update': `Update mutable durable Task fields in place.
111
+
112
+ Required: --task-id ID and the normal Job storage options.
102
113
  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.
103
- Use --clear-routing to remove routing metadata. A model update requires --model; provider defaults to the existing provider or openai.
114
+ Use --clear-routing to remove routing metadata.
104
115
 
105
116
  Example:
106
117
  npx agents-relay task update --repo OWNER/REPO --pr 12 --id job-1 --task-id build --adapter chatgpt --provider chatgpt --model gpt-5.6-sol --reasoning high`,
118
+ 'task show': `Show one durable Task.
119
+
120
+ Required: --task-id ID and the normal Job storage options.`,
121
+ 'task list': `List durable Tasks for one Job.
122
+
123
+ Required: the normal Job storage options.`,
124
+ 'task retry': `Retry one durable Task.
125
+
126
+ Required: --task-id ID and the normal Job storage options.`,
127
+ 'task cancel': `Cancel one durable Task.
128
+
129
+ Required: --task-id ID and the normal Job storage options.`,
107
130
  context: `Resolve the shared execution context used by XChat and Relay.
108
131
 
109
132
  Usage:
@@ -119,13 +142,6 @@ Root overrides for testing/configuration: --workspace-root PATH, --neo-root PATH
119
142
 
120
143
  Example:
121
144
  npx agents-relay context resolve --project-path ~/Workspace/agents-relay --role orchestrator`,
122
- submit: `Queue a task for a managed job.
123
-
124
- Required: --repo OWNER/REPO, --pr NUMBER, --id JOB_ID, and --input TEXT.
125
- 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.
126
-
127
- Example:
128
- 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."`,
129
145
  record: `Backfill work completed outside the relay into durable history.
130
146
 
131
147
  Required: --file PATH or --repo/--pr/--id, plus --summary TEXT.
@@ -173,18 +189,6 @@ Common: --capabilities a,b, --runtime NAME, --availability available,busy, --tas
173
189
 
174
190
  Example:
175
191
  npx agents-relay agent-discover --file .agents-relay.json --capabilities shell`,
176
- retry: `Retry a task by setting it READY.
177
-
178
- Required: --task-id ID and the normal job storage options.
179
-
180
- Example:
181
- npx agents-relay retry --repo OWNER/REPO --pr 12 --id job-1 --task-id build`,
182
- cancel: `Cancel a task.
183
-
184
- Required: --task-id ID and the normal job storage options.
185
-
186
- Example:
187
- npx agents-relay cancel --repo OWNER/REPO --pr 12 --id job-1 --task-id build`
188
192
  };
189
193
  export function helpText(command, action) {
190
194
  if (command === 'job')
@@ -316,19 +320,6 @@ export async function ensureManagedGitHubJob(client, repositoryName, trustedAuth
316
320
  await syncScheduleLabels(client, repositoryName, job);
317
321
  return { job: await store.load(options.id), pr };
318
322
  }
319
- async function appendPlannedTask(store, job, task) {
320
- const existing = job.tasks.find(item => item.id === task.id);
321
- if (existing)
322
- return existing;
323
- const decisionId = `plan-${task.id}`;
324
- const decision = orchestratorPlanningDecision(job, [task], decisionId, task.parentTaskId, job.tasks.filter(item => item.kind === 'planner').length, 'orchestrator');
325
- if (!job.tasks.some(item => item.id === decision.id))
326
- await store.appendTask(decision);
327
- const planned = { ...task, parentTaskId: decision.id, planningDecisionId: decision.id, planningSource: 'orchestrator', planningRound: decision.planningRound };
328
- await store.appendTask(planned);
329
- job.tasks.push(planned, decision);
330
- return planned;
331
- }
332
323
  function workerTaskFromArgs(job, args, now) {
333
324
  const model = arg(args, '--model');
334
325
  const adapterValue = arg(args, '--adapter');
@@ -336,12 +327,14 @@ function workerTaskFromArgs(job, args, now) {
336
327
  const outputKind = arg(args, '--output');
337
328
  const outputPath = arg(args, '--output-path');
338
329
  const input = arg(args, '--input');
330
+ if (!input)
331
+ throw new Error('task create requires --input');
339
332
  if (!['codex', 'chatgpt', 'orchestrator'].includes(adapter))
340
- throw new Error('submit adapter must be codex, chatgpt, or orchestrator');
333
+ throw new Error('task create --adapter must be codex, chatgpt, or orchestrator');
341
334
  if (adapter !== 'orchestrator' && !model)
342
- throw new Error('submit requires --model for codex/chatgpt tasks');
335
+ throw new Error('task create requires --model for codex/chatgpt tasks');
343
336
  if (outputKind !== 'task-pr' && outputKind !== 'file')
344
- throw new Error('submit requires --output task-pr|file');
337
+ throw new Error('task create requires --output task-pr|file');
345
338
  if (outputKind === 'file' && !outputPath)
346
339
  throw new Error('--output file requires --output-path');
347
340
  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 };
@@ -381,15 +374,57 @@ function workerTaskFromArgs(job, args, now) {
381
374
  };
382
375
  }
383
376
  async function runTaskCommand(action, args) {
384
- if (action !== 'update')
377
+ if (!['create', 'update', 'show', 'list', 'retry', 'cancel'].includes(action))
385
378
  throw new Error(usage);
386
379
  const { store, job, localFile } = await storeFor(args);
387
- const id = arg(args, '--task-id');
380
+ if (action === 'list') {
381
+ console.log(JSON.stringify(job.tasks, null, 2));
382
+ return;
383
+ }
384
+ const id = arg(args, '--task-id', action === 'create' ? randomUUID() : '');
388
385
  if (!id)
389
- throw new Error('task update requires --task-id');
386
+ throw new Error(`task ${action} requires --task-id`);
387
+ if (action === 'create') {
388
+ const now = new Date().toISOString();
389
+ const task = workerTaskFromArgs(job, args, now);
390
+ task.id = id;
391
+ await store.appendTask(task);
392
+ const refreshed = await store.load(job.id);
393
+ if (refreshed.state !== 'RUNNING') {
394
+ refreshed.state = 'RUNNING';
395
+ refreshed.updatedAt = now;
396
+ await store.saveJob(refreshed);
397
+ }
398
+ const durableTask = (await store.load(job.id)).tasks.find(item => item.id === task.id) ?? task;
399
+ await saveLocal(localFile, await store.load(job.id));
400
+ await publishWake(eventBus(args), refreshed, task.id, `Task ${task.id} created`);
401
+ console.log(JSON.stringify(durableTask, null, 2));
402
+ return;
403
+ }
390
404
  const task = job.tasks.find(item => item.id === id);
391
405
  if (!task)
392
406
  throw new Error(`Task ${id} not found`);
407
+ if (action === 'show') {
408
+ console.log(JSON.stringify(task, null, 2));
409
+ return;
410
+ }
411
+ if (action === 'retry' || action === 'cancel') {
412
+ if (action === 'retry')
413
+ task.maxAttempts = extendRetryBudget(task);
414
+ task.state = action === 'retry' ? 'READY' : 'CANCELLED';
415
+ task.error = action === 'retry' ? null : 'Cancelled by user';
416
+ task.updatedAt = new Date().toISOString();
417
+ await store.saveTask(task);
418
+ if (action === 'retry' && job.state !== 'RUNNING') {
419
+ job.state = 'RUNNING';
420
+ job.updatedAt = task.updatedAt;
421
+ await store.saveJob(job);
422
+ }
423
+ await saveLocal(localFile, job);
424
+ await publishWake(eventBus(args), job, task.id, `${task.id} ${action} requested`);
425
+ console.log(JSON.stringify(task, null, 2));
426
+ return;
427
+ }
393
428
  if (['RUNNING', 'SUCCEEDED', 'CANCELLED'].includes(task.state))
394
429
  throw new Error(`Task ${id} is ${task.state}; durable execution fields are immutable in this state`);
395
430
  if (hasArg(args, '--adapter')) {
@@ -438,8 +473,7 @@ async function runTaskCommand(action, args) {
438
473
  reasoning: hasArg(args, '--reasoning') ? arg(args, '--reasoning') || undefined : current?.reasoning,
439
474
  cwd: hasArg(args, '--cwd') ? arg(args, '--cwd') || undefined : current?.cwd,
440
475
  projectId: hasArg(args, '--chatgpt-project') ? arg(args, '--chatgpt-project') || undefined : current?.projectId,
441
- decidedBy: 'cli:update',
442
- decidedAt: new Date().toISOString()
476
+ decidedBy: 'cli:update', decidedAt: new Date().toISOString()
443
477
  };
444
478
  if (hasArg(args, '--reasoning') && !hasArg(args, '--timeout'))
445
479
  task.timeoutMs = taskOverallTimeoutMs(task.storySize ?? 'M', task.routing.reasoning);
@@ -714,51 +748,16 @@ async function main() {
714
748
  throw new Error('record requires --summary');
715
749
  const commit = arg(args, '--commit');
716
750
  const task = { jobId: job.id, id, projectName: arg(args, '--project') || undefined, agentName: arg(args, '--agent', 'orchestrator'), parentTaskId: arg(args, '--parent') || null, dependencies: arg(args, '--deps').split(',').filter(Boolean), capabilities: [], adapter: 'orchestrator', input: arg(args, '--input', summary), continuation: null, continuationDeliveredAt: null, state: 'SUCCEEDED', attempt: 1, maxAttempts: 1, leaseOwner: null, leaseExpiresAt: null, executionId: commit || null, threadId: null, result: { summary, data: commit ? { commit } : undefined }, error: null, timeoutMs: 0, createdAt: now, updatedAt: now };
717
- await appendPlannedTask(store, job, task);
751
+ await store.appendTask(task);
752
+ job.tasks.push(task);
718
753
  await saveLocal(localFile, job);
719
754
  console.log(`Recorded ${id}`);
720
755
  return;
721
756
  }
722
- if (command === 'submit') {
723
- const now = new Date().toISOString();
724
- const task = workerTaskFromArgs(job, args, now);
725
- task.id = arg(args, '--task-id', randomUUID());
726
- const planned = await appendPlannedTask(store, job, task);
727
- if (job.state !== 'RUNNING') {
728
- job.state = 'RUNNING';
729
- job.updatedAt = now;
730
- await store.saveJob(job);
731
- }
732
- await saveLocal(localFile, job);
733
- await publishWake(bus, job, planned.id, `Planning decision recorded and task ${planned.id} submitted`);
734
- console.log(`Submitted ${planned.id}`);
735
- return;
736
- }
737
757
  if (command === 'status') {
738
758
  console.log(JSON.stringify(await store.load(job.id), null, 2));
739
759
  return;
740
760
  }
741
- if (command === 'retry' || command === 'cancel') {
742
- const id = arg(args, '--task-id');
743
- const task = job.tasks.find(item => item.id === id);
744
- if (!task)
745
- throw new Error(`Task ${id} not found`);
746
- if (command === 'retry')
747
- task.maxAttempts = extendRetryBudget(task);
748
- task.state = command === 'retry' ? 'READY' : 'CANCELLED';
749
- task.error = command === 'retry' ? null : 'Cancelled by user';
750
- task.updatedAt = new Date().toISOString();
751
- await store.saveTask(task);
752
- if (command === 'retry' && job.state !== 'RUNNING') {
753
- job.state = 'RUNNING';
754
- job.updatedAt = task.updatedAt;
755
- await store.saveJob(job);
756
- }
757
- await saveLocal(localFile, job);
758
- await publishWake(bus, job, task.id, `${task.id} ${command} requested`);
759
- console.log(`${id}: ${task.state}`);
760
- return;
761
- }
762
761
  const reconciler = runtime(store, args, bus);
763
762
  if (command === 'reconcile') {
764
763
  await reconciler.reconcile(job.id);
package/dist/planner.js CHANGED
@@ -11,10 +11,6 @@ export function planningSpec(task) {
11
11
  throw new Error(`Initial task ${task.id} requires a supported adapter`);
12
12
  return { id: task.id, input: task.input, priority: task.priority, storySize: task.storySize, projectName: task.projectName, agentName: task.agentName, dependencies: task.dependencies, capabilities: task.capabilities, workerRequirements: task.workerRequirements, adapter: task.adapter, output: task.output ?? { kind: 'task_pr' }, routing: task.routing, continuation: task.continuation, maxAttempts: task.maxAttempts, timeoutMs: task.timeoutMs };
13
13
  }
14
- export function orchestratorPlanningDecision(job, tasks, id, parentTaskId, round, source = 'orchestrator') {
15
- const now = new Date().toISOString();
16
- return { jobId: job.id, id, kind: 'planner', priority: job.priority, storySize: 'S', agentName: source === 'orchestrator' ? 'orchestrator' : 'planner', parentTaskId, dependencies: [], capabilities: ['planner'], adapter: 'orchestrator', input: `Planning decision supplied by ${source} for ${tasks.length} task(s)`, state: 'SUCCEEDED', attempt: 1, maxAttempts: 1, leaseOwner: null, leaseExpiresAt: null, executionId: null, threadId: null, result: { summary: source === 'orchestrator' ? 'Initial task set supplied by orchestrator' : 'Planning decision completed' }, plannerResult: { objective_status: 'in_progress', assessment: source === 'orchestrator' ? 'Initial task set supplied by orchestrator' : 'Planning decision completed', next_tasks: tasks.map(planningSpec) }, error: null, timeoutMs: 0, createdAt: now, updatedAt: now, planningDecisionId: id, planningSource: source, planningRound: round };
17
- }
18
14
  function record(value) {
19
15
  if (!value || typeof value !== 'object' || Array.isArray(value))
20
16
  throw new Error('Planner result must be an object');
package/dist/pool.js CHANGED
@@ -4,7 +4,7 @@ const RANK = { P0: 0, P1: 1, P2: 2, P3: 3 };
4
4
  export function compareJobPriority(a, b) { return RANK[a.priority ?? 'P2'] - RANK[b.priority ?? 'P2'] || a.createdAt.localeCompare(b.createdAt) || a.id.localeCompare(b.id); }
5
5
  export function runnableManagedJobs(items) { return items.filter(item => item.githubState === 'OPEN' && !item.draft && !['COMPLETED', 'CANCELLED'].includes(item.job.state)).map(item => item.job).sort(compareJobPriority); }
6
6
  export function needsLifecycleReconciliation(job, now = Date.now()) {
7
- // Create-only jobs stay OPEN until a caller explicitly submits work.
7
+ // Create-only Jobs stay OPEN until a caller explicitly creates work.
8
8
  if (job.tasks.length === 0)
9
9
  return false;
10
10
  if (job.tasks.some(task => ['READY', 'QUEUED', 'WAITING'].includes(task.state)))
@@ -386,7 +386,7 @@ export class Reconciler {
386
386
  if (job.initialPlanning?.status === 'pending')
387
387
  return job;
388
388
  const allPlanners = job.tasks.filter(task => task.kind === 'planner').sort((a, b) => a.createdAt.localeCompare(b.createdAt) || a.id.localeCompare(b.id));
389
- if (allPlanners.length === 0)
389
+ if (allPlanners.length === 0 && job.tasks.length === 0)
390
390
  return job;
391
391
  const slot = job.schedule?.currentRunSlot ? Date.parse(job.schedule.currentRunSlot) : Number.NaN;
392
392
  const scopedTasks = Number.isFinite(slot) ? job.tasks.filter(task => Date.parse(task.createdAt) >= slot) : job.tasks;
@@ -395,7 +395,7 @@ export class Reconciler {
395
395
  return job;
396
396
  if (scopedTasks.some(task => task.state === 'FAILED' || task.state === 'BLOCKED'))
397
397
  return job;
398
- if (scopedTasks.length > 0 && !planners.some(task => task.state === 'SUCCEEDED'))
398
+ if (scopedTasks.length > 0 && planners.length > 0 && !planners.some(task => task.state === 'SUCCEEDED'))
399
399
  return job;
400
400
  if (scopedTasks.some(task => task.kind !== 'planner' && !['SUCCEEDED', 'CANCELLED'].includes(task.state)))
401
401
  return job;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agents-relay",
3
- "version": "1.0.50",
3
+ "version": "1.0.51",
4
4
  "description": "Durable async agent jobs coordinated through GitHub pull requests",
5
5
  "type": "module",
6
6
  "bin": {
@@ -62,7 +62,7 @@ Example:
62
62
  ~~~sh
63
63
  npx agents-relay job create --repo OWNER/REPO --head feat/example --base main --id JOB_ID --title "Objective"
64
64
 
65
- npx agents-relay submit --repo OWNER/REPO --pr 12 --id JOB_ID --task-id implement --adapter codex --provider openai --model MODEL --output task-pr --input "Implement the requested change, validate it, and update the managed task/PR."
65
+ npx agents-relay task create --repo OWNER/REPO --pr 12 --id JOB_ID --task-id implement --adapter codex --provider openai --model MODEL --output task-pr --input "Implement the requested change, validate it, and update the managed task/PR."
66
66
 
67
67
  npx agents-relayd --repo OWNER/REPO --events nats
68
68
  ~~~
@@ -96,17 +96,15 @@ ChatGPT thread identity is diagnostic only. It is not persisted as a resumable t
96
96
 
97
97
  ## Autonomous planner
98
98
 
99
- No executable task may be materialized without a completed durable planning
100
- decision. Job creation with caller-supplied initial work records an
101
- orchestrator-owned `planner-0` decision before its initial tasks; autonomous
102
- mode must not start a model planner in that case. Objective-only autonomous
103
- jobs may start a real model planner, and planner children are created only
104
- after that planner succeeds. Fixed jobs do not automatically create another
105
- planning round. Each task exposes `planningDecisionId`, `planningSource`, and
106
- `planningRound` for dashboard lineage. Nested worker delegation must create its
107
- durable planning decision and children through Relay as well.
108
-
109
- Planner-created child tasks must also be descriptive agent work. They must select `codex` or `chatgpt` and include an explicit output contract. A planner must never generate shell-command worker tasks.
99
+ Direct orchestrator-created Tasks do not require a Plan. The orchestrator may create the initial batch with `task create`; those Tasks remain top-level unless the orchestrator explicitly gives them normal task parentage.
100
+
101
+ For autonomous continuation, Relay invokes the planner agent as a pure decision function. The planner receives durable Job/Task state, previous planner rounds, and resolved execution context, then returns structured `{ objective_status, assessment, next_tasks }` output. The planner does not call Relay CLI and does not persist Tasks itself.
102
+
103
+ Each successful planner invocation is one real planner round. Relay persists that planner result, then creates the returned Tasks with lineage back to that planner round. Only planner execution creates planner rounds; Relay never manufactures synthetic `plan-*` records for direct Task creation. In the dashboard, planner rounds are organizational/grouping nodes for the Tasks they produced.
104
+
105
+ A zero-task autonomous Job remains idle. After a direct orchestrator-created initial batch completes, Relay may start the first real planner round. Fixed Jobs do not automatically create planner rounds.
106
+
107
+ Planner-created child Tasks remain descriptive agent work and must include worker requirements, output contract, and `storySize`; worker-router/model-router choose execution routing afterward.
110
108
 
111
109
  ## Durable truth
112
110
 
@@ -4,22 +4,30 @@ You are the default objective planner for Agents Relay autonomous jobs.
4
4
 
5
5
  Before producing tasks, read and follow `../contracts/task-job-contract.md`.
6
6
 
7
- Inspect the durable objective and task tree, then decide whether the objective is satisfied or which descriptive agent tasks should run next.
7
+ You are a pure planning agent. Relay invokes you with the durable Job, existing Tasks, previous planner rounds, and resolved project context. You return one structured planning result. You do not call the Agents Relay CLI, persist state, create Jobs, or mutate Tasks yourself.
8
+
9
+ Relay persists each successful planner invocation as one planner round and creates the returned Tasks under that round.
8
10
 
9
11
  ## Rules
10
12
 
11
- - Treat durable job/task state as authoritative.
13
+ - Treat durable Job/Task state as authoritative.
12
14
  - Never invent completed work or evidence.
13
- - Prefer small independently verifiable tasks.
14
- - Preserve parent/subtask causality and stable IDs.
15
- - A child task describes what an agent must accomplish. Never emit a shell command as a worker task.
16
- - Every child task must declare `workerRequirements` (capabilities plus any runtime, task-kind, trust-domain, region, or label constraints). The worker-router resolves those requirements against the fresh usage snapshot and available workers before model-router chooses provider/model details.
17
- - Do not choose `codex`/`chatgpt` or provider/model heuristically. When a selected worker is supplied by the caller, consume that selection; otherwise leave worker selection to the worker-router.
18
- - Every child task must declare exactly one output: `task_pr` or `file`.
19
- - Do not emit provider/model routing metadata; worker-router and model-router add it after planning.
20
- - Every child task must include `storySize` according to the shared Task / Job Contract. Use `M` when uncertain.
15
+ - First decide whether the objective is already satisfied.
16
+ - If satisfied, return `objective_status: "satisfied"` and `next_tasks: []`.
17
+ - Otherwise return only the next useful executable batch.
18
+ - Do not repeat completed work or recreate an existing Task unless replacement work is explicitly needed.
19
+ - Prefer small independently verifiable Tasks with stable IDs.
20
+ - A Task describes what an agent must accomplish; never emit a shell command as a worker Task.
21
+ - Use dependencies only when execution order actually matters.
22
+ - Every child Task must declare `workerRequirements`; worker-router selects the worker and model-router selects provider/model/reasoning afterward.
23
+ - Do not choose `codex`/`chatgpt`, provider/model, or usage probes.
24
+ - Every child Task must declare exactly one output: `task_pr` or `file`.
25
+ - Every child Task must include `storySize` (`S`, `M`, or `L`); use `M` when uncertain.
26
+ - Do not create bookkeeping, waiting, status-checking, wrapper, or planning Tasks. Your response itself is the planning round.
21
27
  - Return no prose outside the JSON result.
22
28
 
23
29
  ## Output
24
30
 
25
- Return exactly one JSON object with `objective_status`, `assessment`, and `next_tasks`. Each next task contains id, input, `workerRequirements`, output, and storySize. Adapter/routing fields are selected by the runtime after worker-router runs; do not invent them. Each task may also contain priority, projectName, agentName, dependencies, capabilities, maxAttempts, and timeoutMs. When the objective is satisfied, next_tasks must be empty.
31
+ Return exactly one JSON object with `objective_status`, `assessment`, and `next_tasks`.
32
+
33
+ Each item in `next_tasks` contains `id`, `input`, `workerRequirements`, `output`, and `storySize`. It may also contain priority, projectName, agentName, dependencies, capabilities, maxAttempts, and timeoutMs. Adapter/routing fields are selected by Relay after planning; do not invent them.
@@ -20,7 +20,7 @@ A **P0 job** (alias: **self job**) is executed directly by the current active or
20
20
  1. The orchestrator creates the durable job.
21
21
  2. The orchestrator immediately creates the required orchestrator-owned task(s); do not leave the P0 job idle.
22
22
  3. The current orchestrator thread starts executing those tasks immediately rather than delegating them to another worker.
23
- 4. After implementation, the orchestrator performs/reconciles review against the current head.
24
- 5. If review passes and merge requirements are satisfied, the orchestrator merges the job itself and completes normal post-merge reconciliation.
23
+ 4. P0/self jobs do not require the normal reviewer gate unless the user explicitly asks for review.
24
+ 5. After implementation and local verification, the orchestrator merges the job itself and completes normal post-merge reconciliation.
25
25
 
26
26
  `job create` still only creates durable job state; the P0 workflow requires the orchestrator to follow it immediately with task creation and execution.
@@ -40,10 +40,19 @@ A normal task never merges a PR. After all non-superseded work gates pass, Relay
40
40
 
41
41
  Final-review routing follows the same worker-selection boundary as executable work. Capability, availability, explicit reviewer constraints, and resource health are hard gates. Among eligible workers, Relay prefers a reviewer with a different worker identity from the implementation tasks; when that is unavailable it prefers a different provider/model, then falls back to the first eligible worker. The selected worker, `selectedBy` reason, and `reviewerRouting` event data are durable evidence. No provider is a universal reviewer default.
42
42
 
43
- ## Fixed and autonomous jobs
43
+ ## Job, Task, and planner ownership
44
44
 
45
- `job create` persists only the durable job. It never creates executable work. The top-level orchestrator owns the initial execution decision and, when the user intends the job to start, submits the first task batch separately. A zero-task autonomous job stays idle and reconciliation must not bootstrap its first planner.
45
+ The public CLI resource surface is:
46
46
 
47
- `fixed` jobs execute the orchestrator-provided task set and then proceed to final review. Once an autonomous job has been explicitly started by an orchestrator-provided planning batch, it may ask the planner for additional task batches. Planner-produced tasks must obey this same contract, including explicit `storySize`.
47
+ - `job create|update|show|list`
48
+ - `task create|update|show|list|retry|cancel`
49
+
50
+ `job create` persists only durable Job state. It never creates executable work. `task create` creates a real executable Task directly under the Job; direct orchestrator-created Tasks are not wrapped in synthetic planner Tasks and have no planner-round provenance.
51
+
52
+ Planner rounds are internal planner output, not a general public CLI resource. Relay invokes the planner agent, validates its structured result, persists that invocation as one planner round, then materializes `next_tasks` under that planner round. The planner agent itself does not call Relay CLI or persist Tasks. Only an actual planner invocation creates a planner round.
53
+
54
+ A zero-task autonomous Job stays idle. An autonomous Job may begin with one or more direct orchestrator-created Tasks and no Plan. After that direct batch completes, Relay may create the first real planner round. Later planner rounds continue until the objective is satisfied. Fixed Jobs never create autonomous planner rounds.
55
+
56
+ Planner-produced Tasks obey this same contract, including explicit `storySize`.
48
57
 
49
58
  A scheduled autonomous job adds cron/timezone/next-run and missed-run/concurrency policy to the trusted job marker, plus the `agents-relay:autonomous` label and one bounded time-of-day `agents-relay:next-run:*` index label. The GitHub heartbeat discovers only indexed due candidates, validates the marker's exact `nextRunAt`, claims the slot idempotently, advances the durable next run, and pulses `agents-relay:schedule-wake`. GitHub webhook delivery then wakes local Relay for planner/worker execution. While the schedule is active, each satisfied run returns the job to `WAITING` instead of entering final-review/auto-merge; older-cycle failures are historical evidence and do not block a later slot.