agents-relay 2.0.54 → 2.0.56
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-escalation.js
CHANGED
|
@@ -1,37 +1,62 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
|
+
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
2
5
|
import { textOption, arg } from './cli-support.js';
|
|
3
6
|
import { runEscalationGraph } from './workflow/escalation.js';
|
|
4
|
-
function
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
return
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
}
|
|
32
|
-
|
|
7
|
+
export function neoEscalationArgs(handoffPath, policyPath, cwd) {
|
|
8
|
+
return [
|
|
9
|
+
'escalation', '--handoff-file', handoffPath,
|
|
10
|
+
...(policyPath ? ['--on-needs-user-file', policyPath] : []),
|
|
11
|
+
...(cwd ? ['--cwd', cwd] : []),
|
|
12
|
+
];
|
|
13
|
+
}
|
|
14
|
+
function handoffText(request) {
|
|
15
|
+
return [
|
|
16
|
+
'# Intent', request.intent.trim(),
|
|
17
|
+
'', '# Blocked on', request.blockedOn.trim(),
|
|
18
|
+
...(request.context?.trim() ? ['', '# Context', request.context.trim()] : []),
|
|
19
|
+
'',
|
|
20
|
+
].join('\n');
|
|
21
|
+
}
|
|
22
|
+
export async function runNeoEscalation(request) {
|
|
23
|
+
const tempRoot = await mkdtemp(join(tmpdir(), 'agents-relay-escalation-'));
|
|
24
|
+
try {
|
|
25
|
+
const handoffPath = join(tempRoot, 'handoff.md');
|
|
26
|
+
await writeFile(handoffPath, handoffText(request), { encoding: 'utf8', mode: 0o600 });
|
|
27
|
+
let policyPath;
|
|
28
|
+
if (request.onNeedsUser) {
|
|
29
|
+
policyPath = join(tempRoot, 'on-needs-user.md');
|
|
30
|
+
await writeFile(policyPath, request.onNeedsUser, { encoding: 'utf8', mode: 0o600 });
|
|
31
|
+
}
|
|
32
|
+
const args = neoEscalationArgs(handoffPath, policyPath, request.cwd);
|
|
33
|
+
return await new Promise((resolve, reject) => {
|
|
34
|
+
const child = spawn('neo', args, { cwd: request.cwd ?? process.cwd(), stdio: ['ignore', 'pipe', 'pipe'] });
|
|
35
|
+
let stdout = '';
|
|
36
|
+
let stderr = '';
|
|
37
|
+
child.stdout.setEncoding('utf8').on('data', chunk => { stdout += chunk; });
|
|
38
|
+
child.stderr.setEncoding('utf8').on('data', chunk => { stderr += chunk; });
|
|
39
|
+
child.on('error', reject);
|
|
40
|
+
child.on('close', code => {
|
|
41
|
+
let parsed;
|
|
42
|
+
try {
|
|
43
|
+
parsed = JSON.parse(stdout.trim());
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
reject(new Error(`neo escalation returned invalid JSON (exit ${code ?? 'unknown'}): ${stderr || stdout}`));
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
if (!['resolved', 'failed', 'needs_user'].includes(parsed.status)) {
|
|
50
|
+
reject(new Error(`neo escalation returned invalid status ${String(parsed.status)}`));
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
resolve(parsed);
|
|
54
|
+
});
|
|
33
55
|
});
|
|
34
|
-
}
|
|
56
|
+
}
|
|
57
|
+
finally {
|
|
58
|
+
await rm(tempRoot, { recursive: true, force: true });
|
|
59
|
+
}
|
|
35
60
|
}
|
|
36
61
|
export async function runEscalationCommand(args) {
|
|
37
62
|
const intent = await textOption(args, '--intent', '--intent-file');
|
package/dist/reconciler.js
CHANGED
|
@@ -14,7 +14,7 @@ import { executionEventSource, withExecutionBoundary } from './reconciler-runtim
|
|
|
14
14
|
export { executionEventSource } from './reconciler-runtime.js';
|
|
15
15
|
import { CallbackWorkflowEffectHandler } from './workflow/effects.js';
|
|
16
16
|
import { pendingTroubleshooterTasks } from './troubleshooter.js';
|
|
17
|
-
import { prepareManagedExecutionWorktree } from './workspace.js';
|
|
17
|
+
import { prepareManagedExecutionWorktree, taskUsesCanonicalProjectRun } from './workspace.js';
|
|
18
18
|
import { createReviewChairExecutionPlan } from './review-chair.js';
|
|
19
19
|
import { taskTagWorkerPrompt } from './task-tag.js';
|
|
20
20
|
import { recoverExpiredExecutions, recoverStartupOrphans as recoverStartupExecutionOrphans } from './execution-recovery.js';
|
|
@@ -412,7 +412,8 @@ export class Reconciler extends ReconcilerReviewBase {
|
|
|
412
412
|
try {
|
|
413
413
|
const role = task.agentName === 'reviewer' ? 'reviewer' : task.agentName === 'troubleshooter' ? 'troubleshooter' : task.parentTaskId ? 'nested-worker' : 'worker';
|
|
414
414
|
let projectPath = this.options.projectPathForJob ? await this.options.projectPathForJob(job) : task.routing?.cwd ?? process.cwd();
|
|
415
|
-
|
|
415
|
+
const canonicalProjectRun = taskUsesCanonicalProjectRun(task, job.tasks);
|
|
416
|
+
if (['codex', 'chatgpt'].includes(task.adapter) && this.store.pullRequestHead && task.executionId && !canonicalProjectRun) {
|
|
416
417
|
managedHead = await this.store.pullRequestHead();
|
|
417
418
|
if (!managedHead)
|
|
418
419
|
throw new Error(`PR #${job.prNumber} has no resolvable head ref`);
|
|
@@ -435,9 +436,12 @@ export class Reconciler extends ReconcilerReviewBase {
|
|
|
435
436
|
let execution;
|
|
436
437
|
const sourceId = executionEventSource(job.id, task.id, durableExecutionId);
|
|
437
438
|
const baseWorkerInput = await managedWorkerInput(job, task, executionContext);
|
|
439
|
+
const canonicalProjectRun = taskUsesCanonicalProjectRun(task, job.tasks);
|
|
438
440
|
const workerInput = managedHead
|
|
439
441
|
? `${baseWorkerInput}\n\n[Managed PR checkout]\nExecution cwd: ${executionContext.project.path}\nExpected PR head: ${managedHead.ref} @ ${managedHead.sha}\nThis checkout is isolated from the user's primary checkout. Commit only here. Push durable changes with: git push origin HEAD:${managedHead.ref}\nDo not switch, reset, merge into, or commit on the primary checkout.`
|
|
440
|
-
:
|
|
442
|
+
: canonicalProjectRun
|
|
443
|
+
? `${baseWorkerInput}\n\n[Canonical Neo project run]\nExecution cwd: ${executionContext.project.path}\nThis Task owns or descends from a durable runs/ artifact. Work directly in this canonical project directory so all continuations, reviews, QA, and publication steps share the same runs/ evidence. Do not create or switch to an Agents Relay task worktree for this runtime work. Keep generated/private execution artifacts under runs/ and do not commit them.`
|
|
444
|
+
: baseWorkerInput;
|
|
441
445
|
const workerTask = { ...task, input: workerInput, routing: task.routing ? { ...task.routing, cwd: executionContext.project.path } : task.routing };
|
|
442
446
|
const startAckSetup = task.adapter === 'chatgpt' ? await this.startAck(job.id, task.id, sourceId, Date.now(), this.options.startAckTimeoutMs ?? chatGptStartAckTimeoutMs(task.timeoutMs)) : null;
|
|
443
447
|
const closePolicy = task.chatgptTabClosePolicy ?? this.options.chatgptTabClosePolicy ?? adapter.tabClosePolicy ?? 'after-start';
|
package/dist/workspace.js
CHANGED
|
@@ -92,6 +92,24 @@ export function resolveRepositoryProjectPath(repositoryRoot, projectPath) {
|
|
|
92
92
|
throw new Error('Job projectPath escapes repository root');
|
|
93
93
|
return target;
|
|
94
94
|
}
|
|
95
|
+
export function isProjectRunPath(path) {
|
|
96
|
+
if (!path)
|
|
97
|
+
return false;
|
|
98
|
+
const normalized = path.replace(/\\/g, '/').replace(/^\.\//, '');
|
|
99
|
+
return normalized === 'runs' || normalized.startsWith('runs/');
|
|
100
|
+
}
|
|
101
|
+
export function taskUsesCanonicalProjectRun(task, tasks) {
|
|
102
|
+
const byId = new Map(tasks.map(item => [item.id, item]));
|
|
103
|
+
const seen = new Set();
|
|
104
|
+
let current = task;
|
|
105
|
+
while (current && !seen.has(current.id)) {
|
|
106
|
+
seen.add(current.id);
|
|
107
|
+
if (current.output?.kind === 'file' && isProjectRunPath(current.output.path))
|
|
108
|
+
return true;
|
|
109
|
+
current = current.parentTaskId ? byId.get(current.parentTaskId) : undefined;
|
|
110
|
+
}
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
95
113
|
async function git(path, args) {
|
|
96
114
|
return (await execFileAsync('git', ['-C', path, ...args])).stdout.trim();
|
|
97
115
|
}
|
package/package.json
CHANGED
|
@@ -125,6 +125,20 @@ The scheduler heartbeat filters cached managed Jobs, reloads each due Job to val
|
|
|
125
125
|
|
|
126
126
|
## Task evidence and head identity
|
|
127
127
|
|
|
128
|
+
### Canonical Neo Project runs boundary
|
|
129
|
+
|
|
130
|
+
`runs/` is durable runtime evidence, not source code and not temporary checkout state. When a Task's declared file output is under `runs/`, that Task and every semantic descendant execute directly in the canonical resolved project directory. Agents Relay MUST NOT create or switch those Tasks into per-execution Git worktrees.
|
|
131
|
+
|
|
132
|
+
This is a strong execution boundary:
|
|
133
|
+
|
|
134
|
+
- tracked source/code work may use a managed PR worktree;
|
|
135
|
+
- Neo Project runtime work under `runs/` uses the canonical project directory;
|
|
136
|
+
- continuations, reviews, QA, render, and publication Tasks beneath a `runs/` owner reuse that same canonical runtime directory;
|
|
137
|
+
- `runs/` remains ignored/private and must not be committed merely to move evidence between Tasks;
|
|
138
|
+
- a new Task/worktree must never make prior run evidence disappear.
|
|
139
|
+
|
|
140
|
+
For example, a `neo-build-log` episode whose output is `runs/2026-10-02/package.md` executes in the canonical `neo/neo-build-log` project directory, and its review/render/publish descendants see the same `runs/2026-10-02/` tree.
|
|
141
|
+
|
|
128
142
|
Task inputs must make evidence freshness and target identity explicit when those facts affect correctness. A Task may bind its own review, audit, or source evidence to an immutable SHA/version. That frozen target belongs to that Task; it must not be propagated mechanically into later dependent work. A downstream release, deploy, publication, or other Task that runs after dependencies complete must resolve the current durable Job/PR head at execution time unless the Job objective explicitly requires the earlier immutable target.
|
|
129
143
|
|
|
130
144
|
When success depends on the actual current UI, browser state, store listing, runtime state, or another externally observable surface, the Task contract must require fresh evidence from that real current surface. Reusing an existing screenshot, prepared asset, cached observation, or older evidence is valid only when the Job explicitly permits it.
|