agents-relay 2.0.67 → 2.0.69

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 (72) hide show
  1. package/README.md +56 -20
  2. package/dist/adapters.js +39 -3
  3. package/dist/blockers.js +17 -4
  4. package/dist/cli-blocker.js +12 -30
  5. package/dist/cli-help.js +22 -17
  6. package/dist/cli-job-create.js +77 -0
  7. package/dist/cli-local-merge-config.js +11 -0
  8. package/dist/cli-projection.js +23 -55
  9. package/dist/cli-review.js +7 -24
  10. package/dist/cli-support.js +81 -8
  11. package/dist/cli-worker-task.js +63 -0
  12. package/dist/cli.js +223 -185
  13. package/dist/dashboard-html.js +15 -12
  14. package/dist/dashboard.js +27 -2
  15. package/dist/e2e-injection.js +76 -0
  16. package/dist/github-auth.js +4 -3
  17. package/dist/github-binding-order.js +15 -0
  18. package/dist/github-client.js +7 -2
  19. package/dist/github-pr-archive.js +22 -0
  20. package/dist/github-webhook.js +43 -10
  21. package/dist/job-actions.js +29 -0
  22. package/dist/local-authority-store.js +288 -0
  23. package/dist/local-commit-events.js +2 -0
  24. package/dist/local-git-merge.js +124 -0
  25. package/dist/local-job-path.js +7 -0
  26. package/dist/local-job-registry.js +29 -0
  27. package/dist/local-managed-store.js +21 -0
  28. package/dist/local-merge-delivery.js +53 -0
  29. package/dist/local-merge-lock.js +103 -0
  30. package/dist/local-merge-projection.js +60 -0
  31. package/dist/managed-worker-input.js +13 -6
  32. package/dist/manual-plan.js +29 -7
  33. package/dist/no-github.js +11 -0
  34. package/dist/planned-review-head.js +7 -8
  35. package/dist/planner-runtime.js +182 -41
  36. package/dist/planner.js +35 -2
  37. package/dist/pool.js +89 -136
  38. package/dist/projected-store.js +20 -3
  39. package/dist/projection-outbox.js +3 -1
  40. package/dist/publication-store.js +37 -0
  41. package/dist/reconciler-cancellation.js +21 -0
  42. package/dist/reconciler-review.js +99 -29
  43. package/dist/reconciler-runtime.js +72 -35
  44. package/dist/reconciler.js +124 -85
  45. package/dist/relay-store.js +108 -6
  46. package/dist/relayd-control.js +184 -0
  47. package/dist/relayd-dashboard-actions.js +108 -0
  48. package/dist/relayd-utilities.js +107 -0
  49. package/dist/relayd.js +304 -299
  50. package/dist/review-generation-fence.js +12 -0
  51. package/dist/runtime-status-events.js +21 -0
  52. package/dist/runtime-status.js +21 -7
  53. package/dist/scheduled-jobs.js +43 -36
  54. package/dist/service-external-actions.js +27 -0
  55. package/dist/service-publication.js +29 -0
  56. package/dist/store-github.js +17 -6
  57. package/dist/task-actions.js +59 -1
  58. package/dist/task-update.js +1 -1
  59. package/dist/troubleshooter.js +21 -2
  60. package/dist/unblock.js +47 -15
  61. package/dist/worker-rejection.js +11 -0
  62. package/dist/workflow/contracts.js +10 -1
  63. package/dist/workflow/definitions-v2.js +2 -0
  64. package/dist/workflow/job-reconcile-v2.js +9 -60
  65. package/dist/workflow/runtime.js +18 -3
  66. package/dist/workflow-effect-failures.js +30 -0
  67. package/dist/workspace.js +95 -6
  68. package/package.json +6 -3
  69. package/skills/agents-relay/agents/planner.agent.md +3 -0
  70. package/skills/agents-relay/contracts/orchestrator-workflows.md +4 -7
  71. package/workflow/job-lifecycle.mmd +19 -60
  72. package/workflow/review-lifecycle.mmd +5 -8
package/dist/cli.js CHANGED
@@ -1,139 +1,48 @@
1
1
  #!/usr/bin/env node
2
+ import { homedir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import { configureLocalGitMerge } from './cli-local-merge-config.js';
5
+ import { noGitHubMode } from './no-github.js';
6
+ import { discoverLocalRelayStoreSnapshots } from './relay-store.js';
2
7
  import { realpathSync } from 'node:fs';
3
8
  import { once } from 'node:events';
4
9
  import { fileURLToPath } from 'node:url';
5
10
  import { randomUUID } from 'node:crypto';
6
- import { dashboardManagedJobs, InMemoryStore, GitHubStore, resolvePullRequest, listManagedGitHubJobs } from './store.js';
7
- import { githubAuthContext } from './github-auth.js';
11
+ import { dashboardManagedJobs, InMemoryStore } from './store.js';
8
12
  import { taskOverallTimeoutMs } from './types.js';
9
13
  import { Reconciler } from './reconciler.js';
10
14
  import { dashboardJobSetState, dashboardTaskAction, dashboardTaskSetState } from './dashboard-actions.js';
11
15
  export { dashboardJobSetState, dashboardTaskAction, dashboardTaskSetState } from './dashboard-actions.js';
12
16
  import { CodexAdapter, ChatGptAdapter } from './adapters.js';
13
17
  import { CodexThreadContinuation, CommandContinuation, WebhookContinuation } from './continuation.js';
14
- import { eventFor } from './events.js';
15
18
  import { serveDashboard } from './dashboard.js';
16
19
  import { codexAndZaiUsageRegistry } from './usage.js';
17
20
  import { decisionMode, executionDecisionEngine } from './decision.js';
18
21
  import { discoverAgents } from './registry.js';
19
22
  import { AgentObjectivePlanner, CommandObjectivePlanner } from './planner.js';
20
- import { transitionJobEvent } from './scheduler.js';
21
23
  import { applyOrchestratorStartAction, applyOrchestratorCompleteAction, applyTaskHoldAction, applyTaskQueueAction, dispatchTaskAction } from './task-actions.js';
22
24
  import { applyJobActivateAction, applyJobCancelAction } from './job-actions.js';
23
25
  import { resolveExecutionContext } from './execution-context.js';
24
- import { defaultWorkspaceRoot, resolveManagedProjectPath, resolveRepositoryProjectPath } from './workspace.js';
26
+ import { startServiceExternalActionFlusher } from './service-external-actions.js';
27
+ import { handleServicePublication } from './service-publication.js';
28
+ import { defaultWorkspaceRoot, prepareLocalJobExecutionWorktree, resolveManagedProjectPath, resolveRepositoryProjectPath } from './workspace.js';
25
29
  import { runBlockerCommand } from './cli-blocker.js';
26
30
  import { runEscalationCommand } from './cli-escalation.js';
27
31
  import { runProjectionCommand } from './cli-projection.js';
28
32
  import { helpText, usage } from './cli-help.js';
29
33
  import { parseAgentGraph } from './agent-graph.js';
30
- import { runJobUnblockCommand, runTaskUnblockCommand } from './cli-unblock.js';
31
- import { createJobEntity, createTaskEntity } from './workflow/entity-creation.js';
34
+ import { runTaskUnblockCommand } from './cli-unblock.js';
35
+ import { createJobEntity } from './workflow/entity-creation.js';
32
36
  import { runCliEntrypoint } from './cli-shutdown.js';
33
37
  import { shutdownOwnedProcesses } from './owned-process.js';
34
38
  export { helpText } from './cli-help.js';
35
39
  import { runReviewCommand } from './cli-review.js';
36
40
  import { normalizeThreadId, runtimeReviewer } from './reviewer-thread.js';
37
41
  export { runtimeReviewer } from './reviewer-thread.js';
38
- import { SERVICE_DEFAULTS, ServiceEventBus, arg, hasArg, textOption, integerArg, repository, storeFor, saveLocal, githubStore, eventBus, publishWake, publishDependentUnblocked, priority, storySize, executionMode, chatGptTabClosePolicy, requestedExecutionMode, continuation, scheduleFromArgs, jobTagFromArgs, requestManagedReconciliation, relayControlJob, relayTaskCommand, withEventBus } from './cli-support.js';
42
+ 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';
39
43
  export { SERVICE_DEFAULTS, eventBus, executionMode, chatGptTabClosePolicy } from './cli-support.js';
40
- export async function ensureManagedGitHubJob(client, repositoryName, trustedAuthors, options) {
41
- if (!options.id)
42
- throw new Error('--id is required for managed GitHub jobs');
43
- const pr = await resolvePullRequest(client, repositoryName, { prNumber: options.prNumber, head: options.head, base: options.base, title: options.title, body: options.body });
44
- const store = new GitHubStore(client, repositoryName, pr.number, trustedAuthors);
45
- try {
46
- const existing = await store.load(options.id);
47
- return { job: existing, pr };
48
- }
49
- catch (error) {
50
- if (!(error instanceof Error) || !error.message.startsWith('JOB_MARKER_MISSING:'))
51
- throw error;
52
- }
53
- const now = new Date().toISOString();
54
- const job = createJobEntity({
55
- id: options.id,
56
- title: options.title || pr.title || 'Agents Relay job',
57
- objective: options.objective || pr.body || undefined,
58
- projectPath: options.projectPath,
59
- description: pr.body || undefined,
60
- priority: options.priority ?? 'P2',
61
- executionMode: options.executionMode ?? 'fixed',
62
- tag: options.tag,
63
- autoMerge: true,
64
- reviewEnabled: options.reviewEnabled ?? true,
65
- ...(options.reviewerThreadId ? { reviewerThreadId: normalizeThreadId(options.reviewerThreadId, 'reviewer thread ID') } : {}),
66
- ...(options.creatorThreadId ? { creatorThreadId: normalizeThreadId(options.creatorThreadId, 'creator thread ID') } : {}),
67
- prNumber: pr.number,
68
- repository: repositoryName,
69
- continuation: options.continuation ?? null,
70
- createdAt: now,
71
- updatedAt: now,
72
- tasks: [],
73
- ...(options.schedule ? { schedule: options.schedule } : {}),
74
- }, { activate: Boolean(options.schedule) });
75
- await store.saveJob(job);
76
- return { job: await store.load(options.id), pr };
77
- }
78
- async function workerTaskFromArgs(job, args, now) {
79
- const model = arg(args, '--model');
80
- const adapterValue = arg(args, '--adapter');
81
- const adapter = (adapterValue || 'codex');
82
- const outputKind = arg(args, '--output');
83
- const outputPath = arg(args, '--output-path');
84
- const input = await textOption(args, '--input', '--input-file');
85
- const agentGraph = hasArg(args, '--agent-graph') || hasArg(args, '--agent-graph-file') ? await textOption(args, '--agent-graph', '--agent-graph-file') : undefined;
86
- if (agentGraph)
87
- parseAgentGraph(agentGraph);
88
- if (!input.trim())
89
- throw new Error('task create requires --input-file PATH|- or --input MARKDOWN');
90
- if (!['codex', 'chatgpt', 'orchestrator'].includes(adapter))
91
- throw new Error('task create --adapter must be codex, chatgpt, or orchestrator');
92
- if (outputKind !== 'task-pr' && outputKind !== 'file')
93
- throw new Error('task create requires --output task-pr|file');
94
- if (outputKind === 'file' && !outputPath)
95
- throw new Error('--output file requires --output-path');
96
- if (adapter !== 'orchestrator' && ['--provider', '--profile', '--reasoning', '--cwd', '--chatgpt-project'].some(flag => hasArg(args, flag)) && !model)
97
- throw new Error('task create explicit routing requires --model');
98
- 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 };
99
- const workerRequirements = adapter === 'orchestrator' || routing ? undefined : { capabilities: [], runtime: adapterValue ? adapter : undefined };
100
- const closePolicy = hasArg(args, '--chatgpt-tab-close-policy') ? chatGptTabClosePolicy(args) : undefined;
101
- const size = storySize(args);
102
- const dependencies = arg(args, '--deps').split(',').filter(Boolean);
103
- const timeoutMs = adapter === 'orchestrator' ? 0 : hasArg(args, '--timeout') ? integerArg(args, '--timeout', taskOverallTimeoutMs(size, routing?.reasoning)) : routing ? taskOverallTimeoutMs(size, routing.reasoning) : 0;
104
- return createTaskEntity(job, {
105
- jobId: job.id,
106
- id: arg(args, '--task-id', 'initial'),
107
- priority: priority(args, job.priority ?? 'P2'),
108
- storySize: size,
109
- projectName: arg(args, '--project') || undefined,
110
- agentName: arg(args, '--agent') || undefined,
111
- agentGraph,
112
- parentTaskId: arg(args, '--parent') || null,
113
- dependencies,
114
- capabilities: arg(args, '--capabilities').split(',').filter(Boolean),
115
- workerRequirements,
116
- adapter,
117
- chatgptTabClosePolicy: closePolicy,
118
- input: input || 'true',
119
- attachments: arg(args, '--attachments').split(',').filter(Boolean),
120
- output: outputKind === 'file' ? { kind: 'file', path: outputPath } : { kind: 'task_pr' },
121
- routing,
122
- continuation: continuation(args),
123
- continuationDeliveredAt: null,
124
- attempt: 0,
125
- maxAttempts: Number(arg(args, '--max-attempts', '3')),
126
- leaseOwner: null,
127
- leaseExpiresAt: null,
128
- executionId: null,
129
- threadId: null,
130
- result: null,
131
- error: null,
132
- timeoutMs,
133
- createdAt: now,
134
- updatedAt: now
135
- }, { defer: hasArg(args, '--draft') });
136
- }
44
+ import { workerTaskFromArgs } from './cli-worker-task.js';
45
+ export { ensureManagedGitHubJob } from './cli-job-create.js';
137
46
  async function runManagedTaskCommand(action, args) {
138
47
  const jobId = arg(args, '--id');
139
48
  if (!jobId)
@@ -164,16 +73,100 @@ async function runManagedTaskCommand(action, args) {
164
73
  console.log(JSON.stringify(task, null, 2));
165
74
  }
166
75
  async function runTaskCommand(action, args) {
167
- if (!['create', 'update', 'show', 'list', 'start', 'complete', 'retry', 'unblock', 'cancel'].includes(action))
76
+ if (!['create', 'update', 'show', 'list', 'start', 'complete', 'retry', 'recover', 'unblock', 'cancel'].includes(action))
168
77
  throw new Error(usage);
169
- const managed = Boolean(arg(args, '--repo') && arg(args, '--pr') && !arg(args, '--file'));
170
- if (managed && ['create', 'start', 'complete', 'retry', 'cancel'].includes(action)) {
171
- await runManagedTaskCommand(action, args);
172
- return;
78
+ const managed = !arg(args, '--file') && Boolean(arg(args, '--repo') && arg(args, '--id'));
79
+ if (managed) {
80
+ const jobId = arg(args, '--id');
81
+ if (action === 'list') {
82
+ const job = await relayControlJob(args, jobId);
83
+ console.log(JSON.stringify(job.tasks, null, 2));
84
+ return;
85
+ }
86
+ const taskId = arg(args, '--task-id', action === 'create' ? randomUUID() : '');
87
+ if (action === 'show') {
88
+ if (!taskId)
89
+ throw new Error('task show requires --task-id');
90
+ const job = await relayControlJob(args, jobId);
91
+ const task = job.tasks.find(item => item.id === taskId);
92
+ if (!task)
93
+ throw new Error(`Task ${taskId} not found`);
94
+ console.log(JSON.stringify(task, null, 2));
95
+ return;
96
+ }
97
+ if (['create', 'start', 'complete', 'retry', 'cancel'].includes(action)) {
98
+ await runManagedTaskCommand(action, args);
99
+ return;
100
+ }
101
+ if (action === 'recover') {
102
+ if (!taskId)
103
+ throw new Error('task recover requires --task-id');
104
+ const recoveryTaskId = arg(args, '--recovery-task-id');
105
+ if (!recoveryTaskId)
106
+ throw new Error('task recover requires --recovery-task-id');
107
+ const job = await relayControlCommand(args, { command: 'task.recover', jobId, taskId, recoveryTaskId });
108
+ console.log(JSON.stringify(job.tasks.find(item => item.id === taskId), null, 2));
109
+ return;
110
+ }
111
+ if (action === 'unblock') {
112
+ if (!taskId)
113
+ throw new Error('task unblock requires --task-id');
114
+ const reason = arg(args, '--reason');
115
+ if (!reason.trim())
116
+ throw new Error('task unblock requires --reason');
117
+ const job = await relayControlCommand(args, { command: 'task.unblock', jobId, taskId, reason });
118
+ console.log(JSON.stringify(job.tasks.find(item => item.id === taskId), null, 2));
119
+ return;
120
+ }
121
+ if (action === 'update') {
122
+ const job = await relayControlJob(args, jobId);
123
+ const task = job.tasks.find(item => item.id === taskId);
124
+ if (!task)
125
+ throw new Error(`Task ${taskId} not found`);
126
+ const expectedUpdatedAt = task.updatedAt;
127
+ if (['RUNNING', 'UNBLOCKED', 'SUCCEEDED', 'CANCELLED'].includes(task.state))
128
+ throw new Error(`Task ${taskId} is ${task.state}; durable execution fields are immutable in this state`);
129
+ if (hasArg(args, '--adapter')) {
130
+ const adapter = arg(args, '--adapter');
131
+ if (!['codex', 'chatgpt', 'orchestrator'].includes(adapter))
132
+ throw new Error('--adapter must be codex, chatgpt, or orchestrator');
133
+ task.adapter = adapter;
134
+ }
135
+ if (hasArg(args, '--priority'))
136
+ task.priority = priority(args, task.priority ?? job.priority ?? 'P2');
137
+ if (hasArg(args, '--story-size')) {
138
+ task.storySize = storySize(args, task.storySize ?? 'M');
139
+ if (!hasArg(args, '--timeout'))
140
+ task.timeoutMs = taskOverallTimeoutMs(task.storySize, task.routing?.reasoning);
141
+ }
142
+ if (hasArg(args, '--input') || hasArg(args, '--input-file'))
143
+ task.input = await textOption(args, '--input', '--input-file');
144
+ if (hasArg(args, '--project'))
145
+ task.projectName = arg(args, '--project') || undefined;
146
+ if (hasArg(args, '--agent'))
147
+ task.agentName = arg(args, '--agent') || undefined;
148
+ if (hasArg(args, '--parent'))
149
+ task.parentTaskId = arg(args, '--parent') || null;
150
+ if (hasArg(args, '--deps'))
151
+ task.dependencies = arg(args, '--deps').split(',').filter(Boolean);
152
+ if (hasArg(args, '--capabilities'))
153
+ task.capabilities = arg(args, '--capabilities').split(',').filter(Boolean);
154
+ task.timeoutMs = integerArg(args, '--timeout', task.timeoutMs);
155
+ task.maxAttempts = integerArg(args, '--max-attempts', task.maxAttempts);
156
+ if (hasArg(args, '--draft') && hasArg(args, '--queue'))
157
+ throw new Error('--draft and --queue are mutually exclusive');
158
+ if (hasArg(args, '--draft') && task.state === 'QUEUED')
159
+ await applyTaskHoldAction(task);
160
+ if (hasArg(args, '--queue'))
161
+ await applyTaskQueueAction(job, task);
162
+ const updated = await relayTaskCommand(args, { jobId, taskId, action: 'update', task, expectedUpdatedAt });
163
+ console.log(JSON.stringify(updated.tasks.find(item => item.id === taskId), null, 2));
164
+ return;
165
+ }
173
166
  }
174
167
  const loaded = await storeFor(args);
175
168
  const { store, localFile } = loaded;
176
- const job = managed && action === 'update' ? await relayControlJob(args, loaded.job.id) : loaded.job;
169
+ const job = loaded.job;
177
170
  if (action === 'list') {
178
171
  console.log(JSON.stringify(job.tasks, null, 2));
179
172
  return;
@@ -227,11 +220,14 @@ async function runTaskCommand(action, args) {
227
220
  console.log(JSON.stringify(task, null, 2));
228
221
  return;
229
222
  }
230
- if (action === 'retry' || action === 'cancel') {
223
+ if (action === 'retry' || action === 'recover' || action === 'cancel') {
231
224
  let outputJob = job;
232
225
  let outputTask = task;
233
- if (action === 'retry') {
234
- const result = await dispatchTaskAction(store, { id: 'task.retry', jobId: job.id, taskId: id });
226
+ if (action === 'retry' || action === 'recover') {
227
+ const recoveryTaskId = action === 'recover' ? arg(args, '--recovery-task-id') : '';
228
+ if (action === 'recover' && !recoveryTaskId)
229
+ throw new Error('task recover requires --recovery-task-id');
230
+ const result = await dispatchTaskAction(store, { id: action === 'recover' ? 'task.recover' : 'task.retry', jobId: job.id, taskId: id, ...(recoveryTaskId ? { recoveryTaskId } : {}) });
235
231
  if (result.job.state === 'DRAFT') {
236
232
  await applyJobActivateAction(result.job);
237
233
  await store.saveJob(result.job);
@@ -365,75 +361,57 @@ async function runJobCommand(action, args) {
365
361
  if (invalid)
366
362
  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`);
367
363
  createObjective = await textOption(args, '--objective', '--objective-file');
368
- if (!arg(args, '--pr') && !createObjective.trim())
364
+ if ((!arg(args, '--pr') || noGitHubMode(args)) && !createObjective.trim())
369
365
  throw new Error('job create with --head requires --objective-file PATH|- or --objective MARKDOWN containing the complete standalone Job context');
370
366
  }
371
- const auth = await githubAuthContext(args);
372
367
  if (action === 'list') {
373
- console.log(JSON.stringify(await listManagedGitHubJobs(auth.client, repo, auth.trustedAuthors), null, 2));
368
+ console.log(JSON.stringify(await relayJobList(args), null, 2));
374
369
  return;
375
370
  }
376
371
  const id = arg(args, '--id');
377
372
  if (!id)
378
- throw new Error('--id is required for managed GitHub jobs');
373
+ throw new Error('--id is required for managed local Jobs');
379
374
  const prValue = arg(args, '--pr');
380
- const prNumber = prValue ? Number(prValue) : undefined;
375
+ const prNumber = prValue ? Number(prValue) : 0;
381
376
  if (prValue && (!Number.isInteger(prNumber) || (prNumber ?? 0) <= 0))
382
377
  throw new Error('--pr must be a positive integer');
383
378
  if (action === 'create') {
384
- if (!prNumber && !arg(args, '--head'))
385
- throw new Error('job create requires either --head or --pr');
386
379
  const objective = createObjective ?? '';
387
- const result = await ensureManagedGitHubJob(auth.client, repo, auth.trustedAuthors, {
388
- id,
389
- title: arg(args, '--title') || undefined,
390
- objective: objective || undefined,
391
- projectPath: arg(args, '--project-path') || undefined,
392
- priority: priority(args),
393
- executionMode: requestedExecutionMode(args),
394
- schedule: scheduleFromArgs(args),
395
- tag: jobTagFromArgs(args),
396
- reviewEnabled: !hasArg(args, '--no-review'),
397
- continuation: continuation(args),
398
- ...(hasArg(args, '--reviewer-thread-id') ? { reviewerThreadId: arg(args, '--reviewer-thread-id') } : {}),
399
- ...(hasArg(args, '--creator-thread-id') ? { creatorThreadId: arg(args, '--creator-thread-id') } : {}),
400
- prNumber,
401
- head: arg(args, '--head') || undefined,
402
- base: arg(args, '--base', 'main'),
403
- body: arg(args, '--body') || objective || undefined
404
- });
405
- console.log(JSON.stringify({ repository: repo, pr: result.pr.number, job: result.job.id, executionMode: result.job.executionMode, state: result.job.state }, null, 2));
406
- return;
407
- }
408
- if (!prNumber)
409
- throw new Error(`job ${action} requires --pr`);
410
- const store = new GitHubStore(auth.client, repo, prNumber, auth.trustedAuthors);
411
- const job = await store.load(id);
380
+ if (hasArg(args, '--local-merge-source') !== hasArg(args, '--local-merge-target'))
381
+ throw new Error('Supply both --local-merge-source and --local-merge-target');
382
+ const result = await relayControlCommand(args, { command: 'job.create', repository: repo, initialPlan: hasArg(args, '--initial-plan'), options: {
383
+ id, title: arg(args, '--title') || undefined, objective: objective || undefined, projectPath: arg(args, '--project-path') || undefined,
384
+ priority: priority(args), executionMode: requestedExecutionMode(args), schedule: scheduleFromArgs(args), tag: jobTagFromArgs(args), reviewEnabled: !hasArg(args, '--no-review'), continuation: continuation(args),
385
+ reviewerThreadId: hasArg(args, '--reviewer-thread-id') ? arg(args, '--reviewer-thread-id') : undefined, creatorThreadId: hasArg(args, '--creator-thread-id') ? arg(args, '--creator-thread-id') : undefined,
386
+ prNumber, head: arg(args, '--head') || undefined, base: arg(args, '--base', 'main'), body: arg(args, '--body') || objective || undefined,
387
+ localMergeSource: arg(args, '--local-merge-source') || undefined, localMergeTarget: arg(args, '--local-merge-target') || undefined,
388
+ } });
389
+ console.log(JSON.stringify(result, null, 2));
390
+ return;
391
+ }
392
+ const job = await relayControlJob(args, id);
412
393
  if (action === 'unblock') {
413
- await runJobUnblockCommand(args, store, job);
394
+ const reason = arg(args, '--reason');
395
+ if (!reason.trim())
396
+ throw new Error('job unblock requires --reason');
397
+ const updated = await relayControlCommand(args, { command: 'job.unblock', jobId: id, reason });
398
+ console.log(JSON.stringify(updated, null, 2));
414
399
  return;
415
400
  }
416
401
  if (action === 'show') {
417
402
  console.log(JSON.stringify(job, null, 2));
418
403
  return;
419
404
  }
420
- await store.validateJobBinding(id);
405
+ const boundPrNumber = job.externalPrNumber ?? (job.prNumber > 0 ? job.prNumber : 0);
406
+ if (job.repository !== repo || (prValue && boundPrNumber !== prNumber)) {
407
+ throw new Error('Local Job/PR binding mismatch');
408
+ }
421
409
  if (action === 'merge') {
422
- if (job.state === 'CANCELLED')
423
- throw new Error(`Cannot merge cancelled job ${job.id}`);
424
- const headSha = await store.headSha();
425
- await store.mergePullRequest(headSha ?? undefined);
426
- await transitionJobEvent(job, 'pr.merged');
427
- await store.saveJob(job);
428
- const resolved = await store.resolveBlockerDependents(id, 'merge');
429
- await withEventBus(args, bus => publishDependentUnblocked(bus, resolved.unblocked, `Blocker Job ${id} resolved (merge)`));
430
- console.log(JSON.stringify(await store.load(id), null, 2));
431
- return;
410
+ throw new Error('GitHub is audit-only. Configure local merge source/target and reconcile the local Job instead.');
432
411
  }
433
412
  if (action === 'cancel') {
434
- await applyJobCancelAction(store, job);
435
- await store.closePullRequest();
436
- console.log(JSON.stringify(await store.load(id), null, 2));
413
+ const updated = await relayJobStateCommand(args, job.id, 'CANCELLED');
414
+ console.log(JSON.stringify(updated, null, 2));
437
415
  return;
438
416
  }
439
417
  if (hasArg(args, '--title'))
@@ -446,6 +424,8 @@ async function runJobCommand(action, args) {
446
424
  }
447
425
  if (hasArg(args, '--project-path'))
448
426
  job.projectPath = arg(args, '--project-path') || undefined;
427
+ if (hasArg(args, '--local-merge-source') || hasArg(args, '--local-merge-target'))
428
+ configureLocalGitMerge(job, arg(args, '--local-merge-source'), arg(args, '--local-merge-target'), noGitHubMode(args));
449
429
  if (hasArg(args, '--priority'))
450
430
  job.priority = priority(args, job.priority ?? 'P2');
451
431
  if (hasArg(args, '--mode') || hasArg(args, '--execution-mode'))
@@ -476,12 +456,13 @@ async function runJobCommand(action, args) {
476
456
  throw new Error('--continue-kind and --continue-target must be supplied together');
477
457
  job.continuation = next;
478
458
  }
479
- job.updatedAt = new Date().toISOString();
480
- await store.saveJob(job);
481
- console.log(JSON.stringify(await store.load(id), null, 2));
459
+ const patch = {};
460
+ for (const key of ['title', 'objective', 'projectPath', 'priority', 'executionMode', 'schedule', 'tag', 'autoMerge', 'reviewEnabled', 'reviewerThreadId', 'creatorThreadId', 'continuation', 'localMerge'])
461
+ patch[key] = job[key];
462
+ console.log(JSON.stringify(await relayControlCommand(args, { command: 'job.update', jobId: id, patch }), null, 2));
482
463
  }
483
464
  export function runtimePlanner(args) { const plannerCommand = arg([...args], '--planner-command'); const modelRuntime = arg([...args], '--codex', 'codex'); return plannerCommand ? new CommandObjectivePlanner(plannerCommand) : new AgentObjectivePlanner(modelRuntime); }
484
- 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, { 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(job.repository, arg(args, '--project-path'), arg(args, '--workspace-root') || defaultWorkspaceRoot()), job.projectPath) }); }
465
+ 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); } }); }
485
466
  export function dashboardJobAction(store, reconciler, action) {
486
467
  return async (jobId) => {
487
468
  const job = await store.load(jobId);
@@ -491,7 +472,6 @@ export function dashboardJobAction(store, reconciler, action) {
491
472
  }
492
473
  else if (action === 'cancel') {
493
474
  await applyJobCancelAction(store, job);
494
- await store.closePullRequest?.();
495
475
  return await store.load(job.id);
496
476
  }
497
477
  return await reconciler.wake(job.id);
@@ -505,17 +485,34 @@ export async function createService(args) {
505
485
  const usageWindowDays = Number.isInteger(requestedUsageDays) && requestedUsageDays > 0 ? requestedUsageDays : 1;
506
486
  const usageRegistry = codexAndZaiUsageRegistry(undefined, usageWindowDays);
507
487
  const reconciler = runtime(loaded.store, args, bus, usageRegistry);
508
- const repo = arg(args, '--repo');
488
+ const repo = noGitHubMode(args) ? repository(args) : arg(args, '--repo');
509
489
  let overviewProvider;
510
490
  if (repo) {
511
- const overviewAuth = await githubAuthContext(args);
512
- overviewProvider = async () => dashboardManagedJobs(await listManagedGitHubJobs(overviewAuth.client, repo, overviewAuth.trustedAuthors));
491
+ overviewProvider = async () => {
492
+ const snapshots = await discoverLocalRelayStoreSnapshots(join(homedir(), '.agents-relay', 'stores'));
493
+ return dashboardManagedJobs(snapshots.flatMap(snapshot => snapshot.jobs
494
+ .filter(job => job.repository === repo)
495
+ .map(job => ({
496
+ job, githubState: snapshot.facts?.state ?? 'UNKNOWN', draft: false,
497
+ mergedAt: null, closedAt: null, publication: snapshot.publications[job.id]?.latest ?? null, publicationFailure: snapshot.publications[job.id]?.failure ?? null,
498
+ }))));
499
+ };
513
500
  }
514
501
  const webhookSecret = arg(args, '--github-webhook-secret') || process.env.AGENTS_RELAY_GITHUB_WEBHOOK_SECRET || '';
515
- const webhook = repo && webhookSecret ? { secret: webhookSecret, repositories: new Set([repo]), pullRequest: loaded.job.prNumber, wake: (event) => { const wakeEvent = eventFor(loaded.job.id, 'github', null, 'github.webhook', 'queued', 'GitHub webhook ' + event.event + (event.action ? ':' + event.action : ''), 'orchestrator', { deliveryId: event.deliveryId, event: event.event, action: event.action, repository: event.repository, prNumber: event.pullRequest }); if (bus)
516
- void bus.publish(wakeEvent);
517
- else
518
- void reconciler.reconcile(loaded.job.id); } } : undefined;
502
+ const webhook = !noGitHubMode(args) && repo && webhookSecret ? {
503
+ secret: webhookSecret,
504
+ repositories: new Set([repo]),
505
+ pullRequest: loaded.job.externalPrNumber ?? loaded.job.prNumber,
506
+ jobId: loaded.job.id,
507
+ wake: async (event) => {
508
+ if (event.pullRequestFacts && loaded.store.applyPullRequestFacts) {
509
+ await loaded.store.applyPullRequestFacts({
510
+ ...event.pullRequestFacts, observedAt: event.pullRequestFacts.observedAt ?? new Date().toISOString(),
511
+ });
512
+ }
513
+ await handleServicePublication(await loaded.store.load(loaded.job.id), event, bus);
514
+ },
515
+ } : undefined;
519
516
  const dashboardServer = serveDashboard(loaded.store, Number(arg(args, '--port', String(SERVICE_DEFAULTS.port))), bus, Number(arg(args, '--refresh-ms', String(SERVICE_DEFAULTS.dashboardRefreshMs))), loaded.job.id, usageRegistry, overviewProvider, webhook, undefined, undefined, undefined, {
520
517
  start: dashboardJobAction(loaded.store, reconciler, 'start'),
521
518
  reconcile: dashboardJobAction(loaded.store, reconciler, 'reconcile'),
@@ -529,6 +526,7 @@ export async function createService(args) {
529
526
  const stopWatcher = await reconciler.watch(loaded.job.id);
530
527
  const watchdog = setInterval(() => void reconciler.reconcile(loaded.job.id), Number(arg(args, '--interval', String(SERVICE_DEFAULTS.watchdogMs))));
531
528
  watchdog.unref();
529
+ const stopExternalActions = startServiceExternalActionFlusher(loaded.store, reconciler, loaded.job.id, Number(arg(args, '--projection-ms', String(SERVICE_DEFAULTS.projectionFlushMs))));
532
530
  let stopping = null;
533
531
  const service = {
534
532
  store: loaded.store,
@@ -540,6 +538,7 @@ export async function createService(args) {
540
538
  if (stopping)
541
539
  return stopping;
542
540
  clearInterval(watchdog);
541
+ stopExternalActions();
543
542
  service.watchdogCleared = true;
544
543
  const serverClosed = new Promise((resolve, reject) => { dashboardServer.close(error => error ? reject(error) : resolve()); });
545
544
  dashboardServer.closeAllConnections();
@@ -576,7 +575,7 @@ export function isEntrypoint(moduleUrl) {
576
575
  }
577
576
  }
578
577
  async function main() {
579
- const [command, ...args] = process.argv.slice(2);
578
+ const [command, ...args] = process.argv.slice(2).filter(value => value !== '--no-github');
580
579
  if (!command || command === '--help' || command === '-h') {
581
580
  console.log(helpText());
582
581
  return;
@@ -620,19 +619,18 @@ async function main() {
620
619
  return;
621
620
  }
622
621
  if (command === 'init') {
622
+ const file = arg(args, '--file');
623
623
  const now = new Date().toISOString();
624
624
  const initialSchedule = scheduleFromArgs(args);
625
- 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) });
626
- let store;
627
- let file = null;
628
- if (arg(args, '--repo') && arg(args, '--pr'))
629
- store = await githubStore(args);
630
- else {
631
- file = arg(args, '--file');
632
- if (!file)
633
- throw new Error('init requires --repo/--pr or explicit --file demo mode');
634
- store = new InMemoryStore(job);
625
+ const id = arg(args, '--id', randomUUID());
626
+ if (!file) {
627
+ const repo = repository(args);
628
+ 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 } });
629
+ console.log(JSON.stringify(result, null, 2));
630
+ return;
635
631
  }
632
+ 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) });
633
+ const store = new InMemoryStore(job);
636
634
  await store.saveJob(job);
637
635
  await saveLocal(file, job);
638
636
  console.log(`Initialized ${job.id}`);
@@ -655,6 +653,46 @@ async function main() {
655
653
  console.log(JSON.stringify(await response.json(), null, 2));
656
654
  return;
657
655
  }
656
+ if (!arg(args, '--file') && ['plan', 'reconcile', 'record', 'agent-register', 'agent-discover'].includes(command)) {
657
+ const jobId = arg(args, '--id');
658
+ if (!jobId)
659
+ throw new Error(`${command} requires --repo and --id`);
660
+ if (command === 'plan') {
661
+ const guidance = hasArg(args, '--prompt') || hasArg(args, '--prompt-file') ? await textOption(args, '--prompt', '--prompt-file') : undefined;
662
+ console.log(JSON.stringify(await relayControlCommand(args, { command: 'job.plan', jobId, ...(guidance?.trim() ? { guidance: guidance.trim() } : {}) }), null, 2));
663
+ return;
664
+ }
665
+ if (command === 'reconcile') {
666
+ console.log(JSON.stringify(await relayControlCommand(args, { command: 'job.reconcile', jobId }), null, 2));
667
+ return;
668
+ }
669
+ if (command === 'record') {
670
+ const now = new Date().toISOString();
671
+ const id = arg(args, '--task-id', randomUUID());
672
+ const summary = arg(args, '--summary');
673
+ if (!summary)
674
+ throw new Error('record requires --summary');
675
+ const commit = arg(args, '--commit');
676
+ 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 };
677
+ const updated = await relayControlCommand(args, { command: 'record', jobId, task });
678
+ console.log(JSON.stringify(updated.tasks.find(item => item.id === id), null, 2));
679
+ return;
680
+ }
681
+ if (command === 'agent-register') {
682
+ const now = new Date().toISOString();
683
+ 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)
684
+ 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 };
685
+ if (!agent.name || !agent.responsibility || !agent.role || !agent.endpoint.target)
686
+ throw new Error('--name, --responsibility, --role, and --endpoint are required');
687
+ console.log(JSON.stringify(await relayControlCommand(args, { command: 'agent.register', jobId, agent }), null, 2));
688
+ return;
689
+ }
690
+ if (command === 'agent-discover') {
691
+ 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') };
692
+ console.log(JSON.stringify(await relayControlCommand(args, { command: 'agent.discover', jobId, requirements }), null, 2));
693
+ return;
694
+ }
695
+ }
658
696
  const loaded = await storeFor(args);
659
697
  const { store, job, localFile } = loaded;
660
698
  if (command === 'plan') {
@@ -709,7 +747,7 @@ async function main() {
709
747
  }
710
748
  if (command === 'reconcile') {
711
749
  await withEventBus(args, async (bus) => {
712
- if (!localFile) {
750
+ if (!localFile && !noGitHubMode(args)) {
713
751
  await requestManagedReconciliation(bus, job);
714
752
  console.log(JSON.stringify(await store.load(job.id), null, 2));
715
753
  return;