agents-relay 1.0.90 → 1.0.92
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/dist/cli-help.js +192 -0
- package/dist/cli-support.js +103 -0
- package/dist/cli.js +6 -293
- package/dist/dashboard-html.js +292 -0
- package/dist/dashboard.js +2 -292
- package/dist/reconciler-review.js +158 -0
- package/dist/reconciler-runtime.js +404 -0
- package/dist/reconciler.js +66 -535
- package/dist/scheduler.js +11 -33
- package/dist/store-github.js +217 -0
- package/dist/store-task-definition.js +19 -0
- package/dist/store.js +3 -235
- package/dist/workflow/contracts.js +84 -0
- package/dist/workflow/definitions.js +19 -5
- package/dist/workflow/effects.js +74 -0
- package/dist/workflow/mermaid.js +6 -2
- package/dist/workflow/rollout.js +70 -56
- package/dist/workflow/runtime.js +39 -0
- package/dist/workflow/task-reconcile.js +48 -0
- package/package.json +1 -1
- package/workflow/job-lifecycle.mmd +10 -5
- package/workflow/task-lifecycle.mmd +12 -10
package/dist/cli-help.js
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
export const usage = 'agents-relay <job|task|context|init|record|agent-register|agent-discover|reconcile|tick|serve> [options]';
|
|
2
|
+
const help = {
|
|
3
|
+
root: `Agents Relay coordinates durable asynchronous agent jobs through GitHub pull requests.
|
|
4
|
+
|
|
5
|
+
Usage:
|
|
6
|
+
npx agents-relay <command> [options]
|
|
7
|
+
|
|
8
|
+
Commands:
|
|
9
|
+
job create|update|show|list|merge|cancel Manage durable GitHub-backed jobs
|
|
10
|
+
task create|update|show|list|complete|retry|cancel Manage durable tasks
|
|
11
|
+
context resolve Resolve shared project instructions, skills, agents, and runtimes
|
|
12
|
+
init Create a local demo job or initialize a GitHub job
|
|
13
|
+
record Backfill a completed task into durable history
|
|
14
|
+
agent-register Register an agent role and its capabilities
|
|
15
|
+
agent-discover Find registered agents matching hard filters
|
|
16
|
+
reconcile Run reconciliation once
|
|
17
|
+
tick Run the indexed autonomous scheduler heartbeat
|
|
18
|
+
serve Run the dashboard and reconciliation service
|
|
19
|
+
|
|
20
|
+
Run 'npx agents-relay <command> --help' for command options and examples.
|
|
21
|
+
Use 'node dist/cli.js' only for explicit local development/demo mode.`,
|
|
22
|
+
job: `Manage GitHub-backed durable jobs.
|
|
23
|
+
|
|
24
|
+
Usage:
|
|
25
|
+
npx agents-relay job <create|update|show|list|merge|cancel> [options]
|
|
26
|
+
|
|
27
|
+
Actions:
|
|
28
|
+
create Create a durable Job, optionally binding an existing unmanaged PR
|
|
29
|
+
update Partially update mutable metadata on a healthy existing Job
|
|
30
|
+
show Show one durable Job
|
|
31
|
+
list List managed Jobs in a repository
|
|
32
|
+
merge Merge the backing PR and complete the Job
|
|
33
|
+
cancel Close the backing PR and cancel the Job
|
|
34
|
+
|
|
35
|
+
Common options:
|
|
36
|
+
--repo OWNER/REPO GitHub repository
|
|
37
|
+
--id JOB_ID Durable job identifier
|
|
38
|
+
--pr NUMBER Existing PR number
|
|
39
|
+
|
|
40
|
+
Example:
|
|
41
|
+
npx agents-relay job show --repo OWNER/REPO --pr 12 --id job-1`,
|
|
42
|
+
'job create': `Create a durable Job and its trusted marker.
|
|
43
|
+
|
|
44
|
+
Required: --repo OWNER/REPO, --id JOB_ID, and either --head BRANCH or --pr NUMBER.
|
|
45
|
+
With --head, reuse/create the PR for that head. With --pr, bind an existing unmanaged PR.
|
|
46
|
+
Common: --base BRANCH (default: main), --title TEXT, --objective-file PATH|- (preferred), --objective MARKDOWN (short/simple only), --priority P0|P1|P2|P3, --mode fixed|autonomous, --no-auto-merge, --no-review. --objective and --objective-file are mutually exclusive. For a new --head Job, one is required and must contain enough standalone context to complete the Job without the originating chat/thread. Objective Markdown is also the default PR body; --body is an explicit override.
|
|
47
|
+
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.
|
|
48
|
+
|
|
49
|
+
Example:
|
|
50
|
+
npx agents-relay job create --repo OWNER/REPO --head feat/example --base main --id job-1 --title "Objective" --objective-file job.md`,
|
|
51
|
+
'job update': `Partially update mutable metadata on a healthy existing Job.
|
|
52
|
+
|
|
53
|
+
Required: --repo OWNER/REPO, --pr NUMBER, and --id JOB_ID.
|
|
54
|
+
Mutable: --title, --objective-file PATH|- (preferred) or --objective MARKDOWN, --priority, --mode fixed|autonomous, --schedule, --auto-merge|--no-auto-merge, --review|--no-review, and continuation options. Updating the objective also updates the PR body.
|
|
55
|
+
Unspecified fields are preserved. Marker/binding corruption is never repaired implicitly; update fails with a precise diagnostic instead.
|
|
56
|
+
|
|
57
|
+
Example:
|
|
58
|
+
npx agents-relay job update --repo OWNER/REPO --pr 12 --id job-1 --priority P0 --no-auto-merge`,
|
|
59
|
+
'job show': `Show one durable Job.
|
|
60
|
+
|
|
61
|
+
Required: --repo OWNER/REPO, --pr NUMBER, and --id JOB_ID.
|
|
62
|
+
|
|
63
|
+
Example:
|
|
64
|
+
npx agents-relay job show --repo OWNER/REPO --pr 12 --id job-1`,
|
|
65
|
+
'job list': `List managed Jobs in a repository.
|
|
66
|
+
|
|
67
|
+
Required: --repo OWNER/REPO.
|
|
68
|
+
|
|
69
|
+
Example:
|
|
70
|
+
npx agents-relay job list --repo OWNER/REPO`,
|
|
71
|
+
'job merge': `Merge the backing pull request and complete the durable Job.
|
|
72
|
+
|
|
73
|
+
Required: --repo OWNER/REPO, --pr NUMBER, and --id JOB_ID. The current PR head SHA is pinned into the merge request.`,
|
|
74
|
+
'job cancel': `Cancel the durable Job and close its backing pull request.
|
|
75
|
+
|
|
76
|
+
Required: --repo OWNER/REPO, --pr NUMBER, and --id JOB_ID. Nonterminal Tasks are cancelled before the PR is closed.`,
|
|
77
|
+
task: `Manage durable tasks.
|
|
78
|
+
|
|
79
|
+
Usage:
|
|
80
|
+
npx agents-relay task <create|update|show|list|retry|cancel> [options]
|
|
81
|
+
|
|
82
|
+
Actions:
|
|
83
|
+
create Create one durable Task under an existing Job
|
|
84
|
+
update Update mutable Task fields
|
|
85
|
+
show Show one Task
|
|
86
|
+
list List Tasks for a Job
|
|
87
|
+
retry Retry a failed or blocked Task
|
|
88
|
+
complete Complete an orchestrator-owned Task
|
|
89
|
+
cancel Cancel a Task`,
|
|
90
|
+
'task create': `Create one durable Task under an existing Job.
|
|
91
|
+
|
|
92
|
+
Required: normal Job storage options plus --input-file PATH|- (preferred) or --input MARKDOWN, and --output task-pr|file. --input and --input-file are mutually exclusive. Codex/ChatGPT tasks also require --model MODEL.
|
|
93
|
+
Common: --task-id ID, --adapter codex|chatgpt|orchestrator, --priority P0|P1|P2|P3, --story-size S|M|L, --agent NAME, --agent-graph-file PATH|-, --deps a,b, --parent ID, --draft.\nTasks with dependencies start DRAFT; tasks without dependencies start QUEUED unless --draft is given.
|
|
94
|
+
Directly-created Tasks are not wrapped in a synthetic Plan. Planner-generated Tasks are created internally by Relay from planner output.
|
|
95
|
+
|
|
96
|
+
Example:
|
|
97
|
+
npx agents-relay task create --repo OWNER/REPO --pr 12 --id job-1 --task-id build --adapter codex --provider openai --model MODEL --output task-pr --input-file task.md`,
|
|
98
|
+
'task update': `Update mutable durable Task fields in place.
|
|
99
|
+
|
|
100
|
+
Required: --task-id ID and the normal Job storage options.
|
|
101
|
+
Mutable while not RUNNING/SUCCEEDED/CANCELLED: --adapter, --priority, --story-size, --input-file PATH|- or --input MARKDOWN, --project, --agent, --agent-graph-file PATH|-, --parent, --deps, --capabilities, --timeout, --max-attempts, --provider, --model, --profile, --reasoning, --cwd, --chatgpt-project, --chatgpt-tab-close-policy, --draft, --queue.
|
|
102
|
+
Use --clear-routing to remove routing metadata.
|
|
103
|
+
|
|
104
|
+
Example:
|
|
105
|
+
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`,
|
|
106
|
+
'task show': `Show one durable Task.
|
|
107
|
+
|
|
108
|
+
Required: --task-id ID and the normal Job storage options.`,
|
|
109
|
+
'task list': `List durable Tasks for one Job.
|
|
110
|
+
|
|
111
|
+
Required: the normal Job storage options.`,
|
|
112
|
+
'task complete': `Complete one orchestrator-owned Task after the creating orchestrator has finished the work.
|
|
113
|
+
|
|
114
|
+
Required: --task-id ID, --summary TEXT, and the normal Job storage options. The Task must use adapter=orchestrator and be QUEUED or RUNNING. This command is for the same orchestrator turn that created the self-task; it is not a deferred handoff mechanism.`,
|
|
115
|
+
'task retry': `Retry one durable Task.
|
|
116
|
+
|
|
117
|
+
Required: --task-id ID and the normal Job storage options.`,
|
|
118
|
+
'task cancel': `Cancel one durable Task.
|
|
119
|
+
|
|
120
|
+
Required: --task-id ID and the normal Job storage options.`,
|
|
121
|
+
context: `Resolve the shared execution context used by XChat and Relay.
|
|
122
|
+
|
|
123
|
+
Usage:
|
|
124
|
+
npx agents-relay context resolve --project-path PATH --role ROLE [options]
|
|
125
|
+
|
|
126
|
+
Actions:
|
|
127
|
+
resolve Resolve instructions, visible skills/agents, and runtime/delegation metadata`,
|
|
128
|
+
'context resolve': `Resolve shared execution context without creating durable state.
|
|
129
|
+
|
|
130
|
+
Required: --project-path PATH.
|
|
131
|
+
Common: --role orchestrator|planner|reviewer|worker|nested-worker, --skills a,b, --agents a,b.
|
|
132
|
+
Root overrides for testing/configuration: --workspace-root PATH, --neo-root PATH|none, --global-root PATH.
|
|
133
|
+
|
|
134
|
+
Example:
|
|
135
|
+
npx agents-relay context resolve --project-path ~/Workspace/agents-relay --role orchestrator`,
|
|
136
|
+
record: `Backfill work completed outside the relay into durable history.
|
|
137
|
+
|
|
138
|
+
Required: --file PATH or --repo/--pr/--id, plus --summary TEXT.
|
|
139
|
+
Common: --task-id ID, --commit SHA, --project NAME.
|
|
140
|
+
|
|
141
|
+
Example:
|
|
142
|
+
npx agents-relay record --file .agents-relay.json --summary "Built release" --commit abc123`,
|
|
143
|
+
reconcile: `Reconcile a job once, launching ready tasks and persisting results.
|
|
144
|
+
|
|
145
|
+
Required: --file PATH for local demo mode, or --repo OWNER/REPO --pr NUMBER --id JOB_ID.
|
|
146
|
+
|
|
147
|
+
Example:
|
|
148
|
+
npx agents-relay reconcile --repo OWNER/REPO --pr 12 --id job-1`,
|
|
149
|
+
tick: `Process autonomous jobs indexed into current/recent GitHub next-run buckets.
|
|
150
|
+
|
|
151
|
+
Required: --repo OWNER/REPO.
|
|
152
|
+
Common: --lookback-buckets N (default 3).
|
|
153
|
+
|
|
154
|
+
Example:
|
|
155
|
+
npx agents-relay tick --repo OWNER/REPO`,
|
|
156
|
+
init: `Initialize a new job. GitHub mode requires --repo and --pr; local mode requires explicit --file.
|
|
157
|
+
|
|
158
|
+
Common: --id ID, --title TEXT, --objective TEXT, --mode fixed|autonomous, --priority P0|P1|P2|P3.
|
|
159
|
+
|
|
160
|
+
Example:
|
|
161
|
+
npx agents-relay init --repo OWNER/REPO --pr 12 --id job-1 --title "Objective"`,
|
|
162
|
+
serve: `Run the dashboard and reconciliation service for a managed job.
|
|
163
|
+
|
|
164
|
+
Required: --file PATH for local demo mode, or --repo OWNER/REPO --pr NUMBER --id JOB_ID.
|
|
165
|
+
Common: --port PORT, --interval MS, --events nats.
|
|
166
|
+
|
|
167
|
+
Example:
|
|
168
|
+
npx agents-relay serve --repo OWNER/REPO --pr 12 --id job-1`,
|
|
169
|
+
'agent-register': `Register an agent role and its execution endpoint.
|
|
170
|
+
|
|
171
|
+
Required: --file PATH or managed GitHub options, plus --name NAME --responsibility TEXT --role ROLE --endpoint TARGET.
|
|
172
|
+
Common: --capabilities a,b, --runtime shell|codex|chatgpt, --provider NAME, --model NAME.
|
|
173
|
+
|
|
174
|
+
Example:
|
|
175
|
+
npx agents-relay agent-register --file .agents-relay.json --name builder --responsibility "Build code" --role engineer --endpoint shell`,
|
|
176
|
+
'agent-discover': `Find registered agents using hard filters.
|
|
177
|
+
|
|
178
|
+
Required: --file PATH or managed GitHub options.
|
|
179
|
+
Common: --capabilities a,b, --runtime NAME, --availability available,busy, --task-kind NAME, --explore.
|
|
180
|
+
|
|
181
|
+
Example:
|
|
182
|
+
npx agents-relay agent-discover --file .agents-relay.json --capabilities shell`,
|
|
183
|
+
};
|
|
184
|
+
export function helpText(command, action) {
|
|
185
|
+
if (command === 'job')
|
|
186
|
+
return action ? help[`job ${action}`] ?? help.job : help.job;
|
|
187
|
+
if (command === 'task')
|
|
188
|
+
return action ? help[`task ${action}`] ?? help.task : help.task;
|
|
189
|
+
if (command === 'context')
|
|
190
|
+
return action ? help[`context ${action}`] ?? help.context : help.context;
|
|
191
|
+
return command ? help[command] ?? help.root : help.root;
|
|
192
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { InMemoryStore, GitHubStore } from './store.js';
|
|
3
|
+
import { githubAuthContext } from './github-auth.js';
|
|
4
|
+
import { NatsEventBus, eventFor } from './events.js';
|
|
5
|
+
import { parseJobSchedule } from './scheduler.js';
|
|
6
|
+
export const SERVICE_DEFAULTS = { port: 8787, watchdogMs: 300000, webhookWatchdogMs: 1800000, dashboardRefreshMs: 300000 };
|
|
7
|
+
export class ServiceEventBus {
|
|
8
|
+
upstream;
|
|
9
|
+
unsubscribers = new Set();
|
|
10
|
+
constructor(upstream) {
|
|
11
|
+
this.upstream = upstream;
|
|
12
|
+
}
|
|
13
|
+
async publish(event) { await this.upstream.publish(event); }
|
|
14
|
+
async subscribe(jobId, wake) {
|
|
15
|
+
const unsubscribe = await this.upstream.subscribe(jobId, wake);
|
|
16
|
+
this.unsubscribers.add(unsubscribe);
|
|
17
|
+
return async () => { this.unsubscribers.delete(unsubscribe); await unsubscribe(); };
|
|
18
|
+
}
|
|
19
|
+
async subscribeAll(wake) {
|
|
20
|
+
const unsubscribe = await this.upstream.subscribeAll(wake);
|
|
21
|
+
this.unsubscribers.add(unsubscribe);
|
|
22
|
+
return async () => { this.unsubscribers.delete(unsubscribe); await unsubscribe(); };
|
|
23
|
+
}
|
|
24
|
+
async close() {
|
|
25
|
+
const pending = [...this.unsubscribers].map(unsubscribe => unsubscribe());
|
|
26
|
+
this.unsubscribers.clear();
|
|
27
|
+
await Promise.all(pending);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export function arg(args, name, fallback = '') { const index = args.indexOf(name); return index >= 0 ? args[index + 1] ?? fallback : fallback; }
|
|
31
|
+
export function hasArg(args, name) { return args.includes(name); }
|
|
32
|
+
async function readStdinText() {
|
|
33
|
+
const chunks = [];
|
|
34
|
+
for await (const chunk of process.stdin)
|
|
35
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));
|
|
36
|
+
return Buffer.concat(chunks).toString('utf8');
|
|
37
|
+
}
|
|
38
|
+
export async function textOption(args, inlineFlag, fileFlag) {
|
|
39
|
+
if (hasArg(args, inlineFlag) && hasArg(args, fileFlag))
|
|
40
|
+
throw new Error(`${inlineFlag} and ${fileFlag} are mutually exclusive`);
|
|
41
|
+
if (hasArg(args, fileFlag)) {
|
|
42
|
+
const path = arg(args, fileFlag);
|
|
43
|
+
if (!path)
|
|
44
|
+
throw new Error(`${fileFlag} requires a path or - for stdin`);
|
|
45
|
+
return path === '-' ? readStdinText() : readFile(path, 'utf8');
|
|
46
|
+
}
|
|
47
|
+
return arg(args, inlineFlag);
|
|
48
|
+
}
|
|
49
|
+
export function integerArg(args, name, fallback) { if (!hasArg(args, name))
|
|
50
|
+
return fallback; const value = Number(arg(args, name)); if (!Number.isInteger(value) || value < 0)
|
|
51
|
+
throw new Error(`${name} must be a non-negative integer`); return value; }
|
|
52
|
+
export function repository(args) { const value = arg(args, '--repo'); if (!value)
|
|
53
|
+
throw new Error('--repo is required for GitHub-backed operation'); return value; }
|
|
54
|
+
export function pullRequest(args) { const value = Number(arg(args, '--pr')); if (!Number.isInteger(value) || value <= 0)
|
|
55
|
+
throw new Error('--pr is required for GitHub-backed operation'); return value; }
|
|
56
|
+
async function localStore(file) { const parsed = JSON.parse(await readFile(file, 'utf8')); const store = new InMemoryStore(parsed); return { store, job: await store.load(parsed.id) }; }
|
|
57
|
+
export async function githubStore(args) { const auth = await githubAuthContext(args); return new GitHubStore(auth.client, repository(args), pullRequest(args), auth.trustedAuthors); }
|
|
58
|
+
export async function storeFor(args) { const repo = arg(args, '--repo'); const pr = arg(args, '--pr'); if (repo && pr) {
|
|
59
|
+
const store = await githubStore(args);
|
|
60
|
+
const jobId = arg(args, '--id');
|
|
61
|
+
if (!jobId)
|
|
62
|
+
throw new Error('--id is required for GitHub-backed operation');
|
|
63
|
+
return { store, job: await store.load(jobId), localFile: null };
|
|
64
|
+
} const file = arg(args, '--file'); if (!file)
|
|
65
|
+
throw new Error('Local mode is demo/test only; pass --file explicitly'); const loaded = await localStore(file); return { store: loaded.store, job: loaded.job, localFile: file }; }
|
|
66
|
+
export async function saveLocal(file, job) { if (file)
|
|
67
|
+
await writeFile(file, JSON.stringify(job, null, 2)); }
|
|
68
|
+
export function eventBus(args) { if (arg(args, '--events') !== 'nats')
|
|
69
|
+
return undefined; return new NatsEventBus(arg(args, '--nats-url', 'nats://127.0.0.1:4222'), arg(args, '--subject-prefix', 'neo.events.job')); }
|
|
70
|
+
export async function publishWake(bus, job, taskId, message) { if (!bus)
|
|
71
|
+
return; try {
|
|
72
|
+
await bus.publish(eventFor(job.id, taskId, null, 'job.wake', 'queued', message, 'orchestrator', { repository: job.repository, prNumber: job.prNumber }));
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
process.stderr.write(`warning: event wake unavailable: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
76
|
+
} }
|
|
77
|
+
export function priority(args, fallback = 'P2') { const value = arg(args, '--priority', fallback); if (!['P0', 'P1', 'P2', 'P3'].includes(value))
|
|
78
|
+
throw new Error('--priority must be P0, P1, P2, or P3'); return value; }
|
|
79
|
+
export function storySize(args, fallback = 'M') { const value = arg(args, '--story-size', fallback).toUpperCase(); if (!['S', 'M', 'L'].includes(value))
|
|
80
|
+
throw new Error('--story-size must be S, M, or L'); return value; }
|
|
81
|
+
export function executionMode(args, fallback = 'fixed') { const value = arg(args, '--mode', arg(args, '--execution-mode', fallback)); if (value !== 'fixed' && value !== 'autonomous')
|
|
82
|
+
throw new Error('--mode must be fixed or autonomous'); return value; }
|
|
83
|
+
export function chatGptTabClosePolicy(args) {
|
|
84
|
+
const value = arg(args, '--chatgpt-tab-close-policy', process.env.AGENTS_RELAY_CHATGPT_TAB_CLOSE_POLICY || 'after-start');
|
|
85
|
+
if (!['after-start', 'never', 'after-terminal'].includes(value))
|
|
86
|
+
throw new Error('--chatgpt-tab-close-policy must be after-start, never, or after-terminal');
|
|
87
|
+
return value;
|
|
88
|
+
}
|
|
89
|
+
export function requestedExecutionMode(args) { const value = arg(args, '--mode', arg(args, '--execution-mode')); return value ? executionMode(args) : undefined; }
|
|
90
|
+
export function continuation(args) { const kind = arg(args, '--continue-kind'); const target = arg(args, '--continue-target'); return kind && target ? { kind: kind, target, input: arg(args, '--continue-input') || undefined } : null; }
|
|
91
|
+
export function scheduleFromArgs(args) {
|
|
92
|
+
const cron = arg(args, '--schedule');
|
|
93
|
+
if (!cron)
|
|
94
|
+
return undefined;
|
|
95
|
+
const nextRunAt = arg(args, '--schedule-next-run') || undefined;
|
|
96
|
+
return parseJobSchedule({
|
|
97
|
+
cron,
|
|
98
|
+
timezone: arg(args, '--schedule-timezone', 'UTC'),
|
|
99
|
+
nextRunAt,
|
|
100
|
+
missedRunPolicy: arg(args, '--missed-run-policy', 'skip'),
|
|
101
|
+
concurrencyPolicy: arg(args, '--schedule-concurrency', 'skip'),
|
|
102
|
+
});
|
|
103
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { readFile, writeFile } from 'node:fs/promises';
|
|
3
2
|
import { realpathSync } from 'node:fs';
|
|
4
3
|
import { once } from 'node:events';
|
|
5
4
|
import { fileURLToPath } from 'node:url';
|
|
@@ -10,305 +9,19 @@ import { taskOverallTimeoutMs } from './types.js';
|
|
|
10
9
|
import { Reconciler } from './reconciler.js';
|
|
11
10
|
import { CodexAdapter, ChatGptAdapter } from './adapters.js';
|
|
12
11
|
import { CodexThreadContinuation, CommandContinuation, WebhookContinuation } from './continuation.js';
|
|
13
|
-
import {
|
|
12
|
+
import { eventFor } from './events.js';
|
|
14
13
|
import { serveDashboard } from './dashboard.js';
|
|
15
14
|
import { codexAndZaiUsageRegistry } from './usage.js';
|
|
16
15
|
import { discoverAgents } from './registry.js';
|
|
17
16
|
import { AgentObjectivePlanner, CommandObjectivePlanner } from './planner.js';
|
|
18
|
-
import { extendRetryBudget,
|
|
17
|
+
import { extendRetryBudget, transitionJobEvent, transitionTaskEvent } from './scheduler.js';
|
|
19
18
|
import { dueBuckets, processDueScheduledJobs, syncScheduleLabels } from './scheduled-jobs.js';
|
|
20
19
|
import { resolveExecutionContext } from './execution-context.js';
|
|
21
20
|
import { defaultWorkspaceRoot, resolveManagedProjectPath } from './workspace.js';
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
Usage:
|
|
27
|
-
npx agents-relay <command> [options]
|
|
28
|
-
|
|
29
|
-
Commands:
|
|
30
|
-
job create|update|show|list|merge|cancel Manage durable GitHub-backed jobs
|
|
31
|
-
task create|update|show|list|complete|retry|cancel Manage durable tasks
|
|
32
|
-
context resolve Resolve shared project instructions, skills, agents, and runtimes
|
|
33
|
-
init Create a local demo job or initialize a GitHub job
|
|
34
|
-
record Backfill a completed task into durable history
|
|
35
|
-
agent-register Register an agent role and its capabilities
|
|
36
|
-
agent-discover Find registered agents matching hard filters
|
|
37
|
-
reconcile Run reconciliation once
|
|
38
|
-
tick Run the indexed autonomous scheduler heartbeat
|
|
39
|
-
serve Run the dashboard and reconciliation service
|
|
40
|
-
|
|
41
|
-
Run 'npx agents-relay <command> --help' for command options and examples.
|
|
42
|
-
Use 'node dist/cli.js' only for explicit local development/demo mode.`,
|
|
43
|
-
job: `Manage GitHub-backed durable jobs.
|
|
44
|
-
|
|
45
|
-
Usage:
|
|
46
|
-
npx agents-relay job <create|update|show|list|merge|cancel> [options]
|
|
47
|
-
|
|
48
|
-
Actions:
|
|
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
|
|
53
|
-
merge Merge the backing PR and complete the Job
|
|
54
|
-
cancel Close the backing PR and cancel the Job
|
|
55
|
-
|
|
56
|
-
Common options:
|
|
57
|
-
--repo OWNER/REPO GitHub repository
|
|
58
|
-
--id JOB_ID Durable job identifier
|
|
59
|
-
--pr NUMBER Existing PR number
|
|
60
|
-
|
|
61
|
-
Example:
|
|
62
|
-
npx agents-relay job show --repo OWNER/REPO --pr 12 --id job-1`,
|
|
63
|
-
'job create': `Create a durable Job and its trusted marker.
|
|
64
|
-
|
|
65
|
-
Required: --repo OWNER/REPO, --id JOB_ID, and either --head BRANCH or --pr NUMBER.
|
|
66
|
-
With --head, reuse/create the PR for that head. With --pr, bind an existing unmanaged PR.
|
|
67
|
-
Common: --base BRANCH (default: main), --title TEXT, --objective-file PATH|- (preferred), --objective MARKDOWN (short/simple only), --priority P0|P1|P2|P3, --mode fixed|autonomous, --no-auto-merge, --no-review. --objective and --objective-file are mutually exclusive. For a new --head Job, one is required and must contain enough standalone context to complete the Job without the originating chat/thread. Objective Markdown is also the default PR body; --body is an explicit override.
|
|
68
|
-
Job creation never creates executable tasks. The orchestrator decides the initial task batch separately. Re-running create against an already healthy Job is idempotent and does not mutate its metadata.
|
|
69
|
-
|
|
70
|
-
Example:
|
|
71
|
-
npx agents-relay job create --repo OWNER/REPO --head feat/example --base main --id job-1 --title "Objective" --objective-file job.md`,
|
|
72
|
-
'job update': `Partially update mutable metadata on a healthy existing Job.
|
|
73
|
-
|
|
74
|
-
Required: --repo OWNER/REPO, --pr NUMBER, and --id JOB_ID.
|
|
75
|
-
Mutable: --title, --objective-file PATH|- (preferred) or --objective MARKDOWN, --priority, --mode fixed|autonomous, --schedule, --auto-merge|--no-auto-merge, --review|--no-review, and continuation options. Updating the objective also updates the PR body.
|
|
76
|
-
Unspecified fields are preserved. Marker/binding corruption is never repaired implicitly; update fails with a precise diagnostic instead.
|
|
77
|
-
|
|
78
|
-
Example:
|
|
79
|
-
npx agents-relay job update --repo OWNER/REPO --pr 12 --id job-1 --priority P0 --no-auto-merge`,
|
|
80
|
-
'job show': `Show one durable Job.
|
|
81
|
-
|
|
82
|
-
Required: --repo OWNER/REPO, --pr NUMBER, and --id JOB_ID.
|
|
83
|
-
|
|
84
|
-
Example:
|
|
85
|
-
npx agents-relay job show --repo OWNER/REPO --pr 12 --id job-1`,
|
|
86
|
-
'job list': `List managed Jobs in a repository.
|
|
87
|
-
|
|
88
|
-
Required: --repo OWNER/REPO.
|
|
89
|
-
|
|
90
|
-
Example:
|
|
91
|
-
npx agents-relay job list --repo OWNER/REPO`,
|
|
92
|
-
'job merge': `Merge the backing pull request and complete the durable Job.
|
|
93
|
-
|
|
94
|
-
Required: --repo OWNER/REPO, --pr NUMBER, and --id JOB_ID. The current PR head SHA is pinned into the merge request.`,
|
|
95
|
-
'job cancel': `Cancel the durable Job and close its backing pull request.
|
|
96
|
-
|
|
97
|
-
Required: --repo OWNER/REPO, --pr NUMBER, and --id JOB_ID. Nonterminal Tasks are cancelled before the PR is closed.`,
|
|
98
|
-
task: `Manage durable tasks.
|
|
99
|
-
|
|
100
|
-
Usage:
|
|
101
|
-
npx agents-relay task <create|update|show|list|retry|cancel> [options]
|
|
102
|
-
|
|
103
|
-
Actions:
|
|
104
|
-
create Create one durable Task under an existing Job
|
|
105
|
-
update Update mutable Task fields
|
|
106
|
-
show Show one Task
|
|
107
|
-
list List Tasks for a Job
|
|
108
|
-
retry Retry a failed or blocked Task
|
|
109
|
-
complete Complete an orchestrator-owned Task
|
|
110
|
-
cancel Cancel a Task`,
|
|
111
|
-
'task create': `Create one durable Task under an existing Job.
|
|
112
|
-
|
|
113
|
-
Required: normal Job storage options plus --input-file PATH|- (preferred) or --input MARKDOWN, and --output task-pr|file. --input and --input-file are mutually exclusive. Codex/ChatGPT tasks also require --model MODEL.
|
|
114
|
-
Common: --task-id ID, --adapter codex|chatgpt|orchestrator, --priority P0|P1|P2|P3, --story-size S|M|L, --agent NAME, --agent-graph-file PATH|-, --deps a,b, --parent ID, --draft.\nTasks with dependencies start DRAFT; tasks without dependencies start QUEUED unless --draft is given.
|
|
115
|
-
Directly-created Tasks are not wrapped in a synthetic Plan. Planner-generated Tasks are created internally by Relay from planner output.
|
|
116
|
-
|
|
117
|
-
Example:
|
|
118
|
-
npx agents-relay task create --repo OWNER/REPO --pr 12 --id job-1 --task-id build --adapter codex --provider openai --model MODEL --output task-pr --input-file task.md`,
|
|
119
|
-
'task update': `Update mutable durable Task fields in place.
|
|
120
|
-
|
|
121
|
-
Required: --task-id ID and the normal Job storage options.
|
|
122
|
-
Mutable while not RUNNING/SUCCEEDED/CANCELLED: --adapter, --priority, --story-size, --input-file PATH|- or --input MARKDOWN, --project, --agent, --agent-graph-file PATH|-, --parent, --deps, --capabilities, --timeout, --max-attempts, --provider, --model, --profile, --reasoning, --cwd, --chatgpt-project, --chatgpt-tab-close-policy, --draft, --queue.
|
|
123
|
-
Use --clear-routing to remove routing metadata.
|
|
124
|
-
|
|
125
|
-
Example:
|
|
126
|
-
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`,
|
|
127
|
-
'task show': `Show one durable Task.
|
|
128
|
-
|
|
129
|
-
Required: --task-id ID and the normal Job storage options.`,
|
|
130
|
-
'task list': `List durable Tasks for one Job.
|
|
131
|
-
|
|
132
|
-
Required: the normal Job storage options.`,
|
|
133
|
-
'task complete': `Complete one orchestrator-owned Task after the creating orchestrator has finished the work.
|
|
134
|
-
|
|
135
|
-
Required: --task-id ID, --summary TEXT, and the normal Job storage options. The Task must use adapter=orchestrator and be QUEUED or RUNNING. This command is for the same orchestrator turn that created the self-task; it is not a deferred handoff mechanism.`,
|
|
136
|
-
'task retry': `Retry one durable Task.
|
|
137
|
-
|
|
138
|
-
Required: --task-id ID and the normal Job storage options.`,
|
|
139
|
-
'task cancel': `Cancel one durable Task.
|
|
140
|
-
|
|
141
|
-
Required: --task-id ID and the normal Job storage options.`,
|
|
142
|
-
context: `Resolve the shared execution context used by XChat and Relay.
|
|
143
|
-
|
|
144
|
-
Usage:
|
|
145
|
-
npx agents-relay context resolve --project-path PATH --role ROLE [options]
|
|
146
|
-
|
|
147
|
-
Actions:
|
|
148
|
-
resolve Resolve instructions, visible skills/agents, and runtime/delegation metadata`,
|
|
149
|
-
'context resolve': `Resolve shared execution context without creating durable state.
|
|
150
|
-
|
|
151
|
-
Required: --project-path PATH.
|
|
152
|
-
Common: --role orchestrator|planner|reviewer|worker|nested-worker, --skills a,b, --agents a,b.
|
|
153
|
-
Root overrides for testing/configuration: --workspace-root PATH, --neo-root PATH|none, --global-root PATH.
|
|
154
|
-
|
|
155
|
-
Example:
|
|
156
|
-
npx agents-relay context resolve --project-path ~/Workspace/agents-relay --role orchestrator`,
|
|
157
|
-
record: `Backfill work completed outside the relay into durable history.
|
|
158
|
-
|
|
159
|
-
Required: --file PATH or --repo/--pr/--id, plus --summary TEXT.
|
|
160
|
-
Common: --task-id ID, --commit SHA, --project NAME.
|
|
161
|
-
|
|
162
|
-
Example:
|
|
163
|
-
npx agents-relay record --file .agents-relay.json --summary "Built release" --commit abc123`,
|
|
164
|
-
reconcile: `Reconcile a job once, launching ready tasks and persisting results.
|
|
165
|
-
|
|
166
|
-
Required: --file PATH for local demo mode, or --repo OWNER/REPO --pr NUMBER --id JOB_ID.
|
|
167
|
-
|
|
168
|
-
Example:
|
|
169
|
-
npx agents-relay reconcile --repo OWNER/REPO --pr 12 --id job-1`,
|
|
170
|
-
tick: `Process autonomous jobs indexed into current/recent GitHub next-run buckets.
|
|
171
|
-
|
|
172
|
-
Required: --repo OWNER/REPO.
|
|
173
|
-
Common: --lookback-buckets N (default 3).
|
|
174
|
-
|
|
175
|
-
Example:
|
|
176
|
-
npx agents-relay tick --repo OWNER/REPO`,
|
|
177
|
-
init: `Initialize a new job. GitHub mode requires --repo and --pr; local mode requires explicit --file.
|
|
178
|
-
|
|
179
|
-
Common: --id ID, --title TEXT, --objective TEXT, --mode fixed|autonomous, --priority P0|P1|P2|P3.
|
|
180
|
-
|
|
181
|
-
Example:
|
|
182
|
-
npx agents-relay init --repo OWNER/REPO --pr 12 --id job-1 --title "Objective"`,
|
|
183
|
-
serve: `Run the dashboard and reconciliation service for a managed job.
|
|
184
|
-
|
|
185
|
-
Required: --file PATH for local demo mode, or --repo OWNER/REPO --pr NUMBER --id JOB_ID.
|
|
186
|
-
Common: --port PORT, --interval MS, --events nats.
|
|
187
|
-
|
|
188
|
-
Example:
|
|
189
|
-
npx agents-relay serve --repo OWNER/REPO --pr 12 --id job-1`,
|
|
190
|
-
'agent-register': `Register an agent role and its execution endpoint.
|
|
191
|
-
|
|
192
|
-
Required: --file PATH or managed GitHub options, plus --name NAME --responsibility TEXT --role ROLE --endpoint TARGET.
|
|
193
|
-
Common: --capabilities a,b, --runtime shell|codex|chatgpt, --provider NAME, --model NAME.
|
|
194
|
-
|
|
195
|
-
Example:
|
|
196
|
-
npx agents-relay agent-register --file .agents-relay.json --name builder --responsibility "Build code" --role engineer --endpoint shell`,
|
|
197
|
-
'agent-discover': `Find registered agents using hard filters.
|
|
198
|
-
|
|
199
|
-
Required: --file PATH or managed GitHub options.
|
|
200
|
-
Common: --capabilities a,b, --runtime NAME, --availability available,busy, --task-kind NAME, --explore.
|
|
201
|
-
|
|
202
|
-
Example:
|
|
203
|
-
npx agents-relay agent-discover --file .agents-relay.json --capabilities shell`,
|
|
204
|
-
};
|
|
205
|
-
export function helpText(command, action) {
|
|
206
|
-
if (command === 'job')
|
|
207
|
-
return action ? help[`job ${action}`] ?? help.job : help.job;
|
|
208
|
-
if (command === 'task')
|
|
209
|
-
return action ? help[`task ${action}`] ?? help.task : help.task;
|
|
210
|
-
if (command === 'context')
|
|
211
|
-
return action ? help[`context ${action}`] ?? help.context : help.context;
|
|
212
|
-
return command ? help[command] ?? help.root : help.root;
|
|
213
|
-
}
|
|
214
|
-
export const SERVICE_DEFAULTS = { port: 8787, watchdogMs: 300000, webhookWatchdogMs: 1800000, dashboardRefreshMs: 300000 };
|
|
215
|
-
class ServiceEventBus {
|
|
216
|
-
upstream;
|
|
217
|
-
unsubscribers = new Set();
|
|
218
|
-
constructor(upstream) {
|
|
219
|
-
this.upstream = upstream;
|
|
220
|
-
}
|
|
221
|
-
async publish(event) { await this.upstream.publish(event); }
|
|
222
|
-
async subscribe(jobId, wake) {
|
|
223
|
-
const unsubscribe = await this.upstream.subscribe(jobId, wake);
|
|
224
|
-
this.unsubscribers.add(unsubscribe);
|
|
225
|
-
return async () => { this.unsubscribers.delete(unsubscribe); await unsubscribe(); };
|
|
226
|
-
}
|
|
227
|
-
async subscribeAll(wake) {
|
|
228
|
-
const unsubscribe = await this.upstream.subscribeAll(wake);
|
|
229
|
-
this.unsubscribers.add(unsubscribe);
|
|
230
|
-
return async () => { this.unsubscribers.delete(unsubscribe); await unsubscribe(); };
|
|
231
|
-
}
|
|
232
|
-
async close() {
|
|
233
|
-
const pending = [...this.unsubscribers].map(unsubscribe => unsubscribe());
|
|
234
|
-
this.unsubscribers.clear();
|
|
235
|
-
await Promise.all(pending);
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
function arg(args, name, fallback = '') { const index = args.indexOf(name); return index >= 0 ? args[index + 1] ?? fallback : fallback; }
|
|
239
|
-
function hasArg(args, name) { return args.includes(name); }
|
|
240
|
-
async function readStdinText() {
|
|
241
|
-
const chunks = [];
|
|
242
|
-
for await (const chunk of process.stdin)
|
|
243
|
-
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));
|
|
244
|
-
return Buffer.concat(chunks).toString('utf8');
|
|
245
|
-
}
|
|
246
|
-
async function textOption(args, inlineFlag, fileFlag) {
|
|
247
|
-
if (hasArg(args, inlineFlag) && hasArg(args, fileFlag))
|
|
248
|
-
throw new Error(`${inlineFlag} and ${fileFlag} are mutually exclusive`);
|
|
249
|
-
if (hasArg(args, fileFlag)) {
|
|
250
|
-
const path = arg(args, fileFlag);
|
|
251
|
-
if (!path)
|
|
252
|
-
throw new Error(`${fileFlag} requires a path or - for stdin`);
|
|
253
|
-
return path === '-' ? readStdinText() : readFile(path, 'utf8');
|
|
254
|
-
}
|
|
255
|
-
return arg(args, inlineFlag);
|
|
256
|
-
}
|
|
257
|
-
function integerArg(args, name, fallback) { if (!hasArg(args, name))
|
|
258
|
-
return fallback; const value = Number(arg(args, name)); if (!Number.isInteger(value) || value < 0)
|
|
259
|
-
throw new Error(`${name} must be a non-negative integer`); return value; }
|
|
260
|
-
function repository(args) { const value = arg(args, '--repo'); if (!value)
|
|
261
|
-
throw new Error('--repo is required for GitHub-backed operation'); return value; }
|
|
262
|
-
function pullRequest(args) { const value = Number(arg(args, '--pr')); if (!Number.isInteger(value) || value <= 0)
|
|
263
|
-
throw new Error('--pr is required for GitHub-backed operation'); return value; }
|
|
264
|
-
async function localStore(file) { const parsed = JSON.parse(await readFile(file, 'utf8')); const store = new InMemoryStore(parsed); return { store, job: await store.load(parsed.id) }; }
|
|
265
|
-
async function githubStore(args) { const auth = await githubAuthContext(args); return new GitHubStore(auth.client, repository(args), pullRequest(args), auth.trustedAuthors); }
|
|
266
|
-
async function storeFor(args) { const repo = arg(args, '--repo'); const pr = arg(args, '--pr'); if (repo && pr) {
|
|
267
|
-
const store = await githubStore(args);
|
|
268
|
-
const jobId = arg(args, '--id');
|
|
269
|
-
if (!jobId)
|
|
270
|
-
throw new Error('--id is required for GitHub-backed operation');
|
|
271
|
-
return { store, job: await store.load(jobId), localFile: null };
|
|
272
|
-
} const file = arg(args, '--file'); if (!file)
|
|
273
|
-
throw new Error('Local mode is demo/test only; pass --file explicitly'); const loaded = await localStore(file); return { store: loaded.store, job: loaded.job, localFile: file }; }
|
|
274
|
-
async function saveLocal(file, job) { if (file)
|
|
275
|
-
await writeFile(file, JSON.stringify(job, null, 2)); }
|
|
276
|
-
export function eventBus(args) { if (arg(args, '--events') !== 'nats')
|
|
277
|
-
return undefined; return new NatsEventBus(arg(args, '--nats-url', 'nats://127.0.0.1:4222'), arg(args, '--subject-prefix', 'neo.events.job')); }
|
|
278
|
-
async function publishWake(bus, job, taskId, message) { if (!bus)
|
|
279
|
-
return; try {
|
|
280
|
-
await bus.publish(eventFor(job.id, taskId, null, 'job.wake', 'queued', message, 'orchestrator', { repository: job.repository, prNumber: job.prNumber }));
|
|
281
|
-
}
|
|
282
|
-
catch (error) {
|
|
283
|
-
process.stderr.write(`warning: event wake unavailable: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
284
|
-
} }
|
|
285
|
-
function priority(args, fallback = 'P2') { const value = arg(args, '--priority', fallback); if (!['P0', 'P1', 'P2', 'P3'].includes(value))
|
|
286
|
-
throw new Error('--priority must be P0, P1, P2, or P3'); return value; }
|
|
287
|
-
function storySize(args, fallback = 'M') { const value = arg(args, '--story-size', fallback).toUpperCase(); if (!['S', 'M', 'L'].includes(value))
|
|
288
|
-
throw new Error('--story-size must be S, M, or L'); return value; }
|
|
289
|
-
export function executionMode(args, fallback = 'fixed') { const value = arg(args, '--mode', arg(args, '--execution-mode', fallback)); if (value !== 'fixed' && value !== 'autonomous')
|
|
290
|
-
throw new Error('--mode must be fixed or autonomous'); return value; }
|
|
291
|
-
export function chatGptTabClosePolicy(args) {
|
|
292
|
-
const value = arg(args, '--chatgpt-tab-close-policy', process.env.AGENTS_RELAY_CHATGPT_TAB_CLOSE_POLICY || 'after-start');
|
|
293
|
-
if (!['after-start', 'never', 'after-terminal'].includes(value))
|
|
294
|
-
throw new Error('--chatgpt-tab-close-policy must be after-start, never, or after-terminal');
|
|
295
|
-
return value;
|
|
296
|
-
}
|
|
297
|
-
function requestedExecutionMode(args) { const value = arg(args, '--mode', arg(args, '--execution-mode')); return value ? executionMode(args) : undefined; }
|
|
298
|
-
function continuation(args) { const kind = arg(args, '--continue-kind'); const target = arg(args, '--continue-target'); return kind && target ? { kind: kind, target, input: arg(args, '--continue-input') || undefined } : null; }
|
|
299
|
-
function scheduleFromArgs(args) {
|
|
300
|
-
const cron = arg(args, '--schedule');
|
|
301
|
-
if (!cron)
|
|
302
|
-
return undefined;
|
|
303
|
-
const nextRunAt = arg(args, '--schedule-next-run') || undefined;
|
|
304
|
-
return parseJobSchedule({
|
|
305
|
-
cron,
|
|
306
|
-
timezone: arg(args, '--schedule-timezone', 'UTC'),
|
|
307
|
-
nextRunAt,
|
|
308
|
-
missedRunPolicy: arg(args, '--missed-run-policy', 'skip'),
|
|
309
|
-
concurrencyPolicy: arg(args, '--schedule-concurrency', 'skip'),
|
|
310
|
-
});
|
|
311
|
-
}
|
|
21
|
+
import { helpText, usage } from './cli-help.js';
|
|
22
|
+
export { helpText } from './cli-help.js';
|
|
23
|
+
import { SERVICE_DEFAULTS, ServiceEventBus, arg, hasArg, textOption, integerArg, repository, storeFor, saveLocal, githubStore, eventBus, publishWake, priority, storySize, executionMode, chatGptTabClosePolicy, requestedExecutionMode, continuation, scheduleFromArgs, } from './cli-support.js';
|
|
24
|
+
export { SERVICE_DEFAULTS, eventBus, executionMode, chatGptTabClosePolicy } from './cli-support.js';
|
|
312
25
|
export async function ensureManagedGitHubJob(client, repositoryName, trustedAuthors, options) {
|
|
313
26
|
if (!options.id)
|
|
314
27
|
throw new Error('--id is required for managed GitHub jobs');
|