agents-relay 2.0.68 → 2.0.70

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.
Files changed (56) hide show
  1. package/README.md +32 -1
  2. package/dist/adapters.js +39 -3
  3. package/dist/blockers.js +17 -4
  4. package/dist/cli-blocker.js +12 -28
  5. package/dist/cli-help.js +11 -8
  6. package/dist/cli-job-create.js +19 -13
  7. package/dist/cli-local-merge-config.js +4 -2
  8. package/dist/cli-projection.js +23 -55
  9. package/dist/cli-review.js +7 -24
  10. package/dist/cli-support.js +59 -4
  11. package/dist/cli-worker-task.js +63 -0
  12. package/dist/cli.js +179 -129
  13. package/dist/dashboard-html.js +15 -12
  14. package/dist/dashboard.js +26 -1
  15. package/dist/e2e-injection.js +76 -0
  16. package/dist/github-binding-order.js +15 -0
  17. package/dist/github-client.js +7 -2
  18. package/dist/github-pr-archive.js +22 -0
  19. package/dist/github-webhook.js +31 -6
  20. package/dist/job-actions.js +29 -0
  21. package/dist/local-authority-store.js +10 -1
  22. package/dist/local-commit-events.js +2 -0
  23. package/dist/local-git-merge.js +17 -6
  24. package/dist/local-job-path.js +5 -0
  25. package/dist/local-job-registry.js +10 -4
  26. package/dist/local-managed-store.js +2 -4
  27. package/dist/local-merge-delivery.js +3 -2
  28. package/dist/local-merge-projection.js +60 -0
  29. package/dist/managed-worker-input.js +13 -6
  30. package/dist/manual-plan.js +29 -7
  31. package/dist/planner-runtime.js +182 -41
  32. package/dist/planner.js +35 -2
  33. package/dist/pool.js +22 -6
  34. package/dist/reconciler-cancellation.js +21 -0
  35. package/dist/reconciler-review.js +74 -18
  36. package/dist/reconciler-runtime.js +69 -34
  37. package/dist/reconciler.js +92 -26
  38. package/dist/relay-store.js +1 -1
  39. package/dist/relayd-control.js +184 -0
  40. package/dist/relayd-dashboard-actions.js +108 -0
  41. package/dist/relayd-utilities.js +107 -0
  42. package/dist/relayd.js +220 -210
  43. package/dist/review-generation-fence.js +12 -0
  44. package/dist/runtime-status-events.js +21 -0
  45. package/dist/runtime-status.js +13 -0
  46. package/dist/service-publication.js +2 -4
  47. package/dist/task-actions.js +59 -1
  48. package/dist/troubleshooter.js +21 -2
  49. package/dist/unblock.js +47 -15
  50. package/dist/worker-rejection.js +11 -0
  51. package/dist/workflow-effect-failures.js +30 -0
  52. package/dist/workspace.js +95 -6
  53. package/package.json +5 -4
  54. package/skills/agents-relay/agents/planner.agent.md +3 -0
  55. package/skills/agents-relay/contracts/orchestrator-workflows.md +4 -7
  56. package/workflow/review-lifecycle.mmd +3 -0
@@ -0,0 +1,63 @@
1
+ import { taskOverallTimeoutMs } from './types.js';
2
+ import { arg, hasArg, textOption, integerArg, chatGptTabClosePolicy, storySize, priority, continuation } from './cli-support.js';
3
+ import { parseAgentGraph } from './agent-graph.js';
4
+ import { createTaskEntity } from './workflow/entity-creation.js';
5
+ export async function workerTaskFromArgs(job, args, now) {
6
+ const model = arg(args, '--model');
7
+ const adapterValue = arg(args, '--adapter');
8
+ const adapter = (adapterValue || 'codex');
9
+ const outputKind = arg(args, '--output');
10
+ const outputPath = arg(args, '--output-path');
11
+ const input = await textOption(args, '--input', '--input-file');
12
+ const agentGraph = hasArg(args, '--agent-graph') || hasArg(args, '--agent-graph-file') ? await textOption(args, '--agent-graph', '--agent-graph-file') : undefined;
13
+ if (agentGraph)
14
+ parseAgentGraph(agentGraph);
15
+ if (!input.trim())
16
+ throw new Error('task create requires --input-file PATH|- or --input MARKDOWN');
17
+ if (!['codex', 'chatgpt', 'orchestrator'].includes(adapter))
18
+ throw new Error('task create --adapter must be codex, chatgpt, or orchestrator');
19
+ if (outputKind !== 'task-pr' && outputKind !== 'file')
20
+ throw new Error('task create requires --output task-pr|file');
21
+ if (outputKind === 'file' && !outputPath)
22
+ throw new Error('--output file requires --output-path');
23
+ if (adapter !== 'orchestrator' && ['--provider', '--profile', '--reasoning', '--cwd', '--chatgpt-project'].some(flag => hasArg(args, flag)) && !model)
24
+ throw new Error('task create explicit routing requires --model');
25
+ const routing = adapter === 'orchestrator' || !model ? 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 };
26
+ const workerRequirements = adapter === 'orchestrator' || routing ? undefined : { capabilities: [], runtime: adapterValue ? adapter : undefined };
27
+ const closePolicy = hasArg(args, '--chatgpt-tab-close-policy') ? chatGptTabClosePolicy(args) : undefined;
28
+ const size = storySize(args);
29
+ const dependencies = arg(args, '--deps').split(',').filter(Boolean);
30
+ const timeoutMs = adapter === 'orchestrator' ? 0 : hasArg(args, '--timeout') ? integerArg(args, '--timeout', taskOverallTimeoutMs(size, routing?.reasoning)) : routing ? taskOverallTimeoutMs(size, routing.reasoning) : 0;
31
+ return createTaskEntity(job, {
32
+ jobId: job.id,
33
+ id: arg(args, '--task-id', 'initial'),
34
+ priority: priority(args, job.priority ?? 'P2'),
35
+ storySize: size,
36
+ projectName: arg(args, '--project') || undefined,
37
+ agentName: arg(args, '--agent') || undefined,
38
+ agentGraph,
39
+ parentTaskId: arg(args, '--parent') || null,
40
+ dependencies,
41
+ capabilities: arg(args, '--capabilities').split(',').filter(Boolean),
42
+ workerRequirements,
43
+ adapter,
44
+ chatgptTabClosePolicy: closePolicy,
45
+ input: input || 'true',
46
+ attachments: arg(args, '--attachments').split(',').filter(Boolean),
47
+ output: outputKind === 'file' ? { kind: 'file', path: outputPath } : { kind: 'task_pr' },
48
+ routing,
49
+ continuation: continuation(args),
50
+ continuationDeliveredAt: null,
51
+ attempt: 0,
52
+ maxAttempts: Number(arg(args, '--max-attempts', '3')),
53
+ leaseOwner: null,
54
+ leaseExpiresAt: null,
55
+ executionId: null,
56
+ threadId: null,
57
+ result: null,
58
+ error: null,
59
+ timeoutMs,
60
+ createdAt: now,
61
+ updatedAt: now
62
+ }, { defer: hasArg(args, '--draft') });
63
+ }
package/dist/cli.js CHANGED
@@ -1,10 +1,7 @@
1
1
  #!/usr/bin/env node
2
- import { homedir } from 'node:os';
3
- import { join } from 'node:path';
4
- import { openLocalManagedStore } from './local-managed-store.js';
5
2
  import { configureLocalGitMerge } from './cli-local-merge-config.js';
6
- import { DisabledGitHubClient, noGitHubMode } from './no-github.js';
7
- import { discoverLocalRelayStoreSnapshots } from './relay-store.js';
3
+ import { noGitHubMode } from './no-github.js';
4
+ import { discoverLocalRelayStoreSnapshots, relayStoreDirectory } from './relay-store.js';
8
5
  import { realpathSync } from 'node:fs';
9
6
  import { once } from 'node:events';
10
7
  import { fileURLToPath } from 'node:url';
@@ -26,83 +23,24 @@ import { applyJobActivateAction, applyJobCancelAction } from './job-actions.js';
26
23
  import { resolveExecutionContext } from './execution-context.js';
27
24
  import { startServiceExternalActionFlusher } from './service-external-actions.js';
28
25
  import { handleServicePublication } from './service-publication.js';
29
- import { defaultWorkspaceRoot, resolveManagedProjectPath, resolveRepositoryProjectPath } from './workspace.js';
26
+ import { defaultWorkspaceRoot, prepareLocalJobExecutionWorktree, resolveManagedProjectPath, resolveRepositoryProjectPath } from './workspace.js';
30
27
  import { runBlockerCommand } from './cli-blocker.js';
31
28
  import { runEscalationCommand } from './cli-escalation.js';
32
29
  import { runProjectionCommand } from './cli-projection.js';
33
30
  import { helpText, usage } from './cli-help.js';
34
31
  import { parseAgentGraph } from './agent-graph.js';
35
- import { runJobUnblockCommand, runTaskUnblockCommand } from './cli-unblock.js';
36
- import { createJobEntity, createTaskEntity } from './workflow/entity-creation.js';
32
+ import { runTaskUnblockCommand } from './cli-unblock.js';
33
+ import { createJobEntity } from './workflow/entity-creation.js';
37
34
  import { runCliEntrypoint } from './cli-shutdown.js';
38
35
  import { shutdownOwnedProcesses } from './owned-process.js';
39
36
  export { helpText } from './cli-help.js';
40
37
  import { runReviewCommand } from './cli-review.js';
41
38
  import { normalizeThreadId, runtimeReviewer } from './reviewer-thread.js';
42
39
  export { runtimeReviewer } from './reviewer-thread.js';
43
- import { SERVICE_DEFAULTS, ServiceEventBus, arg, hasArg, textOption, integerArg, repository, pullRequest, storeFor, saveLocal, eventBus, publishWake, priority, storySize, executionMode, chatGptTabClosePolicy, requestedExecutionMode, continuation, scheduleFromArgs, jobTagFromArgs, requestManagedReconciliation, relayControlJob, relayTaskCommand, withEventBus } from './cli-support.js';
40
+ import { SERVICE_DEFAULTS, ServiceEventBus, arg, hasArg, textOption, integerArg, repository, storeFor, saveLocal, eventBus, publishWake, priority, storySize, executionMode, chatGptTabClosePolicy, requestedExecutionMode, continuation, scheduleFromArgs, jobTagFromArgs, requestManagedReconciliation, relayControlJob, relayControlCommand, relayJobList, relayJobStateCommand, relayTaskCommand, withEventBus } from './cli-support.js';
44
41
  export { SERVICE_DEFAULTS, eventBus, executionMode, chatGptTabClosePolicy } from './cli-support.js';
45
- import { ensureManagedGitHubJob } from './cli-job-create.js';
42
+ import { workerTaskFromArgs } from './cli-worker-task.js';
46
43
  export { ensureManagedGitHubJob } from './cli-job-create.js';
47
- async function workerTaskFromArgs(job, args, now) {
48
- const model = arg(args, '--model');
49
- const adapterValue = arg(args, '--adapter');
50
- const adapter = (adapterValue || 'codex');
51
- const outputKind = arg(args, '--output');
52
- const outputPath = arg(args, '--output-path');
53
- const input = await textOption(args, '--input', '--input-file');
54
- const agentGraph = hasArg(args, '--agent-graph') || hasArg(args, '--agent-graph-file') ? await textOption(args, '--agent-graph', '--agent-graph-file') : undefined;
55
- if (agentGraph)
56
- parseAgentGraph(agentGraph);
57
- if (!input.trim())
58
- throw new Error('task create requires --input-file PATH|- or --input MARKDOWN');
59
- if (!['codex', 'chatgpt', 'orchestrator'].includes(adapter))
60
- throw new Error('task create --adapter must be codex, chatgpt, or orchestrator');
61
- if (outputKind !== 'task-pr' && outputKind !== 'file')
62
- throw new Error('task create requires --output task-pr|file');
63
- if (outputKind === 'file' && !outputPath)
64
- throw new Error('--output file requires --output-path');
65
- if (adapter !== 'orchestrator' && ['--provider', '--profile', '--reasoning', '--cwd', '--chatgpt-project'].some(flag => hasArg(args, flag)) && !model)
66
- throw new Error('task create explicit routing requires --model');
67
- const routing = adapter === 'orchestrator' || !model ? 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 };
68
- const workerRequirements = adapter === 'orchestrator' || routing ? undefined : { capabilities: [], runtime: adapterValue ? adapter : undefined };
69
- const closePolicy = hasArg(args, '--chatgpt-tab-close-policy') ? chatGptTabClosePolicy(args) : undefined;
70
- const size = storySize(args);
71
- const dependencies = arg(args, '--deps').split(',').filter(Boolean);
72
- const timeoutMs = adapter === 'orchestrator' ? 0 : hasArg(args, '--timeout') ? integerArg(args, '--timeout', taskOverallTimeoutMs(size, routing?.reasoning)) : routing ? taskOverallTimeoutMs(size, routing.reasoning) : 0;
73
- return createTaskEntity(job, {
74
- jobId: job.id,
75
- id: arg(args, '--task-id', 'initial'),
76
- priority: priority(args, job.priority ?? 'P2'),
77
- storySize: size,
78
- projectName: arg(args, '--project') || undefined,
79
- agentName: arg(args, '--agent') || undefined,
80
- agentGraph,
81
- parentTaskId: arg(args, '--parent') || null,
82
- dependencies,
83
- capabilities: arg(args, '--capabilities').split(',').filter(Boolean),
84
- workerRequirements,
85
- adapter,
86
- chatgptTabClosePolicy: closePolicy,
87
- input: input || 'true',
88
- attachments: arg(args, '--attachments').split(',').filter(Boolean),
89
- output: outputKind === 'file' ? { kind: 'file', path: outputPath } : { kind: 'task_pr' },
90
- routing,
91
- continuation: continuation(args),
92
- continuationDeliveredAt: null,
93
- attempt: 0,
94
- maxAttempts: Number(arg(args, '--max-attempts', '3')),
95
- leaseOwner: null,
96
- leaseExpiresAt: null,
97
- executionId: null,
98
- threadId: null,
99
- result: null,
100
- error: null,
101
- timeoutMs,
102
- createdAt: now,
103
- updatedAt: now
104
- }, { defer: hasArg(args, '--draft') });
105
- }
106
44
  async function runManagedTaskCommand(action, args) {
107
45
  const jobId = arg(args, '--id');
108
46
  if (!jobId)
@@ -133,16 +71,100 @@ async function runManagedTaskCommand(action, args) {
133
71
  console.log(JSON.stringify(task, null, 2));
134
72
  }
135
73
  async function runTaskCommand(action, args) {
136
- if (!['create', 'update', 'show', 'list', 'start', 'complete', 'retry', 'unblock', 'cancel'].includes(action))
74
+ if (!['create', 'update', 'show', 'list', 'start', 'complete', 'retry', 'recover', 'unblock', 'cancel'].includes(action))
137
75
  throw new Error(usage);
138
- const managed = !noGitHubMode(args) && Boolean(arg(args, '--repo') && arg(args, '--pr') && !arg(args, '--file'));
139
- if (managed && ['create', 'start', 'complete', 'retry', 'cancel'].includes(action)) {
140
- await runManagedTaskCommand(action, args);
141
- return;
76
+ const managed = !arg(args, '--file') && Boolean(arg(args, '--repo') && arg(args, '--id'));
77
+ if (managed) {
78
+ const jobId = arg(args, '--id');
79
+ if (action === 'list') {
80
+ const job = await relayControlJob(args, jobId);
81
+ console.log(JSON.stringify(job.tasks, null, 2));
82
+ return;
83
+ }
84
+ const taskId = arg(args, '--task-id', action === 'create' ? randomUUID() : '');
85
+ if (action === 'show') {
86
+ if (!taskId)
87
+ throw new Error('task show requires --task-id');
88
+ const job = await relayControlJob(args, jobId);
89
+ const task = job.tasks.find(item => item.id === taskId);
90
+ if (!task)
91
+ throw new Error(`Task ${taskId} not found`);
92
+ console.log(JSON.stringify(task, null, 2));
93
+ return;
94
+ }
95
+ if (['create', 'start', 'complete', 'retry', 'cancel'].includes(action)) {
96
+ await runManagedTaskCommand(action, args);
97
+ return;
98
+ }
99
+ if (action === 'recover') {
100
+ if (!taskId)
101
+ throw new Error('task recover requires --task-id');
102
+ const recoveryTaskId = arg(args, '--recovery-task-id');
103
+ if (!recoveryTaskId)
104
+ throw new Error('task recover requires --recovery-task-id');
105
+ const job = await relayControlCommand(args, { command: 'task.recover', jobId, taskId, recoveryTaskId });
106
+ console.log(JSON.stringify(job.tasks.find(item => item.id === taskId), null, 2));
107
+ return;
108
+ }
109
+ if (action === 'unblock') {
110
+ if (!taskId)
111
+ throw new Error('task unblock requires --task-id');
112
+ const reason = arg(args, '--reason');
113
+ if (!reason.trim())
114
+ throw new Error('task unblock requires --reason');
115
+ const job = await relayControlCommand(args, { command: 'task.unblock', jobId, taskId, reason });
116
+ console.log(JSON.stringify(job.tasks.find(item => item.id === taskId), null, 2));
117
+ return;
118
+ }
119
+ if (action === 'update') {
120
+ const job = await relayControlJob(args, jobId);
121
+ const task = job.tasks.find(item => item.id === taskId);
122
+ if (!task)
123
+ throw new Error(`Task ${taskId} not found`);
124
+ const expectedUpdatedAt = task.updatedAt;
125
+ if (['RUNNING', 'UNBLOCKED', 'SUCCEEDED', 'CANCELLED'].includes(task.state))
126
+ throw new Error(`Task ${taskId} is ${task.state}; durable execution fields are immutable in this state`);
127
+ if (hasArg(args, '--adapter')) {
128
+ const adapter = arg(args, '--adapter');
129
+ if (!['codex', 'chatgpt', 'orchestrator'].includes(adapter))
130
+ throw new Error('--adapter must be codex, chatgpt, or orchestrator');
131
+ task.adapter = adapter;
132
+ }
133
+ if (hasArg(args, '--priority'))
134
+ task.priority = priority(args, task.priority ?? job.priority ?? 'P2');
135
+ if (hasArg(args, '--story-size')) {
136
+ task.storySize = storySize(args, task.storySize ?? 'M');
137
+ if (!hasArg(args, '--timeout'))
138
+ task.timeoutMs = taskOverallTimeoutMs(task.storySize, task.routing?.reasoning);
139
+ }
140
+ if (hasArg(args, '--input') || hasArg(args, '--input-file'))
141
+ task.input = await textOption(args, '--input', '--input-file');
142
+ if (hasArg(args, '--project'))
143
+ task.projectName = arg(args, '--project') || undefined;
144
+ if (hasArg(args, '--agent'))
145
+ task.agentName = arg(args, '--agent') || undefined;
146
+ if (hasArg(args, '--parent'))
147
+ task.parentTaskId = arg(args, '--parent') || null;
148
+ if (hasArg(args, '--deps'))
149
+ task.dependencies = arg(args, '--deps').split(',').filter(Boolean);
150
+ if (hasArg(args, '--capabilities'))
151
+ task.capabilities = arg(args, '--capabilities').split(',').filter(Boolean);
152
+ task.timeoutMs = integerArg(args, '--timeout', task.timeoutMs);
153
+ task.maxAttempts = integerArg(args, '--max-attempts', task.maxAttempts);
154
+ if (hasArg(args, '--draft') && hasArg(args, '--queue'))
155
+ throw new Error('--draft and --queue are mutually exclusive');
156
+ if (hasArg(args, '--draft') && task.state === 'QUEUED')
157
+ await applyTaskHoldAction(task);
158
+ if (hasArg(args, '--queue'))
159
+ await applyTaskQueueAction(job, task);
160
+ const updated = await relayTaskCommand(args, { jobId, taskId, action: 'update', task, expectedUpdatedAt });
161
+ console.log(JSON.stringify(updated.tasks.find(item => item.id === taskId), null, 2));
162
+ return;
163
+ }
142
164
  }
143
165
  const loaded = await storeFor(args);
144
166
  const { store, localFile } = loaded;
145
- const job = managed && action === 'update' ? await relayControlJob(args, loaded.job.id) : loaded.job;
167
+ const job = loaded.job;
146
168
  if (action === 'list') {
147
169
  console.log(JSON.stringify(job.tasks, null, 2));
148
170
  return;
@@ -196,11 +218,14 @@ async function runTaskCommand(action, args) {
196
218
  console.log(JSON.stringify(task, null, 2));
197
219
  return;
198
220
  }
199
- if (action === 'retry' || action === 'cancel') {
221
+ if (action === 'retry' || action === 'recover' || action === 'cancel') {
200
222
  let outputJob = job;
201
223
  let outputTask = task;
202
- if (action === 'retry') {
203
- const result = await dispatchTaskAction(store, { id: 'task.retry', jobId: job.id, taskId: id });
224
+ if (action === 'retry' || action === 'recover') {
225
+ const recoveryTaskId = action === 'recover' ? arg(args, '--recovery-task-id') : '';
226
+ if (action === 'recover' && !recoveryTaskId)
227
+ throw new Error('task recover requires --recovery-task-id');
228
+ const result = await dispatchTaskAction(store, { id: action === 'recover' ? 'task.recover' : 'task.retry', jobId: job.id, taskId: id, ...(recoveryTaskId ? { recoveryTaskId } : {}) });
204
229
  if (result.job.state === 'DRAFT') {
205
230
  await applyJobActivateAction(result.job);
206
231
  await store.saveJob(result.job);
@@ -338,13 +363,7 @@ async function runJobCommand(action, args) {
338
363
  throw new Error('job create with --head requires --objective-file PATH|- or --objective MARKDOWN containing the complete standalone Job context');
339
364
  }
340
365
  if (action === 'list') {
341
- const snapshots = await discoverLocalRelayStoreSnapshots(join(homedir(), '.agents-relay', 'stores'));
342
- const overviews = snapshots.flatMap(snapshot => snapshot.jobs.filter(job => job.repository === repo)
343
- .map(job => ({
344
- job, githubState: snapshot.facts?.state ?? 'UNKNOWN',
345
- draft: false, mergedAt: null, closedAt: null, publication: snapshot.publications[job.id]?.latest ?? null, publicationFailure: snapshot.publications[job.id]?.failure ?? null,
346
- })));
347
- console.log(JSON.stringify(dashboardManagedJobs(overviews), null, 2));
366
+ console.log(JSON.stringify(await relayJobList(args), null, 2));
348
367
  return;
349
368
  }
350
369
  const id = arg(args, '--id');
@@ -358,49 +377,39 @@ async function runJobCommand(action, args) {
358
377
  const objective = createObjective ?? '';
359
378
  if (hasArg(args, '--local-merge-source') !== hasArg(args, '--local-merge-target'))
360
379
  throw new Error('Supply both --local-merge-source and --local-merge-target');
361
- const auth = { client: new DisabledGitHubClient(), trustedAuthors: new Set() };
362
- const result = await ensureManagedGitHubJob(auth.client, repo, auth.trustedAuthors, {
363
- id, noGitHub: noGitHubMode(args),
364
- localMergeSource: arg(args, '--local-merge-source') || undefined,
365
- localMergeTarget: arg(args, '--local-merge-target') || undefined,
366
- title: arg(args, '--title') || undefined,
367
- objective: objective || undefined,
368
- projectPath: arg(args, '--project-path') || undefined,
369
- priority: priority(args),
370
- executionMode: requestedExecutionMode(args),
371
- schedule: scheduleFromArgs(args),
372
- tag: jobTagFromArgs(args),
373
- reviewEnabled: !hasArg(args, '--no-review'),
374
- continuation: continuation(args),
375
- ...(hasArg(args, '--reviewer-thread-id') ? { reviewerThreadId: arg(args, '--reviewer-thread-id') } : {}),
376
- ...(hasArg(args, '--creator-thread-id') ? { creatorThreadId: arg(args, '--creator-thread-id') } : {}),
377
- prNumber,
378
- head: arg(args, '--head') || undefined,
379
- base: arg(args, '--base', 'main'),
380
- body: arg(args, '--body') || objective || undefined
381
- });
382
- console.log(JSON.stringify({ repository: repo, pr: result.pr.number, job: result.job.id, executionMode: result.job.executionMode, state: result.job.state }, null, 2));
380
+ const result = await relayControlCommand(args, { command: 'job.create', repository: repo, initialPlan: hasArg(args, '--initial-plan'), options: {
381
+ id, title: arg(args, '--title') || undefined, objective: objective || undefined, projectPath: arg(args, '--project-path') || undefined,
382
+ priority: priority(args), executionMode: requestedExecutionMode(args), schedule: scheduleFromArgs(args), tag: jobTagFromArgs(args), reviewEnabled: !hasArg(args, '--no-review'), continuation: continuation(args),
383
+ reviewerThreadId: hasArg(args, '--reviewer-thread-id') ? arg(args, '--reviewer-thread-id') : undefined, creatorThreadId: hasArg(args, '--creator-thread-id') ? arg(args, '--creator-thread-id') : undefined,
384
+ prNumber, head: arg(args, '--head') || undefined, base: arg(args, '--base', 'main'), body: arg(args, '--body') || objective || undefined,
385
+ localMergeSource: arg(args, '--local-merge-source') || undefined, localMergeTarget: arg(args, '--local-merge-target') || undefined,
386
+ } });
387
+ console.log(JSON.stringify(result, null, 2));
383
388
  return;
384
389
  }
385
- const loaded = await storeFor(args);
386
- const { store, job } = loaded;
390
+ const job = await relayControlJob(args, id);
387
391
  if (action === 'unblock') {
388
- await runJobUnblockCommand(args, store, job);
392
+ const reason = arg(args, '--reason');
393
+ if (!reason.trim())
394
+ throw new Error('job unblock requires --reason');
395
+ const updated = await relayControlCommand(args, { command: 'job.unblock', jobId: id, reason });
396
+ console.log(JSON.stringify(updated, null, 2));
389
397
  return;
390
398
  }
391
399
  if (action === 'show') {
392
400
  console.log(JSON.stringify(job, null, 2));
393
401
  return;
394
402
  }
395
- if (job.repository !== repo || (prValue && job.prNumber !== prNumber)) {
403
+ const boundPrNumber = job.externalPrNumber ?? (job.prNumber > 0 ? job.prNumber : 0);
404
+ if (job.repository !== repo || (prValue && boundPrNumber !== prNumber)) {
396
405
  throw new Error('Local Job/PR binding mismatch');
397
406
  }
398
407
  if (action === 'merge') {
399
408
  throw new Error('GitHub is audit-only. Configure local merge source/target and reconcile the local Job instead.');
400
409
  }
401
410
  if (action === 'cancel') {
402
- await applyJobCancelAction(store, job);
403
- console.log(JSON.stringify(await store.load(id), null, 2));
411
+ const updated = await relayJobStateCommand(args, job.id, 'CANCELLED');
412
+ console.log(JSON.stringify(updated, null, 2));
404
413
  return;
405
414
  }
406
415
  if (hasArg(args, '--title'))
@@ -445,12 +454,13 @@ async function runJobCommand(action, args) {
445
454
  throw new Error('--continue-kind and --continue-target must be supplied together');
446
455
  job.continuation = next;
447
456
  }
448
- job.updatedAt = new Date().toISOString();
449
- await store.saveJob(job);
450
- console.log(JSON.stringify(await store.load(id), null, 2));
457
+ const patch = {};
458
+ for (const key of ['title', 'objective', 'projectPath', 'priority', 'executionMode', 'schedule', 'tag', 'autoMerge', 'reviewEnabled', 'reviewerThreadId', 'creatorThreadId', 'continuation', 'localMerge'])
459
+ patch[key] = job[key];
460
+ console.log(JSON.stringify(await relayControlCommand(args, { command: 'job.update', jobId: id, patch }), null, 2));
451
461
  }
452
462
  export function runtimePlanner(args) { const plannerCommand = arg([...args], '--planner-command'); const modelRuntime = arg([...args], '--codex', 'codex'); return plannerCommand ? new CommandObjectivePlanner(plannerCommand) : new AgentObjectivePlanner(modelRuntime); }
453
- export function runtime(store, args, bus, usageRegistry = codexAndZaiUsageRegistry()) { const emit = (event) => { process.stderr.write(`${JSON.stringify(event)}\n`); }; const modelRuntime = arg(args, '--codex', 'codex'); const policy = chatGptTabClosePolicy(args); const planner = runtimePlanner(args); return new Reconciler(store, { noGitHub: noGitHubMode(args), owner: arg(args, '--owner', `cli-${process.pid}`), maxConcurrent: Number(arg(args, '--concurrency', '4')), leaseMs: Number(arg(args, '--lease-ms', '300000')), adapters: [new CodexAdapter(modelRuntime), new ChatGptAdapter({ tabClosePolicy: policy })], continuations: [new CodexThreadContinuation(modelRuntime), new CommandContinuation(), new WebhookContinuation()], planner, usageSnapshotProvider: () => usageRegistry.snapshot(), decisionEngine: executionDecisionEngine(), decisionMode: decisionMode(), reviewer: runtimeReviewer(args), eventBus: bus, emit, chatgptTabClosePolicy: policy, projectPathForJob: async (job) => resolveRepositoryProjectPath(await resolveManagedProjectPath(noGitHubMode(args) ? undefined : job.repository, arg(args, '--project-path'), arg(args, '--workspace-root') || defaultWorkspaceRoot()), job.projectPath) }); }
463
+ export function runtime(store, args, bus, usageRegistry = codexAndZaiUsageRegistry()) { const emit = (event) => { process.stderr.write(`${JSON.stringify(event)}\n`); }; const modelRuntime = arg(args, '--codex', 'codex'); const policy = chatGptTabClosePolicy(args); const planner = runtimePlanner(args); return new Reconciler(store, { noGitHub: noGitHubMode(args), owner: arg(args, '--owner', `cli-${process.pid}`), maxConcurrent: Number(arg(args, '--concurrency', '4')), leaseMs: Number(arg(args, '--lease-ms', '300000')), adapters: [new CodexAdapter(modelRuntime), new ChatGptAdapter({ tabClosePolicy: policy })], continuations: [new CodexThreadContinuation(modelRuntime), new CommandContinuation(), new WebhookContinuation()], planner, usageSnapshotProvider: () => usageRegistry.snapshot(), decisionEngine: executionDecisionEngine(), decisionMode: decisionMode(), reviewer: runtimeReviewer(args), eventBus: bus, emit, chatgptTabClosePolicy: policy, projectPathForJob: async (job) => { const workspaceRoot = arg(args, '--workspace-root') || defaultWorkspaceRoot(); const canonical = await resolveManagedProjectPath(noGitHubMode(args) ? undefined : job.repository, arg(args, '--project-path'), workspaceRoot); return job.prNumber === 0 && job.localMerge?.sourceRef ? prepareLocalJobExecutionWorktree(canonical, job, workspaceRoot) : resolveRepositoryProjectPath(canonical, job.projectPath); } }); }
454
464
  export function dashboardJobAction(store, reconciler, action) {
455
465
  return async (jobId) => {
456
466
  const job = await store.load(jobId);
@@ -477,7 +487,7 @@ export async function createService(args) {
477
487
  let overviewProvider;
478
488
  if (repo) {
479
489
  overviewProvider = async () => {
480
- const snapshots = await discoverLocalRelayStoreSnapshots(join(homedir(), '.agents-relay', 'stores'));
490
+ const snapshots = await discoverLocalRelayStoreSnapshots(relayStoreDirectory());
481
491
  return dashboardManagedJobs(snapshots.flatMap(snapshot => snapshot.jobs
482
492
  .filter(job => job.repository === repo)
483
493
  .map(job => ({
@@ -490,7 +500,8 @@ export async function createService(args) {
490
500
  const webhook = !noGitHubMode(args) && repo && webhookSecret ? {
491
501
  secret: webhookSecret,
492
502
  repositories: new Set([repo]),
493
- pullRequest: loaded.job.prNumber,
503
+ pullRequest: loaded.job.externalPrNumber ?? loaded.job.prNumber,
504
+ jobId: loaded.job.id,
494
505
  wake: async (event) => {
495
506
  if (event.pullRequestFacts && loaded.store.applyPullRequestFacts) {
496
507
  await loaded.store.applyPullRequestFacts({
@@ -606,19 +617,18 @@ async function main() {
606
617
  return;
607
618
  }
608
619
  if (command === 'init') {
620
+ const file = arg(args, '--file');
609
621
  const now = new Date().toISOString();
610
622
  const initialSchedule = scheduleFromArgs(args);
611
- const job = createJobEntity({ id: arg(args, '--id', randomUUID()), title: arg(args, '--title', 'Agents Relay job'), objective: arg(args, '--objective') || undefined, priority: priority(args), executionMode: executionMode(args), schedule: initialSchedule, autoMerge: !hasArg(args, '--no-auto-merge'), reviewEnabled: !hasArg(args, '--no-review'), prNumber: Number(arg(args, '--pr', '0')), repository: arg(args, '--repo'), continuation: continuation(args), createdAt: now, updatedAt: now, tasks: [] }, { activate: Boolean(initialSchedule) });
612
- let store;
613
- let file = null;
614
- if (arg(args, '--repo') && arg(args, '--pr'))
615
- store = openLocalManagedStore(repository(args), pullRequest(args)).store;
616
- else {
617
- file = arg(args, '--file');
618
- if (!file)
619
- throw new Error('init requires --repo/--pr or explicit --file demo mode');
620
- store = new InMemoryStore(job);
623
+ const id = arg(args, '--id', randomUUID());
624
+ if (!file) {
625
+ const repo = repository(args);
626
+ const result = await relayControlCommand(args, { command: 'job.create', repository: repo, options: { id, title: arg(args, '--title', 'Agents Relay job'), objective: arg(args, '--objective') || undefined, priority: priority(args), executionMode: executionMode(args), schedule: initialSchedule, reviewEnabled: !hasArg(args, '--no-review'), continuation: continuation(args), prNumber: Number(arg(args, '--pr', '0')) || 0 } });
627
+ console.log(JSON.stringify(result, null, 2));
628
+ return;
621
629
  }
630
+ const job = createJobEntity({ id, title: arg(args, '--title', 'Agents Relay job'), objective: arg(args, '--objective') || undefined, priority: priority(args), executionMode: executionMode(args), schedule: initialSchedule, autoMerge: !hasArg(args, '--no-auto-merge'), reviewEnabled: !hasArg(args, '--no-review'), prNumber: Number(arg(args, '--pr', '0')), repository: arg(args, '--repo'), continuation: continuation(args), createdAt: now, updatedAt: now, tasks: [] }, { activate: Boolean(initialSchedule) });
631
+ const store = new InMemoryStore(job);
622
632
  await store.saveJob(job);
623
633
  await saveLocal(file, job);
624
634
  console.log(`Initialized ${job.id}`);
@@ -641,6 +651,46 @@ async function main() {
641
651
  console.log(JSON.stringify(await response.json(), null, 2));
642
652
  return;
643
653
  }
654
+ if (!arg(args, '--file') && ['plan', 'reconcile', 'record', 'agent-register', 'agent-discover'].includes(command)) {
655
+ const jobId = arg(args, '--id');
656
+ if (!jobId)
657
+ throw new Error(`${command} requires --repo and --id`);
658
+ if (command === 'plan') {
659
+ const guidance = hasArg(args, '--prompt') || hasArg(args, '--prompt-file') ? await textOption(args, '--prompt', '--prompt-file') : undefined;
660
+ console.log(JSON.stringify(await relayControlCommand(args, { command: 'job.plan', jobId, ...(guidance?.trim() ? { guidance: guidance.trim() } : {}) }), null, 2));
661
+ return;
662
+ }
663
+ if (command === 'reconcile') {
664
+ console.log(JSON.stringify(await relayControlCommand(args, { command: 'job.reconcile', jobId }), null, 2));
665
+ return;
666
+ }
667
+ if (command === 'record') {
668
+ const now = new Date().toISOString();
669
+ const id = arg(args, '--task-id', randomUUID());
670
+ const summary = arg(args, '--summary');
671
+ if (!summary)
672
+ throw new Error('record requires --summary');
673
+ const commit = arg(args, '--commit');
674
+ const task = { jobId, 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 };
675
+ const updated = await relayControlCommand(args, { command: 'record', jobId, task });
676
+ console.log(JSON.stringify(updated.tasks.find(item => item.id === id), null, 2));
677
+ return;
678
+ }
679
+ if (command === 'agent-register') {
680
+ const now = new Date().toISOString();
681
+ const agent = { id: arg(args, '--agent-id', randomUUID()), name: arg(args, '--name'), responsibility: arg(args, '--responsibility'), role: arg(args, '--role'), capabilities: arg(args, '--capabilities').split(',').filter(Boolean), boundaries: arg(args, '--boundaries').split('|').filter(Boolean), endpoint: { kind: arg(args, '--endpoint-kind', 'adapter'), target: arg(args, '--endpoint') }, runtime: { adapter: arg(args, '--runtime', arg(args, '--adapter', 'shell')), provider: arg(args, '--provider') || undefined, model: arg(args, '--model') || undefined }, availability: arg(args, '--availability', 'available'), routing: { labels: arg(args, '--labels').split(',').filter(Boolean).reduce((out, item) => { const [key, value] = item.split('=', 2); if (key && value)
682
+ out[key] = value; return out; }, {}), trustDomains: arg(args, '--trust-domains').split(',').filter(Boolean), regions: arg(args, '--regions').split(',').filter(Boolean) }, evidence: { evaluations: [], outcomes: { succeeded: 0, failed: 0, timedOut: 0 }, lastObservedAt: null }, registeredAt: now, updatedAt: now };
683
+ if (!agent.name || !agent.responsibility || !agent.role || !agent.endpoint.target)
684
+ throw new Error('--name, --responsibility, --role, and --endpoint are required');
685
+ console.log(JSON.stringify(await relayControlCommand(args, { command: 'agent.register', jobId, agent }), null, 2));
686
+ return;
687
+ }
688
+ if (command === 'agent-discover') {
689
+ const requirements = { capabilities: arg(args, '--capabilities').split(',').filter(Boolean), runtime: arg(args, '--runtime') || undefined, availability: arg(args, '--availability').split(',').filter(Boolean), trustDomain: arg(args, '--trust-domain') || undefined, taskKind: arg(args, '--task-kind') || undefined, explore: args.includes('--explore') };
690
+ console.log(JSON.stringify(await relayControlCommand(args, { command: 'agent.discover', jobId, requirements }), null, 2));
691
+ return;
692
+ }
693
+ }
644
694
  const loaded = await storeFor(args);
645
695
  const { store, job, localFile } = loaded;
646
696
  if (command === 'plan') {