agents-relay 2.0.171 → 2.0.172
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/agent-content-resolver.js +69 -30
- package/dist/cli.js +15 -10
- package/dist/execution-context.js +25 -42
- package/dist/reconciler-runtime.js +1 -0
- package/dist/reconciler.js +10 -9
- package/dist/relayd-utilities.js +9 -5
- package/dist/relayd.js +23 -13
- package/dist/workspace.js +44 -1
- package/package.json +1 -1
- package/skills/agents-relay/contracts/event-contract.md +5 -3
|
@@ -95,53 +95,92 @@ function mergeSections(base, child) {
|
|
|
95
95
|
}
|
|
96
96
|
return result.trim();
|
|
97
97
|
}
|
|
98
|
-
/**
|
|
99
|
-
export async function resolveAgentMarkdown(name, builtinsRoot, contextRoot) {
|
|
98
|
+
/** Resolve the visible agent through Relay, Neo shared, then project-local layers. */
|
|
99
|
+
export async function resolveAgentMarkdown(name, builtinsRoot, contextRoot, neoRoot) {
|
|
100
100
|
if (!/^[a-zA-Z0-9_-]+$/.test(name))
|
|
101
101
|
throw new Error(`Invalid agent name ${name}`);
|
|
102
|
-
const builtin = join(builtinsRoot, `${name}.agent.md`);
|
|
103
|
-
const local = join(contextRoot, 'agents', `${name}.agent.md`);
|
|
104
|
-
const sources = [];
|
|
105
102
|
const load = async (path, root) => {
|
|
106
103
|
const canonical = await realpath(path);
|
|
107
104
|
const canonicalRoot = await realpath(root);
|
|
108
105
|
if (!(canonical === canonicalRoot || canonical.startsWith(canonicalRoot + '/')))
|
|
109
106
|
throw new Error('Agent file escapes context root');
|
|
110
|
-
const
|
|
107
|
+
const raw = await readFile(canonical, 'utf8');
|
|
108
|
+
const source = parseDocument(raw);
|
|
111
109
|
if (source.name && source.name !== name)
|
|
112
110
|
throw new Error(`Agent name mismatch ${source.name} != ${name}`);
|
|
113
|
-
|
|
114
|
-
|
|
111
|
+
return { ...source, path: canonical, metadata: Object.fromEntries(raw.match(/^---\r?\n([\s\S]*?)\r?\n---/m)?.[1].split(/\r?\n/).flatMap(line => {
|
|
112
|
+
const field = /^([A-Za-z0-9_-]+):\s*(.*)$/.exec(line);
|
|
113
|
+
return field ? [[field[1], field[2].trim().replace(/^['"]|['"]$/g, '')]] : [];
|
|
114
|
+
}) ?? []) };
|
|
115
|
+
};
|
|
116
|
+
const loadLayer = async (root) => {
|
|
117
|
+
for (const suffix of ['.agent.md', '.md']) {
|
|
118
|
+
try {
|
|
119
|
+
return await load(join(root, `${name}${suffix}`), root);
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
if (error.code !== 'ENOENT')
|
|
123
|
+
throw error;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
throw Object.assign(new Error('Agent file not found'), { code: 'ENOENT' });
|
|
115
127
|
};
|
|
116
128
|
let base;
|
|
117
129
|
try {
|
|
118
|
-
base = await
|
|
119
|
-
}
|
|
120
|
-
catch (e) {
|
|
121
|
-
if (e.code !== 'ENOENT')
|
|
122
|
-
throw e;
|
|
123
|
-
}
|
|
124
|
-
let overlay;
|
|
125
|
-
try {
|
|
126
|
-
overlay = await load(local, contextRoot + '/agents');
|
|
130
|
+
base = await loadLayer(builtinsRoot);
|
|
127
131
|
}
|
|
128
132
|
catch (e) {
|
|
129
133
|
if (e.code !== 'ENOENT')
|
|
130
134
|
throw e;
|
|
131
135
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
136
|
+
let effective = base;
|
|
137
|
+
let shared;
|
|
138
|
+
let sharedSources = [];
|
|
139
|
+
let sources = base ? [base.path] : [];
|
|
140
|
+
let metadata = { ...(base?.metadata ?? {}) };
|
|
141
|
+
for (const [layer, root] of [['neo', neoRoot], ['project', contextRoot]]) {
|
|
142
|
+
if (!root)
|
|
143
|
+
continue;
|
|
144
|
+
let overlay;
|
|
145
|
+
try {
|
|
146
|
+
overlay = await loadLayer(join(root, 'agents'));
|
|
147
|
+
}
|
|
148
|
+
catch (e) {
|
|
149
|
+
if (e.code !== 'ENOENT')
|
|
150
|
+
throw e;
|
|
151
|
+
}
|
|
152
|
+
if (!overlay)
|
|
153
|
+
continue;
|
|
154
|
+
const hasOperations = overlay.replace.length || overlay.extend.length;
|
|
155
|
+
if (hasOperations && !overlay.extendsAgent)
|
|
142
156
|
throw new Error('Section operations require extends');
|
|
143
|
-
|
|
157
|
+
if (overlay.extendsAgent && ![`relay/${name}`, `neo/${name}`].includes(overlay.extendsAgent))
|
|
158
|
+
throw new Error(`Only extends: relay/${name} or neo/${name} is supported`);
|
|
159
|
+
if (layer === 'neo' && overlay.extendsAgent === `neo/${name}`)
|
|
160
|
+
throw new Error(`Missing Neo base for agent ${name}`);
|
|
161
|
+
if (overlay.extendsAgent === `relay/${name}` && !base)
|
|
162
|
+
throw new Error(`Missing Relay base for agent ${name}`);
|
|
163
|
+
if (overlay.extendsAgent === `neo/${name}` && !shared)
|
|
164
|
+
throw new Error(`Missing Neo base for agent ${name}`);
|
|
165
|
+
if (!overlay.extendsAgent) {
|
|
166
|
+
effective = overlay;
|
|
167
|
+
metadata = {};
|
|
168
|
+
sources = [overlay.path];
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
const parent = overlay.extendsAgent === `relay/${name}` ? base : overlay.extendsAgent === `neo/${name}` ? shared : effective;
|
|
172
|
+
effective = { ...overlay, body: mergeSections(parent.body, overlay) };
|
|
173
|
+
metadata = { ...parent.metadata };
|
|
174
|
+
sources = [...(overlay.extendsAgent === `relay/${name}` ? [base.path] : overlay.extendsAgent === `neo/${name}` ? sharedSources : sources), overlay.path];
|
|
175
|
+
}
|
|
176
|
+
Object.assign(metadata, overlay.metadata);
|
|
177
|
+
if (layer === 'neo') {
|
|
178
|
+
shared = effective;
|
|
179
|
+
sharedSources = [...sources];
|
|
180
|
+
}
|
|
144
181
|
}
|
|
145
|
-
|
|
146
|
-
|
|
182
|
+
if (!effective)
|
|
183
|
+
throw new Error(`Unknown agent ${name}`);
|
|
184
|
+
const content = effective.body;
|
|
185
|
+
return { content, metadata, sources, hash: createHash('sha256').update(content).digest('hex') };
|
|
147
186
|
}
|
package/dist/cli.js
CHANGED
|
@@ -27,7 +27,7 @@ import { classifyFailedTask } from './task-actions.js';
|
|
|
27
27
|
import { resolveExecutionContext } from './execution-context.js';
|
|
28
28
|
import { startServiceExternalActionFlusher } from './service-external-actions.js';
|
|
29
29
|
import { handleServicePublication } from './service-publication.js';
|
|
30
|
-
import {
|
|
30
|
+
import { ensureLocalJobSourceBranch, prepareLocalJobExecutionWorktree, resolveManagedProjectPath, resolveRepositoryProjectPath, resolveWorkspaceRoot, taskRequiresManagedWorktree } from './workspace.js';
|
|
31
31
|
import { runBlockerCommand } from './cli-blocker.js';
|
|
32
32
|
import { runEscalationCommand } from './cli-escalation.js';
|
|
33
33
|
import { runProjectionCommand } from './cli-projection.js';
|
|
@@ -404,10 +404,8 @@ async function runContextCommand(action, args) {
|
|
|
404
404
|
const roles = ['orchestrator', 'planner', 'reviewer', 'worker', 'nested-worker'];
|
|
405
405
|
if (!roles.includes(roleValue))
|
|
406
406
|
throw new Error('--role must be orchestrator, planner, reviewer, worker, or nested-worker');
|
|
407
|
-
const neoRootArg = arg(args, '--neo-root');
|
|
408
407
|
const context = await resolveExecutionContext(projectPath, roleValue, {
|
|
409
408
|
workspaceRoot: arg(args, '--workspace-root') || undefined,
|
|
410
|
-
neoRoot: neoRootArg === 'none' ? null : neoRootArg || undefined,
|
|
411
409
|
globalRoot: arg(args, '--global-root') || undefined,
|
|
412
410
|
explicitSkills: arg(args, '--skills').split(',').filter(Boolean),
|
|
413
411
|
explicitAgents: arg(args, '--agents').split(',').filter(Boolean),
|
|
@@ -559,16 +557,23 @@ export function runtime(store, args, bus, usageRegistry = codexAndZaiUsageRegist
|
|
|
559
557
|
const modelRuntime = arg(args, '--codex', 'codex');
|
|
560
558
|
const policy = chatGptTabClosePolicy(args);
|
|
561
559
|
const planner = runtimePlanner(args);
|
|
562
|
-
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) => {
|
|
563
|
-
const workspaceRoot = arg(args, '--workspace-root') ||
|
|
560
|
+
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, executionContextRoots: { workspaceRoot: arg(args, '--workspace-root') || process.env.AGENTS_RELAY_WORKSPACE_ROOT }, projectPathForJob: async (job, task) => {
|
|
561
|
+
const workspaceRoot = await resolveWorkspaceRoot(arg(args, '--workspace-root') || undefined);
|
|
564
562
|
const canonical = await resolveManagedProjectPath(noGitHubMode(args) ? undefined : job.repository, arg(args, '--project-path'), workspaceRoot);
|
|
565
|
-
if (!
|
|
563
|
+
if (!(task ? taskRequiresManagedWorktree(job, task) : Boolean(job.localMerge?.sourceRef)))
|
|
566
564
|
return resolveRepositoryProjectPath(canonical, job.projectPath);
|
|
567
|
-
const sourceRef = await ensureLocalJobSourceBranch(canonical, job);
|
|
565
|
+
const sourceRef = job.localMerge?.sourceRef ?? await ensureLocalJobSourceBranch(canonical, job);
|
|
568
566
|
if (!job.localMerge?.sourceRef) {
|
|
569
|
-
if (!store.mutateJob)
|
|
570
|
-
throw new Error('Managed
|
|
571
|
-
await store.mutateJob(job.id, current =>
|
|
567
|
+
if (!task || !store.mutateJob)
|
|
568
|
+
throw new Error('Managed source branch binding requires an executing code Task and atomic SQLite mutation');
|
|
569
|
+
const bound = await store.mutateJob(job.id, current => {
|
|
570
|
+
const activeTask = current.tasks.find(item => item.id === task.id);
|
|
571
|
+
if (current.localMerge || current.state !== 'ACTIVE' || !activeTask || !['QUEUED', 'RUNNING'].includes(activeTask.state) || !taskRequiresManagedWorktree(current, activeTask))
|
|
572
|
+
return current;
|
|
573
|
+
return { ...current, localMerge: { sourceRef, targetRef: 'refs/heads/main', status: 'pending' }, updatedAt: new Date().toISOString() };
|
|
574
|
+
});
|
|
575
|
+
if (!bound.localMerge?.sourceRef)
|
|
576
|
+
throw new Error('Job changed before managed source branch binding');
|
|
572
577
|
}
|
|
573
578
|
return prepareLocalJobExecutionWorktree(canonical, await store.load(job.id), workspaceRoot);
|
|
574
579
|
} });
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { resolveAgentMarkdown } from './agent-content-resolver.js';
|
|
2
|
+
import { resolveWorkspaceRoot } from './workspace.js';
|
|
2
3
|
import { access, readFile, readdir, realpath, stat } from 'node:fs/promises';
|
|
3
4
|
import { homedir } from 'node:os';
|
|
4
5
|
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
@@ -25,22 +26,6 @@ async function directory(path) {
|
|
|
25
26
|
return false;
|
|
26
27
|
}
|
|
27
28
|
}
|
|
28
|
-
function configuredWorkspaceRoot(projectPath, explicit) {
|
|
29
|
-
const configured = explicit ?? process.env.AGENTS_RELAY_WORKSPACE_ROOT ?? process.env.XCHAT_WORKSPACE_ROOT;
|
|
30
|
-
if (configured)
|
|
31
|
-
return resolve(configured);
|
|
32
|
-
const conventional = join(homedir(), 'Workspace');
|
|
33
|
-
return isWithin(projectPath, conventional) ? conventional : dirname(projectPath);
|
|
34
|
-
}
|
|
35
|
-
async function configuredNeoRoot(workspaceRoot, explicit) {
|
|
36
|
-
if (explicit === null)
|
|
37
|
-
return null;
|
|
38
|
-
const configured = explicit ?? process.env.NEO_SHARED_ROOT;
|
|
39
|
-
if (configured)
|
|
40
|
-
return await directory(resolve(configured)) ? await realpath(resolve(configured)) : null;
|
|
41
|
-
const candidate = join(workspaceRoot, 'neo');
|
|
42
|
-
return await directory(candidate) ? await realpath(candidate) : null;
|
|
43
|
-
}
|
|
44
29
|
function configuredGlobalRoot(explicit) {
|
|
45
30
|
return resolve(explicit ?? process.env.AGENTS_HOME ?? join(homedir(), '.agents'));
|
|
46
31
|
}
|
|
@@ -176,9 +161,9 @@ export async function resolveExecutionContext(projectPath, role, options = {}) {
|
|
|
176
161
|
const project = await realpath(resolve(projectPath)).catch(() => { throw new Error(`Project path not found: ${projectPath}`); });
|
|
177
162
|
if (!await directory(project))
|
|
178
163
|
throw new Error(`Project path is not a directory: ${projectPath}`);
|
|
179
|
-
const
|
|
180
|
-
const
|
|
181
|
-
const neoRoot = await
|
|
164
|
+
const workspaceRoot = await resolveWorkspaceRoot(options.workspaceRoot);
|
|
165
|
+
const neoCandidate = join(workspaceRoot, 'neo');
|
|
166
|
+
const neoRoot = await directory(neoCandidate) ? await realpath(neoCandidate) : neoCandidate;
|
|
182
167
|
const globalCandidate = configuredGlobalRoot(options.globalRoot);
|
|
183
168
|
const globalRoot = await realpath(globalCandidate).catch(() => globalCandidate);
|
|
184
169
|
const layerRoots = [
|
|
@@ -197,28 +182,26 @@ export async function resolveExecutionContext(projectPath, role, options = {}) {
|
|
|
197
182
|
}
|
|
198
183
|
const skills = selectByName([...skillMap.values()].sort((a, b) => a.name.localeCompare(b.name)), options.explicitSkills, 'skill');
|
|
199
184
|
const agentMap = new Map();
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
}
|
|
221
|
-
agentMap.set(agent.name, agent);
|
|
185
|
+
const agentNames = new Set();
|
|
186
|
+
for (const [root, source] of [[dirname(BUNDLED_AGENTS_ROOT), 'builtin'], [neoRoot, 'neo'], [project, 'project']]) {
|
|
187
|
+
for (const agent of await agentFiles(root, source, []))
|
|
188
|
+
agentNames.add(agent.name);
|
|
189
|
+
}
|
|
190
|
+
for (const name of agentNames) {
|
|
191
|
+
const resolved = await resolveAgentMarkdown(name, BUNDLED_AGENTS_ROOT, project, neoRoot);
|
|
192
|
+
const fields = resolved.metadata;
|
|
193
|
+
const sourcePath = resolved.sources[resolved.sources.length - 1];
|
|
194
|
+
const source = sourcePath?.startsWith(project + '/') ? 'project'
|
|
195
|
+
: sourcePath?.startsWith(neoRoot + '/') ? 'neo' : 'builtin';
|
|
196
|
+
agentMap.set(name, {
|
|
197
|
+
name, path: sourcePath, source,
|
|
198
|
+
description: fields.description,
|
|
199
|
+
capabilities: listValue(fields.capabilities),
|
|
200
|
+
runtime: fields.runtime || listValue(fields.runtimes)[0] || undefined,
|
|
201
|
+
trustDomains: listValue(fields.trustDomains ?? fields.trust_domains ?? fields.trust),
|
|
202
|
+
delegation: listValue(fields.delegation),
|
|
203
|
+
restrictions: listValue(fields.restrictions ?? fields.boundaries),
|
|
204
|
+
});
|
|
222
205
|
}
|
|
223
206
|
for (const registration of options.registeredAgents ?? []) {
|
|
224
207
|
if (agentMap.has(registration.name))
|
|
@@ -252,7 +235,7 @@ export async function resolveAgentContract(context, agentName) {
|
|
|
252
235
|
throw new Error(`Agent ${agentName} is not visible in the resolved execution context`);
|
|
253
236
|
if (!agent.path)
|
|
254
237
|
throw new Error(`Agent ${agentName} has no readable Markdown contract`);
|
|
255
|
-
const resolved = await resolveAgentMarkdown(agentName, BUNDLED_AGENTS_ROOT, context.project.path);
|
|
238
|
+
const resolved = await resolveAgentMarkdown(agentName, BUNDLED_AGENTS_ROOT, context.project.path, context.project.neoRoot);
|
|
256
239
|
const definition = resolved.content;
|
|
257
240
|
if (!definition.trim())
|
|
258
241
|
throw new Error(`Agent ${agentName} Markdown contract is empty`);
|
|
@@ -282,6 +282,7 @@ export class ReconcilerRuntimeBase {
|
|
|
282
282
|
? 'worker_outcome_unknown' : platformFailure ? 'worker_platform_failure' : failureReason;
|
|
283
283
|
task.leaseOwner = null;
|
|
284
284
|
task.leaseExpiresAt = null;
|
|
285
|
+
task.executionId = null;
|
|
285
286
|
},
|
|
286
287
|
});
|
|
287
288
|
if (!transition)
|
package/dist/reconciler.js
CHANGED
|
@@ -442,17 +442,11 @@ export class Reconciler extends ReconcilerReviewBase {
|
|
|
442
442
|
let executionContext;
|
|
443
443
|
try {
|
|
444
444
|
const role = task.agentName === 'reviewer' ? 'reviewer' : task.agentName === 'troubleshooter' ? 'troubleshooter' : task.parentTaskId ? 'nested-worker' : 'worker';
|
|
445
|
-
|
|
446
|
-
const canonicalProjectRun = taskUsesCanonicalProjectRun(task, job.tasks);
|
|
445
|
+
const projectPath = this.options.projectPathForJob ? await this.options.projectPathForJob(job, task) : task.routing?.cwd ?? process.cwd();
|
|
447
446
|
executionContext = await this.resolvedContext(job, task, role, projectPath);
|
|
448
447
|
}
|
|
449
448
|
catch (error) {
|
|
450
|
-
task.
|
|
451
|
-
task.leaseExpiresAt = null;
|
|
452
|
-
task.executionId = null;
|
|
453
|
-
await transitionTaskEvent(task, 'block');
|
|
454
|
-
task.error = `Execution checkout unavailable: ${error instanceof Error ? error.message : String(error)}`;
|
|
455
|
-
await this.store.saveTask(task);
|
|
449
|
+
await this.failLaunch(job.id, task.id, task.executionId, `Execution context unavailable: ${error instanceof Error ? error.message : String(error)}`);
|
|
456
450
|
return;
|
|
457
451
|
}
|
|
458
452
|
const startedAt = Date.now();
|
|
@@ -460,7 +454,14 @@ export class Reconciler extends ReconcilerReviewBase {
|
|
|
460
454
|
const controller = new AbortController();
|
|
461
455
|
let execution;
|
|
462
456
|
const sourceId = executionEventSource(job.id, task.id, durableExecutionId);
|
|
463
|
-
|
|
457
|
+
let baseWorkerInput;
|
|
458
|
+
try {
|
|
459
|
+
baseWorkerInput = await managedWorkerInput(job, task, executionContext);
|
|
460
|
+
}
|
|
461
|
+
catch (error) {
|
|
462
|
+
await this.failLaunch(job.id, task.id, task.executionId, `Execution contract unavailable: ${error instanceof Error ? error.message : String(error)}`);
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
464
465
|
const canonicalProjectRun = taskUsesCanonicalProjectRun(task, job.tasks);
|
|
465
466
|
const workerInput = canonicalProjectRun
|
|
466
467
|
? `${baseWorkerInput}\n\n[Canonical Neo project run]\nExecution cwd: ${executionContext.project.path}\nKeep generated artifacts under runs/.`
|
package/dist/relayd-utilities.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
3
|
import { executionRouterRulesPath } from './routing-policy.js';
|
|
4
4
|
import { RelayObservability, setObservability } from './observability.js';
|
|
5
5
|
/** Initialize the process-wide structured observability sink once, from environment. */
|
|
@@ -37,7 +37,8 @@ Usage:
|
|
|
37
37
|
Options:
|
|
38
38
|
--no-github Local-only mode: no GitHub auth, transport, PR, merge, or fetch
|
|
39
39
|
--repo OWNER/REPO Watch one repository
|
|
40
|
-
--workspace PATH
|
|
40
|
+
--workspace-root PATH Workspace Root (default: AGENTS_RELAY_WORKSPACE_ROOT, shared Neo config, or ~/Workspace)
|
|
41
|
+
--workspace PATH Legacy alias for --workspace-root
|
|
41
42
|
--port PORT Dashboard port (default: 8787)
|
|
42
43
|
--concurrency N Maximum concurrent workers (default: 4)
|
|
43
44
|
--interval MS Recovery interval
|
|
@@ -50,7 +51,7 @@ Legacy single-job mode also accepts --repo OWNER/REPO --pr NUMBER --id JOB_ID.
|
|
|
50
51
|
|
|
51
52
|
Examples:
|
|
52
53
|
npx agents-relayd --repo OWNER/REPO
|
|
53
|
-
npx agents-relayd --workspace ~/Workspace`;
|
|
54
|
+
npx agents-relayd --workspace-root ~/Workspace`;
|
|
54
55
|
export function firstStageChoices(decisionInput) {
|
|
55
56
|
const stages = decisionInput?.stages;
|
|
56
57
|
if (!Array.isArray(stages) || stages.length === 0)
|
|
@@ -61,7 +62,10 @@ export function firstStageChoices(decisionInput) {
|
|
|
61
62
|
export function value(args, name, fallback = '') { const index = args.indexOf(name); return index >= 0 ? args[index + 1] ?? fallback : fallback; }
|
|
62
63
|
export function normalizeDaemonArguments(argv) {
|
|
63
64
|
const repository = value(argv, '--repo') || null;
|
|
64
|
-
|
|
65
|
+
if (argv.includes('--workspace-root') && argv.includes('--workspace'))
|
|
66
|
+
throw new Error('--workspace-root and --workspace cannot both be supplied');
|
|
67
|
+
const workspaceRootValue = value(argv, '--workspace-root') || value(argv, '--workspace') || process.env.AGENTS_RELAY_WORKSPACE_ROOT;
|
|
68
|
+
const workspaceRoot = workspaceRootValue ? resolve(workspaceRootValue) : null;
|
|
65
69
|
const prValue = value(argv, '--pr');
|
|
66
70
|
const id = value(argv, '--id');
|
|
67
71
|
if ((prValue && !id) || (!prValue && id))
|
|
@@ -73,7 +77,7 @@ export function normalizeDaemonArguments(argv) {
|
|
|
73
77
|
const pullRequest = prValue ? Number(prValue) : null;
|
|
74
78
|
if (pullRequest !== null && (!Number.isInteger(pullRequest) || pullRequest <= 0))
|
|
75
79
|
throw new Error('--pr must be positive');
|
|
76
|
-
return { repository, workspaceRoot
|
|
80
|
+
return { repository, workspaceRoot, pullRequest, jobId: id || null, serviceArguments: [...argv] };
|
|
77
81
|
}
|
|
78
82
|
export function recoveryScanRepositories(repositories, cached, dormantProbeIndex = 0) {
|
|
79
83
|
const byRepository = new Map();
|
package/dist/relayd.js
CHANGED
|
@@ -34,7 +34,7 @@ import { applyJobActivateAction } from './job-actions.js';
|
|
|
34
34
|
import { managedTaskUpdateHandler } from './task-update.js';
|
|
35
35
|
import { dashboardJobSetState, dashboardTaskAction, dashboardTaskSetState } from './dashboard-actions.js';
|
|
36
36
|
import { codexAndZaiUsageRegistry } from './usage.js';
|
|
37
|
-
import {
|
|
37
|
+
import { discoverWorkspaceRepositories, ensureLocalJobSourceBranch, prepareLocalJobExecutionWorktree, resolveManagedProjectPath, resolveRepositoryProjectPath, resolveWorkspaceRoot, taskRequiresManagedWorktree } from './workspace.js';
|
|
38
38
|
import { deriveJobRuntimeStatus } from './runtime-status.js';
|
|
39
39
|
import { createRuntimeStatusPublisher } from './runtime-status-events.js';
|
|
40
40
|
import { subscribeLocalCommits } from './local-commit-events.js';
|
|
@@ -97,15 +97,16 @@ export async function runDaemon(argv) {
|
|
|
97
97
|
const bus = eventResolution.bus;
|
|
98
98
|
const stopRuntimeStatusEvents = bus ? subscribeLocalCommits(createRuntimeStatusPublisher(bus)) : () => { };
|
|
99
99
|
process.stderr.write(`${eventBusResolutionMessage(eventResolution)}\n`);
|
|
100
|
-
const workspaceRoot =
|
|
101
|
-
|
|
100
|
+
const workspaceRoot = await resolveWorkspaceRoot(normalized.workspaceRoot ?? undefined);
|
|
101
|
+
const explicitProjectPath = value(argv, '--project-path');
|
|
102
|
+
const localProjectPath = noGitHub && !explicitProjectPath && !argv.includes('--workspace') && !argv.includes('--workspace-root') && !process.env.AGENTS_RELAY_WORKSPACE_ROOT
|
|
103
|
+
? process.cwd() : explicitProjectPath || workspaceRoot;
|
|
102
104
|
const discoveredRepositories = normalized.repository || noGitHub ? [] : await discoverWorkspaceRepositories(workspaceRoot).catch(() => []);
|
|
103
|
-
const repositories = noGitHub && !normalized.repository ? [`local/${basename(
|
|
105
|
+
const repositories = noGitHub && !normalized.repository ? [`local/${basename(localProjectPath)}`] : normalized.repository ? [normalized.repository] : discoveredRepositories.map(item => item.repository);
|
|
104
106
|
const repositoryPaths = new Map(discoveredRepositories.map(item => [item.repository, item.path]));
|
|
105
107
|
if (noGitHub)
|
|
106
108
|
for (const repo of repositories)
|
|
107
|
-
repositoryPaths.set(repo,
|
|
108
|
-
const explicitProjectPath = value(argv, '--project-path');
|
|
109
|
+
repositoryPaths.set(repo, localProjectPath);
|
|
109
110
|
const usageRegistry = codexAndZaiUsageRegistry();
|
|
110
111
|
const decisionAuditStore = new JsonlDecisionAuditStore(process.env.AGENTS_RELAY_DECISION_AUDIT_PATH ?? join(homedir(), '.agents-relay', 'decision-audit.jsonl'));
|
|
111
112
|
let cachedOverviews = [];
|
|
@@ -152,15 +153,24 @@ export async function runDaemon(argv) {
|
|
|
152
153
|
return dashboardManagedJobs(entries.filter(item => !['COMPLETED', 'CANCELLED'].includes(item.job.state) || counts.get(item.job.id) !== 1));
|
|
153
154
|
};
|
|
154
155
|
cachedOverviews = await inventoryFromLocal();
|
|
155
|
-
const makeReconciler = (store, maxConcurrent) => new Reconciler(store, { noGitHub, owner: `relayd-${process.pid}`, maxConcurrent, leaseMs: Number(value(argv, '--lease-ms', '300000')), adapters: [new CodexAdapter(value(argv, '--codex', 'codex')), new ChatGptAdapter()], continuations: [new CodexThreadContinuation(value(argv, '--codex', 'codex')), new CommandContinuation(), new WebhookContinuation()], planner: runtimePlanner(argv), usageSnapshotProvider: () => usageRegistry.snapshot(), runtimeSummaryProvider: () => ({ chatgptRunning: summarizeRuntime(cachedOverviews.map(item => item.job)).workers.chatgpt }), decisionEngine: executionDecisionEngine(), decisionMode: decisionMode(), decisionAuditStore, eventBus: bus, escalationNotifier: escalationNotifierFromEnvironment(), reviewer: runtimeReviewer(argv), projectPathForJob: async (job) => {
|
|
156
|
+
const makeReconciler = (store, maxConcurrent) => new Reconciler(store, { noGitHub, owner: `relayd-${process.pid}`, maxConcurrent, leaseMs: Number(value(argv, '--lease-ms', '300000')), adapters: [new CodexAdapter(value(argv, '--codex', 'codex')), new ChatGptAdapter()], continuations: [new CodexThreadContinuation(value(argv, '--codex', 'codex')), new CommandContinuation(), new WebhookContinuation()], planner: runtimePlanner(argv), usageSnapshotProvider: () => usageRegistry.snapshot(), runtimeSummaryProvider: () => ({ chatgptRunning: summarizeRuntime(cachedOverviews.map(item => item.job)).workers.chatgpt }), decisionEngine: executionDecisionEngine(), decisionMode: decisionMode(), decisionAuditStore, eventBus: bus, escalationNotifier: escalationNotifierFromEnvironment(), reviewer: runtimeReviewer(argv), executionContextRoots: { workspaceRoot }, projectPathForJob: async (job, task) => {
|
|
156
157
|
const canonical = repositoryPaths.get(job.repository) ?? await resolveManagedProjectPath(job.repository, explicitProjectPath, workspaceRoot);
|
|
157
|
-
if (!
|
|
158
|
+
if (!(task ? taskRequiresManagedWorktree(job, task) : Boolean(job.localMerge?.sourceRef)))
|
|
158
159
|
return resolveRepositoryProjectPath(canonical, job.projectPath);
|
|
159
|
-
const sourceRef = await ensureLocalJobSourceBranch(canonical, job);
|
|
160
|
-
if (!
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
160
|
+
const sourceRef = job.localMerge?.sourceRef ?? await ensureLocalJobSourceBranch(canonical, job);
|
|
161
|
+
if (!job.localMerge?.sourceRef) {
|
|
162
|
+
if (!task || !store.mutateJob)
|
|
163
|
+
throw new Error('Managed source branch binding requires an executing code Task and atomic SQLite mutation');
|
|
164
|
+
const bound = await store.mutateJob(job.id, current => {
|
|
165
|
+
const activeTask = current.tasks.find(item => item.id === task.id);
|
|
166
|
+
if (current.localMerge || current.state !== 'ACTIVE' || !activeTask || !['QUEUED', 'RUNNING'].includes(activeTask.state) || !taskRequiresManagedWorktree(current, activeTask))
|
|
167
|
+
return current;
|
|
168
|
+
return { ...current, localMerge: { sourceRef, targetRef: 'refs/heads/main', status: 'pending' }, updatedAt: new Date().toISOString() };
|
|
169
|
+
});
|
|
170
|
+
if (!bound.localMerge?.sourceRef)
|
|
171
|
+
throw new Error('Job changed before managed source branch binding');
|
|
172
|
+
}
|
|
173
|
+
return prepareLocalJobExecutionWorktree(canonical, await store.load(job.id), workspaceRoot);
|
|
164
174
|
} });
|
|
165
175
|
const refreshAll = async (_full = false) => {
|
|
166
176
|
cachedOverviews = await inventoryFromLocal();
|
package/dist/workspace.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { lstat, mkdir, readdir, realpath, stat } from 'node:fs/promises';
|
|
1
|
+
import { lstat, mkdir, readFile, readdir, realpath, stat } from 'node:fs/promises';
|
|
2
2
|
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
4
|
import { createHash } from 'node:crypto';
|
|
@@ -17,6 +17,46 @@ async function gitOutput(args) {
|
|
|
17
17
|
export function defaultWorkspaceRoot() {
|
|
18
18
|
return join(homedir(), 'Workspace');
|
|
19
19
|
}
|
|
20
|
+
export async function resolveWorkspaceRoot(explicit) {
|
|
21
|
+
let configured = explicit ?? process.env.AGENTS_RELAY_WORKSPACE_ROOT;
|
|
22
|
+
if (configured === undefined) {
|
|
23
|
+
const configPath = process.env.NEO_WORKSPACE_CONFIG ?? join(homedir(), '.config', 'neo', 'workspace.json');
|
|
24
|
+
let configText;
|
|
25
|
+
try {
|
|
26
|
+
configText = await readFile(configPath, 'utf8');
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
if (error.code !== 'ENOENT')
|
|
30
|
+
throw new Error(`Cannot read workspace configuration: ${error instanceof Error ? error.message : String(error)}`);
|
|
31
|
+
}
|
|
32
|
+
if (configText !== undefined) {
|
|
33
|
+
let config;
|
|
34
|
+
try {
|
|
35
|
+
config = JSON.parse(configText);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
throw new Error(`Invalid workspace configuration at ${configPath}: expected valid JSON`);
|
|
39
|
+
}
|
|
40
|
+
if (typeof config !== 'object' || config === null || !('schemaVersion' in config) || config.schemaVersion !== 1 || !('workspaceRoot' in config) || typeof config.workspaceRoot !== 'string' || !isAbsolute(config.workspaceRoot)) {
|
|
41
|
+
throw new Error(`Invalid workspace configuration at ${configPath}`);
|
|
42
|
+
}
|
|
43
|
+
configured = config.workspaceRoot;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
// On first launch the default workspace may not exist yet. Only user-selected
|
|
47
|
+
// or saved roots are mandatory existing directories.
|
|
48
|
+
if (configured === undefined)
|
|
49
|
+
return defaultWorkspaceRoot();
|
|
50
|
+
const candidate = resolve(configured);
|
|
51
|
+
try {
|
|
52
|
+
if (!(await stat(candidate)).isDirectory())
|
|
53
|
+
throw new Error('not a directory');
|
|
54
|
+
return await realpath(candidate);
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
throw new Error(`Configured Workspace Root is not an accessible directory: ${candidate}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
20
60
|
export function githubRepositoryFromRemote(remote) {
|
|
21
61
|
const value = remote.trim().replace(/\.git$/, '');
|
|
22
62
|
const match = value.match(/github\.com[/:]([^/]+)\/([^/]+)$/i);
|
|
@@ -193,6 +233,9 @@ export function managedLocalJobWorktreePath(repository, jobId, workspaceRoot = d
|
|
|
193
233
|
export function jobRequiresManagedWorktree(job) {
|
|
194
234
|
return !(job.prNumber > 0) && (Boolean(job.localMerge?.sourceRef) || job.tasks.some(task => task.output?.kind === 'task_pr'));
|
|
195
235
|
}
|
|
236
|
+
export function taskRequiresManagedWorktree(job, task) {
|
|
237
|
+
return jobRequiresManagedWorktree(job) && !(task && taskUsesCanonicalProjectRun(task, job.tasks));
|
|
238
|
+
}
|
|
196
239
|
/** Bind an ordinary SQLite Job to one deterministic local work branch.
|
|
197
240
|
* The target is always main, and branch creation never switches the user's checkout.
|
|
198
241
|
* This is a Git-local operation and never needs GitHub credentials.
|
package/package.json
CHANGED
|
@@ -4,13 +4,15 @@
|
|
|
4
4
|
|
|
5
5
|
## Execution event source
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
The Common Events Bus protocol (`<workspace>/neo/skills/events-bus/references/protocol.md`) owns the public `events.publish` input format. Its `source` field is a **string** identifying the exact current Relay execution:
|
|
8
8
|
|
|
9
9
|
```text
|
|
10
|
-
source
|
|
10
|
+
source = "job/<job-id>/task/<task-id>/execution/<execution-id>"
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
When calling the Neo MCP command facade, pass `command: "publish"` and nest `type`, `source`, and `data` under `args`. Do not pass an object such as `{ "source": { "id": "..." } }`; `source.id` belongs only to the internal Events Bus envelope.
|
|
14
|
+
|
|
15
|
+
Relay supplies the complete source string in the managed Task context. Agents must copy it unchanged; they must not infer a Job/Task ID, generate an execution ID, or reuse an identity from a previous attempt. Transport acceptance alone does not establish that Relay accepted the event for the current execution.
|
|
14
16
|
|
|
15
17
|
## Producer contract
|
|
16
18
|
|