agents-relay 1.0.49 → 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,16 +11,16 @@ 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
 
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:
@@ -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,21 +54,23 @@ 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
- To adopt an existing unmanaged PR, including a PR with no comments:
59
+ To create a Job on an existing unmanaged PR, use the same `job create` action with `--pr` instead of `--head`:
61
60
 
62
61
  ~~~sh
63
- npx agents-relay job adopt --repo OWNER/REPO --pr 6 --id job-6 --title "Existing objective"
62
+ npx agents-relay job create --repo OWNER/REPO --pr 6 --id job-6 --title "Existing objective"
64
63
  ~~~
65
64
 
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.
65
+ Use `job update` for partial mutable metadata changes on an already healthy Job marker/binding:
67
66
 
68
67
  ~~~sh
69
- npx agents-relay job repair --repo OWNER/REPO --pr 6 --id job-6
68
+ npx agents-relay job update --repo OWNER/REPO --pr 6 --id job-6 --priority P0 --no-auto-merge
70
69
  ~~~
71
70
 
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.
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.
72
+
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.
73
74
 
74
75
  The original init command remains supported for backward compatibility:
75
76
 
@@ -89,7 +90,7 @@ For explicit local demo/test mode:
89
90
  npm install
90
91
  npm run build
91
92
  node dist/cli.js init --file .agents-relay.json --title "Dogfood" --id demo
92
- 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"
93
94
  node dist/cli.js reconcile --file .agents-relay.json
94
95
  npx agents-relay serve --file .agents-relay.json
95
96
  ~~~
@@ -141,7 +142,7 @@ For Codex and ChatGPT, **events are authoritative task state**. Event publicatio
141
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.
142
143
 
143
144
  ~~~sh
144
- 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 \
145
146
  --task-id research-ui --adapter chatgpt \
146
147
  --capabilities model,chatgpt,browser-harness \
147
148
  --provider openai --model MODEL \
@@ -151,4 +152,4 @@ npx agents-relay submit --repo OWNER/REPO --pr 5 --id job-1 \
151
152
 
152
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.
153
154
 
154
- 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|status|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
 
@@ -27,83 +27,106 @@ 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
31
- task update Update mutable durable task fields in place
30
+ job create|update|show|list Manage durable GitHub-backed jobs
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
- status Show the durable job state
39
37
  reconcile Run reconciliation once
40
38
  tick Run the indexed autonomous scheduler heartbeat
41
- retry|cancel Change a task state
42
39
  serve Run the dashboard and reconciliation service
43
40
 
44
41
  Run 'npx agents-relay <command> --help' for command options and examples.
45
42
  Use 'node dist/cli.js' only for explicit local development/demo mode.`,
46
- job: `Manage a GitHub-backed durable job.
43
+ job: `Manage GitHub-backed durable jobs.
47
44
 
48
45
  Usage:
49
- npx agents-relay job <create|adopt|repair> [options]
46
+ npx agents-relay job <create|update|show|list> [options]
50
47
 
51
48
  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
49
+ create Create a durable Job, optionally binding an existing unmanaged PR
50
+ update Partially update mutable metadata on a healthy existing Job
51
+ show Show one durable Job
52
+ list List managed Jobs in a repository
55
53
 
56
54
  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
55
+ --repo OWNER/REPO GitHub repository
56
+ --id JOB_ID Durable job identifier
57
+ --pr NUMBER Existing PR number
68
58
 
69
59
  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.
60
+ npx agents-relay job show --repo OWNER/REPO --pr 12 --id job-1`,
61
+ 'job create': `Create a durable Job and its trusted marker.
72
62
 
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.
63
+ Required: --repo OWNER/REPO, --id JOB_ID, and either --head BRANCH or --pr NUMBER.
64
+ With --head, reuse/create the PR for that head. With --pr, bind an existing unmanaged PR.
65
+ Common: --base BRANCH (default: main), --title TEXT, --body TEXT, --objective TEXT, --priority P0|P1|P2|P3, --mode fixed|autonomous, --no-auto-merge, --no-review.
66
+ 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
67
 
77
68
  Example:
78
69
  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.
70
+ 'job update': `Partially update mutable metadata on a healthy existing Job.
80
71
 
81
72
  Required: --repo OWNER/REPO, --pr NUMBER, and --id JOB_ID.
82
- Common: --title TEXT, --objective TEXT, --mode fixed|autonomous, --no-auto-merge.
73
+ Mutable: --title, --objective, --priority, --mode fixed|autonomous, --schedule, --auto-merge|--no-auto-merge, --review|--no-review, and continuation options.
74
+ Unspecified fields are preserved. Marker/binding corruption is never repaired implicitly; update fails with a precise diagnostic instead.
83
75
 
84
76
  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.
77
+ npx agents-relay job update --repo OWNER/REPO --pr 12 --id job-1 --priority P0 --no-auto-merge`,
78
+ 'job show': `Show one durable Job.
87
79
 
88
80
  Required: --repo OWNER/REPO, --pr NUMBER, and --id JOB_ID.
89
81
 
90
82
  Example:
91
- npx agents-relay job repair --repo OWNER/REPO --pr 12 --id job-12`,
92
- task: `Manage a durable task.
83
+ npx agents-relay job show --repo OWNER/REPO --pr 12 --id job-1`,
84
+ 'job list': `List managed Jobs in a repository.
85
+
86
+ Required: --repo OWNER/REPO.
87
+
88
+ Example:
89
+ npx agents-relay job list --repo OWNER/REPO`,
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.
107
+
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.
100
111
 
101
- Required: --task-id ID and the normal job storage options.
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.
@@ -133,12 +149,6 @@ Common: --task-id ID, --commit SHA, --project NAME.
133
149
 
134
150
  Example:
135
151
  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
152
  reconcile: `Reconcile a job once, launching ready tasks and persisting results.
143
153
 
144
154
  Required: --file PATH for local demo mode, or --repo OWNER/REPO --pr NUMBER --id JOB_ID.
@@ -179,18 +189,6 @@ Common: --capabilities a,b, --runtime NAME, --availability available,busy, --tas
179
189
 
180
190
  Example:
181
191
  npx agents-relay agent-discover --file .agents-relay.json --capabilities shell`,
182
- retry: `Retry a task by setting it READY.
183
-
184
- Required: --task-id ID and the normal job storage options.
185
-
186
- Example:
187
- npx agents-relay retry --repo OWNER/REPO --pr 12 --id job-1 --task-id build`,
188
- cancel: `Cancel a task.
189
-
190
- Required: --task-id ID and the normal job storage options.
191
-
192
- Example:
193
- npx agents-relay cancel --repo OWNER/REPO --pr 12 --id job-1 --task-id build`
194
192
  };
195
193
  export function helpText(command, action) {
196
194
  if (command === 'job')
@@ -287,52 +285,41 @@ export async function ensureManagedGitHubJob(client, repositoryName, trustedAuth
287
285
  throw new Error('--id is required for managed GitHub jobs');
288
286
  const pr = await resolvePullRequest(client, repositoryName, { prNumber: options.prNumber, head: options.head, base: options.base, title: options.title, body: options.body });
289
287
  const store = new GitHubStore(client, repositoryName, pr.number, trustedAuthors);
290
- let job;
291
288
  try {
292
- job = await store.load(options.id);
289
+ const existing = await store.load(options.id);
290
+ if (existing.schedule)
291
+ await syncScheduleLabels(client, repositoryName, existing);
292
+ return { job: existing, pr };
293
293
  }
294
294
  catch (error) {
295
- if (!(error instanceof Error) || !error.message.includes('has no durable'))
295
+ if (!(error instanceof Error) || !error.message.startsWith('JOB_MARKER_MISSING:'))
296
296
  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;
297
+ }
298
+ const now = new Date().toISOString();
299
+ const job = {
300
+ id: options.id,
301
+ title: options.title || pr.title || 'Agents Relay job',
302
+ objective: options.objective || pr.body || undefined,
303
+ description: pr.body || undefined,
304
+ priority: options.priority ?? 'P2',
305
+ executionMode: options.executionMode ?? 'fixed',
306
+ autoMerge: options.autoMerge ?? true,
307
+ reviewEnabled: options.reviewEnabled ?? true,
308
+ prNumber: pr.number,
309
+ repository: repositoryName,
310
+ state: 'OPEN',
311
+ continuation: options.continuation ?? null,
312
+ createdAt: now,
313
+ updatedAt: now,
314
+ tasks: []
315
+ };
306
316
  if (options.schedule)
307
317
  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
318
  await store.saveJob(job);
319
319
  if (job.schedule)
320
320
  await syncScheduleLabels(client, repositoryName, job);
321
321
  return { job: await store.load(options.id), pr };
322
322
  }
323
- async function appendPlannedTask(store, job, task) {
324
- const existing = job.tasks.find(item => item.id === task.id);
325
- if (existing)
326
- return existing;
327
- const decisionId = `plan-${task.id}`;
328
- const decision = orchestratorPlanningDecision(job, [task], decisionId, task.parentTaskId, job.tasks.filter(item => item.kind === 'planner').length, 'orchestrator');
329
- if (!job.tasks.some(item => item.id === decision.id))
330
- await store.appendTask(decision);
331
- const planned = { ...task, parentTaskId: decision.id, planningDecisionId: decision.id, planningSource: 'orchestrator', planningRound: decision.planningRound };
332
- await store.appendTask(planned);
333
- job.tasks.push(planned, decision);
334
- return planned;
335
- }
336
323
  function workerTaskFromArgs(job, args, now) {
337
324
  const model = arg(args, '--model');
338
325
  const adapterValue = arg(args, '--adapter');
@@ -340,12 +327,14 @@ function workerTaskFromArgs(job, args, now) {
340
327
  const outputKind = arg(args, '--output');
341
328
  const outputPath = arg(args, '--output-path');
342
329
  const input = arg(args, '--input');
330
+ if (!input)
331
+ throw new Error('task create requires --input');
343
332
  if (!['codex', 'chatgpt', 'orchestrator'].includes(adapter))
344
- throw new Error('submit adapter must be codex, chatgpt, or orchestrator');
333
+ throw new Error('task create --adapter must be codex, chatgpt, or orchestrator');
345
334
  if (adapter !== 'orchestrator' && !model)
346
- throw new Error('submit requires --model for codex/chatgpt tasks');
335
+ throw new Error('task create requires --model for codex/chatgpt tasks');
347
336
  if (outputKind !== 'task-pr' && outputKind !== 'file')
348
- throw new Error('submit requires --output task-pr|file');
337
+ throw new Error('task create requires --output task-pr|file');
349
338
  if (outputKind === 'file' && !outputPath)
350
339
  throw new Error('--output file requires --output-path');
351
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 };
@@ -385,15 +374,57 @@ function workerTaskFromArgs(job, args, now) {
385
374
  };
386
375
  }
387
376
  async function runTaskCommand(action, args) {
388
- if (action !== 'update')
377
+ if (!['create', 'update', 'show', 'list', 'retry', 'cancel'].includes(action))
389
378
  throw new Error(usage);
390
379
  const { store, job, localFile } = await storeFor(args);
391
- 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() : '');
392
385
  if (!id)
393
- 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
+ }
394
404
  const task = job.tasks.find(item => item.id === id);
395
405
  if (!task)
396
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
+ }
397
428
  if (['RUNNING', 'SUCCEEDED', 'CANCELLED'].includes(task.state))
398
429
  throw new Error(`Task ${id} is ${task.state}; durable execution fields are immutable in this state`);
399
430
  if (hasArg(args, '--adapter')) {
@@ -442,8 +473,7 @@ async function runTaskCommand(action, args) {
442
473
  reasoning: hasArg(args, '--reasoning') ? arg(args, '--reasoning') || undefined : current?.reasoning,
443
474
  cwd: hasArg(args, '--cwd') ? arg(args, '--cwd') || undefined : current?.cwd,
444
475
  projectId: hasArg(args, '--chatgpt-project') ? arg(args, '--chatgpt-project') || undefined : current?.projectId,
445
- decidedBy: 'cli:update',
446
- decidedAt: new Date().toISOString()
476
+ decidedBy: 'cli:update', decidedAt: new Date().toISOString()
447
477
  };
448
478
  if (hasArg(args, '--reasoning') && !hasArg(args, '--timeout'))
449
479
  task.timeoutMs = taskOverallTimeoutMs(task.storySize ?? 'M', task.routing.reasoning);
@@ -474,43 +504,90 @@ async function runContextCommand(action, args) {
474
504
  console.log(JSON.stringify(context, null, 2));
475
505
  }
476
506
  async function runJobCommand(action, args) {
477
- if (!['create', 'adopt', 'repair'].includes(action))
507
+ if (!['create', 'update', 'show', 'list'].includes(action))
478
508
  throw new Error(usage);
479
509
  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
510
  if (action === 'create') {
484
511
  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
512
  const invalid = taskOnlyFlags.find(flag => hasArg(args, flag));
486
513
  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`);
514
+ 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
515
  }
489
516
  const auth = await githubAuthContext(args);
490
- const client = auth.client;
491
- const trustedAuthors = auth.trustedAuthors;
517
+ if (action === 'list') {
518
+ console.log(JSON.stringify(await listManagedGitHubJobs(auth.client, repo, auth.trustedAuthors), null, 2));
519
+ return;
520
+ }
521
+ const id = arg(args, '--id');
522
+ if (!id)
523
+ throw new Error('--id is required for managed GitHub jobs');
492
524
  const prValue = arg(args, '--pr');
493
525
  const prNumber = prValue ? Number(prValue) : undefined;
494
526
  if (prValue && (!Number.isInteger(prNumber) || (prNumber ?? 0) <= 0))
495
527
  throw new Error('--pr must be a positive integer');
496
- if ((action === 'adopt' || action === 'repair') && !prNumber)
528
+ if (action === 'create') {
529
+ if (!prNumber && !arg(args, '--head'))
530
+ throw new Error('job create requires either --head or --pr');
531
+ const result = await ensureManagedGitHubJob(auth.client, repo, auth.trustedAuthors, {
532
+ id,
533
+ title: arg(args, '--title') || undefined,
534
+ objective: arg(args, '--objective') || undefined,
535
+ priority: priority(args),
536
+ executionMode: requestedExecutionMode(args),
537
+ schedule: scheduleFromArgs(args),
538
+ autoMerge: !hasArg(args, '--no-auto-merge'),
539
+ reviewEnabled: !hasArg(args, '--no-review'),
540
+ continuation: continuation(args),
541
+ prNumber,
542
+ head: arg(args, '--head') || undefined,
543
+ base: arg(args, '--base', 'main'),
544
+ body: arg(args, '--body') || undefined
545
+ });
546
+ console.log(JSON.stringify({ repository: repo, pr: result.pr.number, job: result.job.id, executionMode: result.job.executionMode, state: result.job.state }, null, 2));
547
+ return;
548
+ }
549
+ if (!prNumber)
497
550
  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));
551
+ const store = new GitHubStore(auth.client, repo, prNumber, auth.trustedAuthors);
552
+ const job = await store.load(id);
553
+ if (action === 'show') {
554
+ console.log(JSON.stringify(job, null, 2));
555
+ return;
556
+ }
557
+ await store.validateJobBinding(id);
558
+ if (hasArg(args, '--title'))
559
+ job.title = arg(args, '--title');
560
+ if (hasArg(args, '--objective'))
561
+ job.objective = arg(args, '--objective');
562
+ if (hasArg(args, '--priority'))
563
+ job.priority = priority(args, job.priority ?? 'P2');
564
+ if (hasArg(args, '--mode') || hasArg(args, '--execution-mode'))
565
+ job.executionMode = executionMode(args, job.executionMode ?? 'fixed');
566
+ if (hasArg(args, '--schedule'))
567
+ job.schedule = scheduleFromArgs(args);
568
+ if (hasArg(args, '--auto-merge') && hasArg(args, '--no-auto-merge'))
569
+ throw new Error('choose only one of --auto-merge or --no-auto-merge');
570
+ if (hasArg(args, '--auto-merge'))
571
+ job.autoMerge = true;
572
+ if (hasArg(args, '--no-auto-merge'))
573
+ job.autoMerge = false;
574
+ if (hasArg(args, '--review') && hasArg(args, '--no-review'))
575
+ throw new Error('choose only one of --review or --no-review');
576
+ if (hasArg(args, '--review'))
577
+ job.reviewEnabled = true;
578
+ if (hasArg(args, '--no-review'))
579
+ job.reviewEnabled = false;
580
+ if (hasArg(args, '--continue-kind') || hasArg(args, '--continue-target')) {
581
+ const next = continuation(args);
582
+ if (!next)
583
+ throw new Error('--continue-kind and --continue-target must be supplied together');
584
+ job.continuation = next;
585
+ }
586
+ job.updatedAt = new Date().toISOString();
587
+ await store.saveJob(job);
588
+ if (hasArg(args, '--schedule') && job.schedule)
589
+ await syncScheduleLabels(auth.client, repo, job);
590
+ console.log(JSON.stringify(await store.load(id), null, 2));
514
591
  }
515
592
  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
593
  export function runtimeReviewer() { return { requirements: { capabilities: ['model'] } }; }
@@ -671,51 +748,16 @@ async function main() {
671
748
  throw new Error('record requires --summary');
672
749
  const commit = arg(args, '--commit');
673
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 };
674
- await appendPlannedTask(store, job, task);
751
+ await store.appendTask(task);
752
+ job.tasks.push(task);
675
753
  await saveLocal(localFile, job);
676
754
  console.log(`Recorded ${id}`);
677
755
  return;
678
756
  }
679
- if (command === 'submit') {
680
- const now = new Date().toISOString();
681
- const task = workerTaskFromArgs(job, args, now);
682
- task.id = arg(args, '--task-id', randomUUID());
683
- const planned = await appendPlannedTask(store, job, task);
684
- if (job.state !== 'RUNNING') {
685
- job.state = 'RUNNING';
686
- job.updatedAt = now;
687
- await store.saveJob(job);
688
- }
689
- await saveLocal(localFile, job);
690
- await publishWake(bus, job, planned.id, `Planning decision recorded and task ${planned.id} submitted`);
691
- console.log(`Submitted ${planned.id}`);
692
- return;
693
- }
694
757
  if (command === 'status') {
695
758
  console.log(JSON.stringify(await store.load(job.id), null, 2));
696
759
  return;
697
760
  }
698
- if (command === 'retry' || command === 'cancel') {
699
- const id = arg(args, '--task-id');
700
- const task = job.tasks.find(item => item.id === id);
701
- if (!task)
702
- throw new Error(`Task ${id} not found`);
703
- if (command === 'retry')
704
- task.maxAttempts = extendRetryBudget(task);
705
- task.state = command === 'retry' ? 'READY' : 'CANCELLED';
706
- task.error = command === 'retry' ? null : 'Cancelled by user';
707
- task.updatedAt = new Date().toISOString();
708
- await store.saveTask(task);
709
- if (command === 'retry' && job.state !== 'RUNNING') {
710
- job.state = 'RUNNING';
711
- job.updatedAt = task.updatedAt;
712
- await store.saveJob(job);
713
- }
714
- await saveLocal(localFile, job);
715
- await publishWake(bus, job, task.id, `${task.id} ${command} requested`);
716
- console.log(`${id}: ${task.state}`);
717
- return;
718
- }
719
761
  const reconciler = runtime(store, args, bus);
720
762
  if (command === 'reconcile') {
721
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/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.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.