agents-relay 2.0.68 → 2.0.70
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -1
- package/dist/adapters.js +39 -3
- package/dist/blockers.js +17 -4
- package/dist/cli-blocker.js +12 -28
- package/dist/cli-help.js +11 -8
- package/dist/cli-job-create.js +19 -13
- package/dist/cli-local-merge-config.js +4 -2
- package/dist/cli-projection.js +23 -55
- package/dist/cli-review.js +7 -24
- package/dist/cli-support.js +59 -4
- package/dist/cli-worker-task.js +63 -0
- package/dist/cli.js +179 -129
- package/dist/dashboard-html.js +15 -12
- package/dist/dashboard.js +26 -1
- package/dist/e2e-injection.js +76 -0
- package/dist/github-binding-order.js +15 -0
- package/dist/github-client.js +7 -2
- package/dist/github-pr-archive.js +22 -0
- package/dist/github-webhook.js +31 -6
- package/dist/job-actions.js +29 -0
- package/dist/local-authority-store.js +10 -1
- package/dist/local-commit-events.js +2 -0
- package/dist/local-git-merge.js +17 -6
- package/dist/local-job-path.js +5 -0
- package/dist/local-job-registry.js +10 -4
- package/dist/local-managed-store.js +2 -4
- package/dist/local-merge-delivery.js +3 -2
- package/dist/local-merge-projection.js +60 -0
- package/dist/managed-worker-input.js +13 -6
- package/dist/manual-plan.js +29 -7
- package/dist/planner-runtime.js +182 -41
- package/dist/planner.js +35 -2
- package/dist/pool.js +22 -6
- package/dist/reconciler-cancellation.js +21 -0
- package/dist/reconciler-review.js +74 -18
- package/dist/reconciler-runtime.js +69 -34
- package/dist/reconciler.js +92 -26
- package/dist/relay-store.js +1 -1
- package/dist/relayd-control.js +184 -0
- package/dist/relayd-dashboard-actions.js +108 -0
- package/dist/relayd-utilities.js +107 -0
- package/dist/relayd.js +220 -210
- package/dist/review-generation-fence.js +12 -0
- package/dist/runtime-status-events.js +21 -0
- package/dist/runtime-status.js +13 -0
- package/dist/service-publication.js +2 -4
- package/dist/task-actions.js +59 -1
- package/dist/troubleshooter.js +21 -2
- package/dist/unblock.js +47 -15
- package/dist/worker-rejection.js +11 -0
- package/dist/workflow-effect-failures.js +30 -0
- package/dist/workspace.js +95 -6
- package/package.json +5 -4
- package/skills/agents-relay/agents/planner.agent.md +3 -0
- package/skills/agents-relay/contracts/orchestrator-workflows.md +4 -7
- package/workflow/review-lifecycle.mmd +3 -0
package/README.md
CHANGED
|
@@ -188,7 +188,7 @@ Pushes to `main` run `.github/workflows/publish.yml`: install, test, choose the
|
|
|
188
188
|
|
|
189
189
|
Normal consumers should invoke the CLI with `npx agents-relay ...` (or `npx agents-relayd ...`).
|
|
190
190
|
|
|
191
|
-
After a successful
|
|
191
|
+
After a successful publish, the workflow records one idempotent `agents-relay:package-published:v1` marker comment using local-merge Job metadata. It uses the closed display PR when present, otherwise a reusable `[Agents Relay] Publication events` issue. The GitHub `issue_comment` webhook resolves the embedded Job ID back to its durable SQLite Job and emits a correlated `package.published` events-bus event containing repository, package, version, registry, commit SHA, PR number, and webhook delivery ID. Its source identity is the GitHub delivery (`github/<repository>/delivery/<delivery-id>`), not a worker execution identity.
|
|
192
192
|
|
|
193
193
|
`package.published` is a release fact, not a service-management command. Agents Relay does not pull packages, restart PM2, or decide which local service consumes a package. An external deployment subscriber may listen for this event, verify that the exact version is pullable, update/restart the affected service, verify its running version and health, and publish its own deployment outcome event. This keeps publication, deployment, and healthy-running as separate facts and removes the need for an orchestrator thread to synchronously wait for npm readiness.
|
|
194
194
|
|
|
@@ -305,3 +305,34 @@ For autonomous Jobs, `UNBLOCKED` is historical: Relay does not retry that old Ta
|
|
|
305
305
|
### Browser Workspace product boundary
|
|
306
306
|
|
|
307
307
|
Agents Relay owns the persistent `Relay` browser workspace as product infrastructure. At daemon startup it runs `browser-workspace status`, dynamically imports the returned absolute `admin_helper`, and calls `ensureWorkspace("Relay", 6)`. This bootstrap is best-effort: Browser Workspace discovery/helper failures are logged and skipped so they do not prevent `agents-relayd` from starting; ChatGPT browser work may remain unavailable until Browser Workspace recovers. Ordinary ChatGPT worker activity uses the public Browser Workspace one-shot `platform run ... --auto-session` command; orchestrator-owned self-healing flows may still use explicit session commands. Relay does not create or delete workspaces through CLI lifecycle commands.
|
|
308
|
+
|
|
309
|
+
## Shared publication reporting
|
|
310
|
+
|
|
311
|
+
Repositories with an existing verified publish/release workflow can report the final result back to Agents Relay with the reusable actions in this repository:
|
|
312
|
+
|
|
313
|
+
```yaml
|
|
314
|
+
permissions:
|
|
315
|
+
contents: read
|
|
316
|
+
issues: write
|
|
317
|
+
pull-requests: read
|
|
318
|
+
|
|
319
|
+
- name: Report verified publication
|
|
320
|
+
uses: lalalic/agents-relay/.github/actions/report-publication@publication-v1
|
|
321
|
+
with:
|
|
322
|
+
token: ${{ github.token }}
|
|
323
|
+
artifact: my-package-or-artifact
|
|
324
|
+
version: ${{ steps.release.outputs.version }}
|
|
325
|
+
channel: https://registry.npmjs.org
|
|
326
|
+
|
|
327
|
+
- name: Report release failure
|
|
328
|
+
if: ${{ failure() }}
|
|
329
|
+
continue-on-error: true
|
|
330
|
+
uses: lalalic/agents-relay/.github/actions/report-publication-failure@publication-v1
|
|
331
|
+
with:
|
|
332
|
+
token: ${{ github.token }}
|
|
333
|
+
stage: release workflow
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
The caller must report only a release it has actually verified. For local-first Jobs, the action reads the Job ID and optional display PR from the authoritative local-merge commit message and posts the compatible `agents-relay:package-published:v1` or publish-failure marker to that display PR or to a reusable publication issue. Historical GitHub-merged PR correlation remains supported as a fallback. The marker and GitHub webhook are transport/correlation only: Agents Relay resolves the originating Job and persists publication facts through `RelayStore.recordPublication(...)` into the authoritative SQLite `publication_delivery` and `publication_state` tables. Publication success or failure never changes a completed Job's lifecycle state.
|
|
337
|
+
|
|
338
|
+
`artifact` can identify an npm/PyPI package, Chrome extension, GitHub Release artifact, or a custom published artifact. `channel` identifies the registry/store/release channel and is displayed as publication metadata. Repeated reports of the same artifact/version are idempotent.
|
package/dist/adapters.js
CHANGED
|
@@ -2,6 +2,8 @@ import { readFile, writeFile, unlink } from 'node:fs/promises';
|
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { pathToFileURL } from 'node:url';
|
|
4
4
|
import { randomUUID } from 'node:crypto';
|
|
5
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
6
|
+
import { withTestInjection } from './e2e-injection.js';
|
|
5
7
|
import { spawnOwnedProcess, terminateOwnedProcess, ownedProcessTerminationReason } from './owned-process.js';
|
|
6
8
|
export function buildCodexArgs(route, input) {
|
|
7
9
|
const args = ['exec', '--json'];
|
|
@@ -27,6 +29,21 @@ function parseThreadStarted(line) {
|
|
|
27
29
|
return undefined;
|
|
28
30
|
}
|
|
29
31
|
}
|
|
32
|
+
function parseCodexFailure(line) {
|
|
33
|
+
try {
|
|
34
|
+
const value = JSON.parse(line);
|
|
35
|
+
if (value.type === 'turn.failed' && value.error && typeof value.error === 'object' && !Array.isArray(value.error)) {
|
|
36
|
+
const message = value.error.message;
|
|
37
|
+
if (typeof message === 'string' && message.trim())
|
|
38
|
+
return { message: message.trim(), priority: 2 };
|
|
39
|
+
}
|
|
40
|
+
if (value.type === 'error' && typeof value.message === 'string' && value.message.trim()) {
|
|
41
|
+
return { message: value.message.trim(), priority: 1 };
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
catch { /* non-JSON output is not structured Codex failure evidence */ }
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
30
47
|
function appendTail(current, chunk, maxBytes = 16384) {
|
|
31
48
|
const combined = current + chunk;
|
|
32
49
|
return Buffer.byteLength(combined) <= maxBytes
|
|
@@ -42,6 +59,8 @@ export class CodexAdapter {
|
|
|
42
59
|
this.command = command;
|
|
43
60
|
}
|
|
44
61
|
launch(task, signal, context = {}) {
|
|
62
|
+
if (context.testInjection?.method === 'worker.codex.launch')
|
|
63
|
+
return withTestInjection('worker.codex.launch', context.testInjection, () => this.launch(task, signal, { ...context, testInjection: undefined }));
|
|
45
64
|
if (!task.routing)
|
|
46
65
|
throw new Error(`Task ${task.id} requires routing metadata before Codex launch`);
|
|
47
66
|
const child = spawnOwnedProcess(this.command, buildCodexArgs(task.routing, task.input), {
|
|
@@ -52,6 +71,7 @@ export class CodexAdapter {
|
|
|
52
71
|
});
|
|
53
72
|
let stdoutBuffer = '';
|
|
54
73
|
let stderrTail = '';
|
|
74
|
+
let structuredFailure;
|
|
55
75
|
let bufferedThreadId;
|
|
56
76
|
let launchedResolve;
|
|
57
77
|
let launchedReject;
|
|
@@ -59,6 +79,9 @@ export class CodexAdapter {
|
|
|
59
79
|
launched.catch(() => { });
|
|
60
80
|
let execution;
|
|
61
81
|
const consumeLine = (line) => {
|
|
82
|
+
const failure = parseCodexFailure(line);
|
|
83
|
+
if (failure && (!structuredFailure || failure.priority >= structuredFailure.priority))
|
|
84
|
+
structuredFailure = failure;
|
|
62
85
|
const threadId = parseThreadStarted(line);
|
|
63
86
|
if (!threadId)
|
|
64
87
|
return;
|
|
@@ -89,7 +112,7 @@ export class CodexAdapter {
|
|
|
89
112
|
resolve({ summary: 'Codex runtime exited after task execution; terminal task state is event-owned' });
|
|
90
113
|
}
|
|
91
114
|
else {
|
|
92
|
-
reject(new Error(stderrTail.trim() || `Codex exited ${code}`));
|
|
115
|
+
reject(new Error(structuredFailure?.message || stderrTail.trim() || `Codex exited ${code}`));
|
|
93
116
|
}
|
|
94
117
|
});
|
|
95
118
|
});
|
|
@@ -148,7 +171,11 @@ export function parseDriverJson(output) {
|
|
|
148
171
|
function browserWorkspaceCli(override = process.env.AGENTS_RELAY_BROWSER_WORKSPACE_COMMAND) {
|
|
149
172
|
return override ? { command: override, prefix: [] } : { command: 'uvx', prefix: ['browser-workspace'] };
|
|
150
173
|
}
|
|
174
|
+
const browserWorkspaceInjection = new AsyncLocalStorage();
|
|
151
175
|
function browserWorkspaceCommand(cli, args, stdin, signal) {
|
|
176
|
+
return Promise.resolve(withTestInjection('browserWorkspace.command', browserWorkspaceInjection.getStore(), () => rawBrowserWorkspaceCommand(cli, args, stdin, signal)));
|
|
177
|
+
}
|
|
178
|
+
function rawBrowserWorkspaceCommand(cli, args, stdin, signal) {
|
|
152
179
|
return new Promise((resolve, reject) => {
|
|
153
180
|
const child = spawnOwnedProcess(cli.command, [...cli.prefix, ...args], {
|
|
154
181
|
invocation: 'browser-workspace.command',
|
|
@@ -344,14 +371,23 @@ export class ChatGptAdapter {
|
|
|
344
371
|
if (controller.signal.aborted)
|
|
345
372
|
throw new Error('browser-worker execution aborted');
|
|
346
373
|
const policy = chatGptTabClosePolicy(task.chatgptTabClosePolicy, this.tabClosePolicy);
|
|
347
|
-
const
|
|
374
|
+
const submit = () => (this.runner ?? browserWorkerRunner())(definition, {
|
|
348
375
|
prompt: task.input,
|
|
349
376
|
...(task.attachments?.length ? { files: task.attachments } : {}),
|
|
350
377
|
...(task.chatgptThreadDelivery ? { thread: task.chatgptThreadDelivery } : {}),
|
|
351
378
|
tabClosePolicy: policy,
|
|
352
379
|
}, route, controller.signal);
|
|
380
|
+
const run = context.testInjection?.method === 'browserWorkspace.command'
|
|
381
|
+
? browserWorkspaceInjection.run(context.testInjection, submit) : submit();
|
|
353
382
|
try {
|
|
354
|
-
|
|
383
|
+
// Start acknowledgement may expire while browser submission is still pending.
|
|
384
|
+
// Race its rejection with delivery so a hung browser cannot hold a QUEUED
|
|
385
|
+
// execution until the full lease expires. A successful ack never shortcuts delivery.
|
|
386
|
+
const submission = run.submitted ?? run.promise;
|
|
387
|
+
const startAckFailure = context.waitForWorkerStart?.then(() => new Promise(() => { }), error => Promise.reject(error));
|
|
388
|
+
const output = await (startAckFailure
|
|
389
|
+
? Promise.race([submission, startAckFailure])
|
|
390
|
+
: submission);
|
|
355
391
|
const response = parseBrowserWorkerResponse(output);
|
|
356
392
|
if (task.chatgptThreadDelivery && (typeof response.thread_id !== 'string' || response.thread_id !== task.chatgptThreadDelivery.threadId)) {
|
|
357
393
|
throw new Error('browser-worker submitted to an unexpected ChatGPT thread');
|
package/dist/blockers.js
CHANGED
|
@@ -38,10 +38,23 @@ export async function registerBlockerDependency(options) {
|
|
|
38
38
|
}
|
|
39
39
|
if (blocker.blockerResolutionMode && blocker.blockerResolutionMode !== requestedMode)
|
|
40
40
|
throw new Error(`Blocker ${blocker.id} already uses ${blocker.blockerResolutionMode} resolution`);
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
41
|
+
if (options.blockerStore.mutateJob) {
|
|
42
|
+
await options.blockerStore.mutateJob(blocker.id, current => {
|
|
43
|
+
if (current.blockerResolutionMode && current.blockerResolutionMode !== requestedMode)
|
|
44
|
+
throw new Error(`Blocker ${current.id} already uses ${current.blockerResolutionMode} resolution`);
|
|
45
|
+
current.blockerResolutionMode = requestedMode;
|
|
46
|
+
if (!(current.unblocks ?? []).some(item => dependentKey(item) === dependentKey(relation)))
|
|
47
|
+
current.unblocks = [...(current.unblocks ?? []), relation];
|
|
48
|
+
current.updatedAt = now;
|
|
49
|
+
return current;
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
blocker.blockerResolutionMode = requestedMode;
|
|
54
|
+
blocker.unblocks = [...(blocker.unblocks ?? []), relation];
|
|
55
|
+
blocker.updatedAt = now;
|
|
56
|
+
await options.blockerStore.saveJob(blocker);
|
|
57
|
+
}
|
|
45
58
|
task.blockedBy = taskBlocker;
|
|
46
59
|
task.updatedAt = now;
|
|
47
60
|
await options.dependentStore.saveTask(task);
|
package/dist/cli-blocker.js
CHANGED
|
@@ -1,43 +1,27 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { registerBlockerDependency } from './blockers.js';
|
|
3
|
-
import { arg, publishDependentUnblocked, repository, withEventBus } from './cli-support.js';
|
|
1
|
+
import { arg, relayControlCommand, relayControlJob, repository } from './cli-support.js';
|
|
4
2
|
import { usage } from './cli-help.js';
|
|
5
3
|
export async function runBlockerCommand(action, args) {
|
|
6
4
|
if (!['add', 'resolve', 'show'].includes(action))
|
|
7
5
|
throw new Error(usage);
|
|
8
|
-
|
|
6
|
+
repository(args);
|
|
9
7
|
const id = arg(args, '--id');
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
throw new Error('blocker commands require --repo, --pr, and --id for the blocker Job');
|
|
13
|
-
const blockerStore = openLocalManagedStore(repo, pr).store;
|
|
8
|
+
if (!id)
|
|
9
|
+
throw new Error('blocker commands require --repo and --id for the blocker Job');
|
|
14
10
|
if (action === 'show') {
|
|
15
|
-
console.log(JSON.stringify(await
|
|
11
|
+
console.log(JSON.stringify(await relayControlJob(args, id), null, 2));
|
|
16
12
|
return;
|
|
17
13
|
}
|
|
18
14
|
if (action === 'resolve') {
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
console.log(JSON.stringify({ blocker: id, resolution: 'verified', ...summary }, null, 2));
|
|
15
|
+
const result = await relayControlCommand(args, { command: 'blocker.resolve', jobId: id, resolution: 'verified' });
|
|
16
|
+
console.log(JSON.stringify({ blocker: id, resolution: 'verified', ...result }, null, 2));
|
|
22
17
|
return;
|
|
23
18
|
}
|
|
24
|
-
const
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
if (!dependentRepo || !Number.isInteger(dependentPr) || dependentPr <= 0 || !dependentJob || !dependentTask) {
|
|
29
|
-
throw new Error('blocker add requires --dependent-repo, --dependent-pr, --dependent-job, and --dependent-task');
|
|
30
|
-
}
|
|
19
|
+
const dependentJobId = arg(args, '--dependent-job');
|
|
20
|
+
const dependentTaskId = arg(args, '--dependent-task');
|
|
21
|
+
if (!dependentJobId || !dependentTaskId)
|
|
22
|
+
throw new Error('blocker add requires --dependent-job and --dependent-task');
|
|
31
23
|
const modeValue = arg(args, '--resolution', 'merge');
|
|
32
24
|
if (!['merge', 'verified'].includes(modeValue))
|
|
33
25
|
throw new Error('--resolution must be merge or verified');
|
|
34
|
-
|
|
35
|
-
blockerStore,
|
|
36
|
-
blockerJobId: id,
|
|
37
|
-
dependentStore: openLocalManagedStore(dependentRepo, dependentPr).store,
|
|
38
|
-
dependentJobId: dependentJob,
|
|
39
|
-
dependentTaskId: dependentTask,
|
|
40
|
-
resolutionMode: modeValue,
|
|
41
|
-
});
|
|
42
|
-
console.log(JSON.stringify(relation, null, 2));
|
|
26
|
+
console.log(JSON.stringify(await relayControlCommand(args, { command: 'blocker.add', jobId: id, dependentJobId, dependentTaskId, resolutionMode: modeValue }), null, 2));
|
|
43
27
|
}
|
package/dist/cli-help.js
CHANGED
|
@@ -59,12 +59,12 @@ Optional: --thread-id THREAD_ID persists a reviewer override; omitted uses Job r
|
|
|
59
59
|
'job create': `Create a SQLite-authoritative Job.
|
|
60
60
|
|
|
61
61
|
Required: --repo OWNER/REPO, --id JOB_ID, and either --head BRANCH or --pr NUMBER.
|
|
62
|
-
With --head,
|
|
63
|
-
Common: --base BRANCH (default: main), --title TEXT, --objective-file PATH|- (preferred), --objective MARKDOWN (short/simple only), --project-path RELATIVE_PATH, --priority P0|P1|P2|P3, --mode fixed|autonomous, --no-review, --tag JSON, --reviewer-thread-id ID, --creator-thread-id ID. --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.
|
|
64
|
-
Job creation always enables auto-merge
|
|
62
|
+
With --head, bind the local source branch and use --base as its local merge target without a GitHub query. With --pr, bind an existing external PR reference as optional metadata without a GitHub query.
|
|
63
|
+
Common: --base BRANCH (default: main), --title TEXT, --objective-file PATH|- (preferred), --objective MARKDOWN (short/simple only), --project-path RELATIVE_PATH, --priority P0|P1|P2|P3, --mode fixed|autonomous, --initial-plan, --no-review, --tag JSON, --reviewer-thread-id ID, --creator-thread-id ID. --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.
|
|
64
|
+
Job creation always enables auto-merge. By default it is create-only; --initial-plan follows durable Job creation with the first real Planner round, activates the Job, and publishes the normal wake so Relay can continue without a second orchestrator command. Re-running create against an already healthy Job is idempotent and does not mutate its metadata.
|
|
65
65
|
|
|
66
66
|
Example:
|
|
67
|
-
npx agents-relay job create --repo OWNER/REPO --head feat/example --base main --id job-1 --title "Objective" --objective-file job.md`,
|
|
67
|
+
npx agents-relay job create --repo OWNER/REPO --head feat/example --base main --id job-1 --title "Objective" --objective-file job.md --initial-plan`,
|
|
68
68
|
'job update': `Partially update mutable metadata on a healthy existing Job.
|
|
69
69
|
|
|
70
70
|
Required: --repo OWNER/REPO, --pr NUMBER, and --id JOB_ID.
|
|
@@ -144,6 +144,9 @@ Required: --task-id ID, --summary TEXT, and the normal Job storage options. The
|
|
|
144
144
|
'task retry': `Retry one durable Task.
|
|
145
145
|
|
|
146
146
|
Required: --task-id ID and the normal Job storage options.`,
|
|
147
|
+
'task recover': `Retry one failed/blocked durable Task from its active automatic Troubleshooter without cancelling that recovery Task.
|
|
148
|
+
|
|
149
|
+
Required: --task-id ID, --recovery-task-id TROUBLESHOOTER_ID, and the normal Job storage options. The recovery Task must be an active Troubleshooter whose parentTaskId is the original Task.`,
|
|
147
150
|
'task cancel': `Cancel one durable Task.
|
|
148
151
|
|
|
149
152
|
Required: --task-id ID and the normal Job storage options.`,
|
|
@@ -216,7 +219,7 @@ Example:
|
|
|
216
219
|
npx agents-relay serve --repo OWNER/REPO --pr 12 --id job-1`,
|
|
217
220
|
'agent-register': `Register an agent role and its execution endpoint.
|
|
218
221
|
|
|
219
|
-
Required: --
|
|
222
|
+
Required for durable service mode: --repo OWNER/REPO --id JOB_ID plus --name NAME --responsibility TEXT --role ROLE --endpoint TARGET. Use --file only for explicit demo mode.
|
|
220
223
|
Common: --capabilities a,b, --runtime shell|codex|chatgpt, --provider NAME, --model NAME.
|
|
221
224
|
|
|
222
225
|
Example:
|
|
@@ -224,8 +227,8 @@ Example:
|
|
|
224
227
|
projection: `Inspect and explicitly repair deterministic V2 GitHub projection conflicts.
|
|
225
228
|
|
|
226
229
|
Usage:
|
|
227
|
-
npx agents-relay projection conflicts --repo OWNER/REPO --
|
|
228
|
-
npx agents-relay projection resolve --repo OWNER/REPO --
|
|
230
|
+
npx agents-relay projection conflicts --repo OWNER/REPO --id JOB_ID
|
|
231
|
+
npx agents-relay projection resolve --repo OWNER/REPO --id JOB_ID --projection-id ID --action requeue|discard-local
|
|
229
232
|
|
|
230
233
|
A deterministic same-id/different-definition append conflict is classified once, stops
|
|
231
234
|
consuming retry budget, and retains structured diagnostic evidence. It is never
|
|
@@ -234,7 +237,7 @@ auto-overwritten: an operator resolves it after reconciling the divergent defini
|
|
|
234
237
|
discard-local Drop the blocked local mutation so a fresh one can be enqueued`,
|
|
235
238
|
'agent-discover': `Find registered agents using hard filters.
|
|
236
239
|
|
|
237
|
-
Required: --file
|
|
240
|
+
Required for durable service mode: --repo OWNER/REPO --id JOB_ID. Use --file only for explicit demo mode.
|
|
238
241
|
Common: --capabilities a,b, --runtime NAME, --availability available,busy, --task-kind NAME, --explore.
|
|
239
242
|
|
|
240
243
|
Example:
|
package/dist/cli-job-create.js
CHANGED
|
@@ -1,31 +1,31 @@
|
|
|
1
1
|
import { openLocalManagedStore } from './local-managed-store.js';
|
|
2
2
|
import { createJobEntity } from './workflow/entity-creation.js';
|
|
3
3
|
import { normalizeThreadId } from './reviewer-thread.js';
|
|
4
|
-
import { discoverLocalRelayStoreSnapshots } from './relay-store.js';
|
|
5
|
-
import { homedir } from 'node:os';
|
|
6
|
-
import { join } from 'node:path';
|
|
4
|
+
import { discoverLocalRelayStoreSnapshots, relayStoreDirectory } from './relay-store.js';
|
|
7
5
|
/**
|
|
8
|
-
* Job lifecycle bootstrap: SQLite is authoritative.
|
|
9
|
-
*
|
|
6
|
+
* Job lifecycle bootstrap: SQLite is authoritative. No GitHub request is
|
|
7
|
+
* required; an explicit PR number is optional external metadata.
|
|
10
8
|
*/
|
|
11
9
|
export async function ensureManagedGitHubJob(_client, repositoryName, _trustedAuthors, options) {
|
|
12
10
|
if (!options.id)
|
|
13
11
|
throw new Error('--id is required for managed local Jobs');
|
|
14
|
-
const
|
|
15
|
-
if (!Number.isInteger(
|
|
12
|
+
const requestedNumber = options.prNumber ?? 0;
|
|
13
|
+
if (!Number.isInteger(requestedNumber) || requestedNumber < 0)
|
|
16
14
|
throw new Error('Invalid external PR binding number');
|
|
17
|
-
const
|
|
18
|
-
body: options.body ?? options.objective ?? '', state: 'OPEN', head: '', base: options.base ?? '' };
|
|
19
|
-
const directory = options.storeDirectory ??
|
|
15
|
+
const placeholder = { number: requestedNumber, title: options.title ?? options.id,
|
|
16
|
+
body: options.body ?? options.objective ?? '', state: 'OPEN', head: options.head ?? '', base: options.base ?? '' };
|
|
17
|
+
const directory = options.storeDirectory ?? relayStoreDirectory();
|
|
20
18
|
const alreadyKnown = (await discoverLocalRelayStoreSnapshots(directory)).flatMap(snapshot => snapshot.jobs.filter(job => job.id === options.id).map(job => ({ job, path: snapshot.path })));
|
|
21
19
|
if (alreadyKnown.length > 1)
|
|
22
20
|
throw new Error('Duplicate immutable Job ID across SQLite stores: ' + options.id);
|
|
23
21
|
if (alreadyKnown.length === 1) {
|
|
24
22
|
const existing = alreadyKnown[0].job;
|
|
25
|
-
if (existing.repository !== repositoryName || (options.prNumber !== undefined && existing.prNumber !==
|
|
23
|
+
if (existing.repository !== repositoryName || (options.prNumber !== undefined && existing.prNumber !== requestedNumber))
|
|
26
24
|
throw new Error('Immutable Job ID is already bound to a different repository or external PR');
|
|
27
|
-
return { job: existing, pr: { ...
|
|
25
|
+
return { job: existing, pr: { ...placeholder, number: existing.prNumber } };
|
|
28
26
|
}
|
|
27
|
+
const pr = placeholder;
|
|
28
|
+
const number = pr.number;
|
|
29
29
|
const local = openLocalManagedStore(repositoryName, number, {
|
|
30
30
|
directory, jobId: options.id, noGitHub: true,
|
|
31
31
|
});
|
|
@@ -34,6 +34,12 @@ export async function ensureManagedGitHubJob(_client, repositoryName, _trustedAu
|
|
|
34
34
|
if (existing)
|
|
35
35
|
return { job: existing, pr };
|
|
36
36
|
const now = new Date().toISOString();
|
|
37
|
+
const localMergeSource = options.localMergeSource ?? (options.head ? `refs/heads/${options.head}` : undefined);
|
|
38
|
+
if (options.localMergeTarget && options.localMergeTarget !== 'refs/heads/main')
|
|
39
|
+
throw new Error('Local merge target must always be main');
|
|
40
|
+
if (options.base && options.base !== 'main')
|
|
41
|
+
throw new Error('Job base must always be main');
|
|
42
|
+
const localMergeTarget = localMergeSource ? 'refs/heads/main' : undefined;
|
|
37
43
|
const job = createJobEntity({
|
|
38
44
|
id: options.id,
|
|
39
45
|
title: options.title || pr.title || 'Agents Relay job',
|
|
@@ -44,7 +50,7 @@ export async function ensureManagedGitHubJob(_client, repositoryName, _trustedAu
|
|
|
44
50
|
executionMode: options.executionMode ?? 'fixed',
|
|
45
51
|
tag: options.tag,
|
|
46
52
|
autoMerge: true,
|
|
47
|
-
...(
|
|
53
|
+
...(localMergeSource && localMergeTarget ? { localMerge: { sourceRef: localMergeSource, targetRef: localMergeTarget, status: 'pending' } } : {}),
|
|
48
54
|
reviewEnabled: options.reviewEnabled ?? true,
|
|
49
55
|
...(options.reviewerThreadId
|
|
50
56
|
? { reviewerThreadId: normalizeThreadId(options.reviewerThreadId, 'reviewer thread ID') }
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
export function configureLocalGitMerge(job, source, target, localOnly) {
|
|
2
2
|
// The GitHub integration is observational; local merge applies in either mode.
|
|
3
3
|
void localOnly;
|
|
4
|
-
if (!source
|
|
5
|
-
throw new Error('
|
|
4
|
+
if (!source)
|
|
5
|
+
throw new Error('Local merge source is required');
|
|
6
|
+
if (target !== 'refs/heads/main')
|
|
7
|
+
throw new Error('Local Git merge target must always be refs/heads/main');
|
|
6
8
|
if (!['ACTIVE', 'DRAFT'].includes(job.state))
|
|
7
9
|
throw new Error('Cannot reconfigure merge of a terminal Job');
|
|
8
10
|
job.localMerge = { sourceRef: source, targetRef: target, status: 'pending' };
|
package/dist/cli-projection.js
CHANGED
|
@@ -1,61 +1,29 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { join } from 'node:path';
|
|
3
|
-
import { SQLiteRelayStore, relayStorePath } from './relay-store.js';
|
|
4
|
-
import { arg, integerArg, pullRequest, repository } from './cli-support.js';
|
|
5
|
-
const DEFAULT_STORE_DIR = join(homedir(), '.agents-relay', 'stores');
|
|
6
|
-
function storePath(args) {
|
|
7
|
-
const explicit = arg(args, '--store');
|
|
8
|
-
if (explicit)
|
|
9
|
-
return explicit;
|
|
10
|
-
return relayStorePath(arg(args, '--store-dir', DEFAULT_STORE_DIR), repository(args), pullRequest(args));
|
|
11
|
-
}
|
|
1
|
+
import { arg, integerArg, relayControlCommand } from './cli-support.js';
|
|
12
2
|
export async function runProjectionCommand(action, args) {
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
}
|
|
20
|
-
if (action === 'resolve') {
|
|
21
|
-
const id = integerArg(args, '--projection-id', 0);
|
|
22
|
-
if (id <= 0)
|
|
23
|
-
throw new Error('projection resolve requires --projection-id ID');
|
|
24
|
-
const raw = arg(args, '--action', 'requeue');
|
|
25
|
-
if (raw !== 'requeue' && raw !== 'discard-local' && raw !== 'discard_local')
|
|
26
|
-
throw new Error('--action must be requeue or discard-local');
|
|
27
|
-
const resolution = raw === 'requeue' ? 'requeue' : 'discard_local';
|
|
28
|
-
const note = arg(args, '--note') || undefined;
|
|
29
|
-
const resolved = await store.resolveProjectionConflict(id, { action: resolution, ...(note ? { note } : {}) });
|
|
30
|
-
if (!resolved)
|
|
31
|
-
throw new Error(`Projection ${id} is not in a resolvable conflict state`);
|
|
32
|
-
console.log(`Resolved projection ${id} with ${resolution}`);
|
|
33
|
-
return;
|
|
34
|
-
}
|
|
35
|
-
throw new Error(`projection ${action || '<action>'} is not supported; use conflicts or resolve`);
|
|
3
|
+
const jobId = arg(args, '--id');
|
|
4
|
+
if (!jobId)
|
|
5
|
+
throw new Error('projection commands require --id JOB_ID');
|
|
6
|
+
if (action === 'conflicts' || action === 'list') {
|
|
7
|
+
console.log(JSON.stringify(await relayControlCommand(args, { command: 'projection.conflicts', jobId }), null, 2));
|
|
8
|
+
return;
|
|
36
9
|
}
|
|
37
|
-
|
|
38
|
-
|
|
10
|
+
if (action === 'resolve') {
|
|
11
|
+
const id = integerArg(args, '--projection-id', 0);
|
|
12
|
+
if (id <= 0)
|
|
13
|
+
throw new Error('projection resolve requires --projection-id ID');
|
|
14
|
+
const raw = arg(args, '--action', 'requeue');
|
|
15
|
+
if (!['requeue', 'discard-local', 'discard_local'].includes(raw))
|
|
16
|
+
throw new Error('--action must be requeue or discard-local');
|
|
17
|
+
const resolution = raw === 'requeue' ? 'requeue' : 'discard_local';
|
|
18
|
+
const note = arg(args, '--note') || undefined;
|
|
19
|
+
const result = await relayControlCommand(args, { command: 'projection.resolve', jobId, projectionId: id, action: resolution, ...(note ? { note } : {}) });
|
|
20
|
+
console.log(JSON.stringify(result, null, 2));
|
|
21
|
+
return;
|
|
39
22
|
}
|
|
23
|
+
throw new Error(`projection ${action || '<action>'} is not supported; use conflicts or resolve`);
|
|
40
24
|
}
|
|
41
25
|
export function projectionHelp(action) {
|
|
42
|
-
if (action === 'resolve')
|
|
43
|
-
return `Explicitly repair one classified projection conflict
|
|
44
|
-
|
|
45
|
-
Required: --projection-id ID --action requeue|discard-local, plus --store PATH or --repo OWNER/REPO --pr NUMBER.
|
|
46
|
-
Actions:
|
|
47
|
-
requeue Re-arm the recorded projection after the divergent side is reconciled
|
|
48
|
-
discard-local Drop the blocked local mutation so a fresh one can be enqueued
|
|
49
|
-
|
|
50
|
-
Never silently overwrites either side. The recorded diagnostic remains until resolution.`;
|
|
51
|
-
}
|
|
52
|
-
return `Inspect and explicitly repair deterministic V2 GitHub projection conflicts.
|
|
53
|
-
|
|
54
|
-
Usage:
|
|
55
|
-
npx agents-relay projection conflicts --repo OWNER/REPO --pr NUMBER [--store PATH]
|
|
56
|
-
npx agents-relay projection resolve --repo OWNER/REPO --pr NUMBER --projection-id ID --action requeue|discard-local
|
|
57
|
-
|
|
58
|
-
A deterministic same-id/different-definition append conflict is classified once, stops
|
|
59
|
-
consuming retry budget, and retains structured diagnostic evidence. It is never
|
|
60
|
-
auto-overwritten: an operator resolves it after reconciling the divergent definition.`;
|
|
26
|
+
if (action === 'resolve')
|
|
27
|
+
return `Explicitly repair one classified projection conflict.\n\nRequired: --repo OWNER/REPO --id JOB_ID --projection-id ID --action requeue|discard-local.`;
|
|
28
|
+
return `Inspect and explicitly repair deterministic V2 GitHub projection conflicts.\n\nUsage:\n npx agents-relay projection conflicts --repo OWNER/REPO --id JOB_ID\n npx agents-relay projection resolve --repo OWNER/REPO --id JOB_ID --projection-id ID --action requeue|discard-local`;
|
|
61
29
|
}
|
package/dist/cli-review.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { normalizeThreadId
|
|
1
|
+
import { arg, hasArg, relayControlCommand } from './cli-support.js';
|
|
2
|
+
import { normalizeThreadId } from './reviewer-thread.js';
|
|
3
3
|
export function reviewRequestThreadOverride(args) {
|
|
4
4
|
const values = [...args];
|
|
5
5
|
return hasArg(values, '--thread-id')
|
|
@@ -9,27 +9,10 @@ export function reviewRequestThreadOverride(args) {
|
|
|
9
9
|
export async function runReviewCommand(action, reviewArgs) {
|
|
10
10
|
if (action !== 'request')
|
|
11
11
|
throw new Error('review supports: request');
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
const jobId = arg(reviewArgs, '--id');
|
|
13
|
+
if (!arg(reviewArgs, '--repo') || !jobId)
|
|
14
|
+
throw new Error('review request requires --repo and --id');
|
|
14
15
|
const threadId = reviewRequestThreadOverride(reviewArgs);
|
|
15
|
-
const {
|
|
16
|
-
|
|
17
|
-
throw new Error('review request requires a managed GitHub Job');
|
|
18
|
-
if (job.state === 'CANCELLED' || job.state === 'COMPLETED')
|
|
19
|
-
throw new Error('Cannot request review for terminal Job');
|
|
20
|
-
if (!job.reviewEnabled)
|
|
21
|
-
throw new Error('Review disabled for this Job');
|
|
22
|
-
if (job.tasks.some(task => task.agentName === 'reviewer' && ['RUNNING', 'QUEUED', 'DRAFT'].includes(task.state)))
|
|
23
|
-
throw new Error('Review already queued or running; refusing duplicate request');
|
|
24
|
-
if (job.review?.outcome === 'approved')
|
|
25
|
-
throw new Error('Current head already approved; review request not needed');
|
|
26
|
-
if (threadId !== undefined) {
|
|
27
|
-
job.reviewerThreadId = threadId;
|
|
28
|
-
job.updatedAt = new Date().toISOString();
|
|
29
|
-
await store.saveJob(job);
|
|
30
|
-
}
|
|
31
|
-
const destination = resolveReviewerThreadDestination(job);
|
|
32
|
-
await withEventBus(reviewArgs, async (bus) => requestManagedReconciliation(bus, job));
|
|
33
|
-
console.log(JSON.stringify({ jobId: job.id, pr: job.prNumber, reviewerThreadId: destination?.threadId ?? null, status: 'requested', note: 'Relay graph will create the review Task once eligible; this is not a delivery receipt.' }, null, 2));
|
|
34
|
-
return;
|
|
16
|
+
const job = await relayControlCommand(reviewArgs, { command: 'review.request', jobId, ...(threadId !== undefined ? { threadId } : {}) });
|
|
17
|
+
console.log(JSON.stringify({ jobId: job.id, pr: job.externalPrNumber ?? job.prNumber ?? 0, reviewerThreadId: job.reviewerThreadId ?? null, status: 'requested', note: 'Relay graph will create the review Task once eligible; this is not a delivery receipt.' }, null, 2));
|
|
35
18
|
}
|
package/dist/cli-support.js
CHANGED
|
@@ -4,8 +4,7 @@ import { githubAuthContext } from './github-auth.js';
|
|
|
4
4
|
import { noGitHubMode } from './no-github.js';
|
|
5
5
|
import { basename } from 'node:path';
|
|
6
6
|
import { createJobIdRegistryResolver } from './local-job-registry.js';
|
|
7
|
-
import {
|
|
8
|
-
import { join } from 'node:path';
|
|
7
|
+
import { relayStoreDirectory } from './relay-store.js';
|
|
9
8
|
import { NatsEventBus, eventFor } from './events.js';
|
|
10
9
|
import { parseJobSchedule } from './scheduler.js';
|
|
11
10
|
import { parseJobTagJson } from './task-tag.js';
|
|
@@ -73,12 +72,13 @@ export async function storeFor(args) {
|
|
|
73
72
|
const jobId = arg(args, '--id');
|
|
74
73
|
if (!repo || !jobId)
|
|
75
74
|
throw new Error('Local Job requires --repo and --id (or explicit --file)');
|
|
76
|
-
const directory = arg(args, '--store-dir') ||
|
|
75
|
+
const directory = arg(args, '--store-dir') || relayStoreDirectory();
|
|
77
76
|
const binding = await createJobIdRegistryResolver(directory, new Map())(jobId);
|
|
78
77
|
if (!binding || binding.job.repository !== repo)
|
|
79
78
|
throw new Error('Immutable local Job not found for repository: ' + jobId);
|
|
80
79
|
const suppliedPr = arg(args, '--pr');
|
|
81
|
-
|
|
80
|
+
const boundPr = binding.job.externalPrNumber ?? (binding.job.prNumber > 0 ? binding.job.prNumber : 0);
|
|
81
|
+
if (suppliedPr && boundPr !== Number(suppliedPr))
|
|
82
82
|
throw new Error('Local Job external PR binding mismatch');
|
|
83
83
|
return { store: binding.store, job: await binding.store.load(jobId), localFile: null };
|
|
84
84
|
}
|
|
@@ -107,6 +107,61 @@ export async function relayControlJob(args, jobId) {
|
|
|
107
107
|
throw new Error(`Agents Relay job request failed: ${await response.text() || `HTTP ${response.status}`}`);
|
|
108
108
|
return await response.json();
|
|
109
109
|
}
|
|
110
|
+
export async function relayControlCommand(args, body) {
|
|
111
|
+
const host = arg(args, '--host', '127.0.0.1');
|
|
112
|
+
const port = Number(arg(args, '--port', String(SERVICE_DEFAULTS.port)));
|
|
113
|
+
if (!Number.isInteger(port) || port <= 0 || port > 65535)
|
|
114
|
+
throw new Error('--port must be an integer between 1 and 65535');
|
|
115
|
+
let response;
|
|
116
|
+
try {
|
|
117
|
+
response = await fetch(`http://${host}:${port}/api/control`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) });
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
throw new Error(`Agents Relay service unavailable at ${host}:${port}: ${error instanceof Error ? error.message : String(error)}`);
|
|
121
|
+
}
|
|
122
|
+
if (!response.ok)
|
|
123
|
+
throw new Error(`Agents Relay control command failed: ${await response.text() || `HTTP ${response.status}`}`);
|
|
124
|
+
return await response.json();
|
|
125
|
+
}
|
|
126
|
+
export async function relayJobList(args) {
|
|
127
|
+
const host = arg(args, '--host', '127.0.0.1');
|
|
128
|
+
const port = Number(arg(args, '--port', String(SERVICE_DEFAULTS.port)));
|
|
129
|
+
const repo = arg(args, '--repo');
|
|
130
|
+
let response;
|
|
131
|
+
try {
|
|
132
|
+
response = await fetch(`http://${host}:${port}/api/jobs`);
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
throw new Error(`Agents Relay service unavailable at ${host}:${port}: ${error instanceof Error ? error.message : String(error)}`);
|
|
136
|
+
}
|
|
137
|
+
if (!response.ok)
|
|
138
|
+
throw new Error(`Agents Relay jobs request failed: ${await response.text() || `HTTP ${response.status}`}`);
|
|
139
|
+
const jobs = await response.json();
|
|
140
|
+
return repo ? jobs.filter(job => job.repository === repo) : jobs;
|
|
141
|
+
}
|
|
142
|
+
export async function relayJobStateCommand(args, jobId, state) {
|
|
143
|
+
const host = arg(args, '--host', '127.0.0.1');
|
|
144
|
+
const port = Number(arg(args, '--port', String(SERVICE_DEFAULTS.port)));
|
|
145
|
+
if (!Number.isInteger(port) || port <= 0 || port > 65535)
|
|
146
|
+
throw new Error('--port must be an integer between 1 and 65535');
|
|
147
|
+
const params = new URLSearchParams({ id: jobId, state });
|
|
148
|
+
const repo = arg(args, '--repo');
|
|
149
|
+
const pr = Number(arg(args, '--pr'));
|
|
150
|
+
if (repo)
|
|
151
|
+
params.set('repo', repo);
|
|
152
|
+
if (Number.isInteger(pr) && pr > 0)
|
|
153
|
+
params.set('pr', String(pr));
|
|
154
|
+
let response;
|
|
155
|
+
try {
|
|
156
|
+
response = await fetch(`http://${host}:${port}/api/job-state?${params.toString()}`, { method: 'POST' });
|
|
157
|
+
}
|
|
158
|
+
catch (error) {
|
|
159
|
+
throw new Error(`Agents Relay service unavailable at ${host}:${port}: ${error instanceof Error ? error.message : String(error)}`);
|
|
160
|
+
}
|
|
161
|
+
if (!response.ok)
|
|
162
|
+
throw new Error(`Agents Relay job state command failed: ${await response.text() || `HTTP ${response.status}`}`);
|
|
163
|
+
return await response.json();
|
|
164
|
+
}
|
|
110
165
|
export async function relayTaskCommand(args, body) {
|
|
111
166
|
const host = arg(args, '--host', '127.0.0.1');
|
|
112
167
|
const port = Number(arg(args, '--port', String(SERVICE_DEFAULTS.port)));
|