agents-relay 2.0.67 → 2.0.69
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +56 -20
- package/dist/adapters.js +39 -3
- package/dist/blockers.js +17 -4
- package/dist/cli-blocker.js +12 -30
- package/dist/cli-help.js +22 -17
- package/dist/cli-job-create.js +77 -0
- package/dist/cli-local-merge-config.js +11 -0
- package/dist/cli-projection.js +23 -55
- package/dist/cli-review.js +7 -24
- package/dist/cli-support.js +81 -8
- package/dist/cli-worker-task.js +63 -0
- package/dist/cli.js +223 -185
- package/dist/dashboard-html.js +15 -12
- package/dist/dashboard.js +27 -2
- package/dist/e2e-injection.js +76 -0
- package/dist/github-auth.js +4 -3
- 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 +43 -10
- package/dist/job-actions.js +29 -0
- package/dist/local-authority-store.js +288 -0
- package/dist/local-commit-events.js +2 -0
- package/dist/local-git-merge.js +124 -0
- package/dist/local-job-path.js +7 -0
- package/dist/local-job-registry.js +29 -0
- package/dist/local-managed-store.js +21 -0
- package/dist/local-merge-delivery.js +53 -0
- package/dist/local-merge-lock.js +103 -0
- 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/no-github.js +11 -0
- package/dist/planned-review-head.js +7 -8
- package/dist/planner-runtime.js +182 -41
- package/dist/planner.js +35 -2
- package/dist/pool.js +89 -136
- package/dist/projected-store.js +20 -3
- package/dist/projection-outbox.js +3 -1
- package/dist/publication-store.js +37 -0
- package/dist/reconciler-cancellation.js +21 -0
- package/dist/reconciler-review.js +99 -29
- package/dist/reconciler-runtime.js +72 -35
- package/dist/reconciler.js +124 -85
- package/dist/relay-store.js +108 -6
- 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 +304 -299
- package/dist/review-generation-fence.js +12 -0
- package/dist/runtime-status-events.js +21 -0
- package/dist/runtime-status.js +21 -7
- package/dist/scheduled-jobs.js +43 -36
- package/dist/service-external-actions.js +27 -0
- package/dist/service-publication.js +29 -0
- package/dist/store-github.js +17 -6
- package/dist/task-actions.js +59 -1
- package/dist/task-update.js +1 -1
- package/dist/troubleshooter.js +21 -2
- package/dist/unblock.js +47 -15
- package/dist/worker-rejection.js +11 -0
- package/dist/workflow/contracts.js +10 -1
- package/dist/workflow/definitions-v2.js +2 -0
- package/dist/workflow/job-reconcile-v2.js +9 -60
- package/dist/workflow/runtime.js +18 -3
- package/dist/workflow-effect-failures.js +30 -0
- package/dist/workspace.js +95 -6
- package/package.json +6 -3
- package/skills/agents-relay/agents/planner.agent.md +3 -0
- package/skills/agents-relay/contracts/orchestrator-workflows.md +4 -7
- package/workflow/job-lifecycle.mmd +19 -60
- package/workflow/review-lifecycle.mmd +5 -8
package/README.md
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
# Agents Relay
|
|
2
2
|
|
|
3
|
-
Agents Relay is a
|
|
3
|
+
Agents Relay is a TypeScript runtime for durable asynchronous agent Jobs driven locally by SQLite and canonical workflow Graphs. The EventBus wakes reconciliation; an EventBus message or GitHub PR comment is never the lifecycle authority. Job, Plan, Task, Review, Execution, Lease and Recovery state survive daemon restarts in SQLite.
|
|
4
|
+
|
|
5
|
+
V2 uses a read-only local SQLite Job Registry for discovery. The immutable jobId selects the Job Store even when prNumber is zero; scheduled slot claims are atomic across processes. GitHub is **audit-only**: its PR/head/publish events are persisted for reporting, never consulted to start, stop, or complete local Workflows. Local Git delivery and its confirmed merge receipt—not a remote PR MERGED fact—gate Graph completion. Job creation and local Task operations require no GitHub authentication or PR.
|
|
4
6
|
|
|
5
|
-
V2 separates **operational execution state** from that collaboration record. A local RelayStore may use SQLite/WAL for runtime snapshots, event idempotency and a durable GitHub projection outbox. ProjectedDurableStore bootstraps from the existing GitHub representation, commits Job/Plan/Task runtime changes locally, and lets GitHubProjectionWriter project them asynchronously. GitHub therefore remains the human-readable durable Job/Plan/Task/Review surface without being required for every high-frequency runtime transition.
|
|
6
7
|
|
|
7
8
|
The canonical V2 architecture contract is
|
|
8
9
|
[DESIGN-PRINCIPLES.md](DESIGN-PRINCIPLES.md). README sections below describe
|
|
@@ -10,23 +11,27 @@ the runtime; they do not replace that contract.
|
|
|
10
11
|
|
|
11
12
|
## V2 architecture
|
|
12
13
|
|
|
13
|
-
|
|
14
|
+
\`\`\`mermaid
|
|
14
15
|
flowchart LR
|
|
15
|
-
Human[Human / orchestrator] -->
|
|
16
|
-
|
|
17
|
-
Worker[Workers / adapters] --> Events[
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
Graphs -->
|
|
22
|
-
|
|
23
|
-
Store -->
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
16
|
+
Human[Human / orchestrator] --> CLI[CLI / Dashboard]
|
|
17
|
+
CLI --> Store[(SQLite / WAL<br/>Job Task Plan Review truth)]
|
|
18
|
+
Worker[Workers / adapters] --> Events[EventBus<br/>wake + observations]
|
|
19
|
+
Events --> Coordinator[ReconcileCoordinator]
|
|
20
|
+
Store --> Coordinator
|
|
21
|
+
Coordinator --> Graphs[Canonical Mermaid Graph Engine]
|
|
22
|
+
Graphs --> Effects[Stateless actions / effects]
|
|
23
|
+
Effects --> Store
|
|
24
|
+
Store --> Views[Dashboard / Scheduler / Recovery]
|
|
25
|
+
Effects --> Outbox[Durable external-action outbox]
|
|
26
|
+
Outbox --> GH[GitHub Commit / PR / Review / Merge API]
|
|
27
|
+
GH --> Facts[Confirmed external facts]
|
|
28
|
+
Facts --> Store
|
|
29
|
+
\`\`\`
|
|
30
|
+
|
|
31
|
+
SQLite holds the sole lifecycle authority; the Graph Engine selects every transition.
|
|
32
|
+
GitHub operations are explicitly requested and confirmed by adapters, never used to
|
|
33
|
+
discover or reconstruct Job/Task state. EventBus delivery wakes reconciliation;
|
|
34
|
+
it cannot replace the durable SQLite record.
|
|
30
35
|
|
|
31
36
|
### Workflow graph navigation
|
|
32
37
|
|
|
@@ -53,7 +58,7 @@ These paths always represent the current canonical graphs. Historical graph defi
|
|
|
53
58
|
|
|
54
59
|
## Quick start
|
|
55
60
|
|
|
56
|
-
For
|
|
61
|
+
For new work, create a local Job through Agents Relay. The CLI can bind a known PR number without GitHub access; creating a new PR head uses the external GitHub adapter. Tasks are validated against the SQLite Job, not GitHub markers.
|
|
57
62
|
|
|
58
63
|
~~~sh
|
|
59
64
|
cat > job.md <<'EOF'
|
|
@@ -183,7 +188,7 @@ Pushes to `main` run `.github/workflows/publish.yml`: install, test, choose the
|
|
|
183
188
|
|
|
184
189
|
Normal consumers should invoke the CLI with `npx agents-relay ...` (or `npx agents-relayd ...`).
|
|
185
190
|
|
|
186
|
-
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.
|
|
187
192
|
|
|
188
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.
|
|
189
194
|
|
|
@@ -300,3 +305,34 @@ For autonomous Jobs, `UNBLOCKED` is historical: Relay does not retry that old Ta
|
|
|
300
305
|
### Browser Workspace product boundary
|
|
301
306
|
|
|
302
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,45 +1,27 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { githubAuthContext } from './github-auth.js';
|
|
3
|
-
import { registerBlockerDependency } from './blockers.js';
|
|
4
|
-
import { arg, publishDependentUnblocked, repository, withEventBus } from './cli-support.js';
|
|
1
|
+
import { arg, relayControlCommand, relayControlJob, repository } from './cli-support.js';
|
|
5
2
|
import { usage } from './cli-help.js';
|
|
6
3
|
export async function runBlockerCommand(action, args) {
|
|
7
4
|
if (!['add', 'resolve', 'show'].includes(action))
|
|
8
5
|
throw new Error(usage);
|
|
9
|
-
|
|
6
|
+
repository(args);
|
|
10
7
|
const id = arg(args, '--id');
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
throw new Error('blocker commands require --repo, --pr, and --id for the blocker Job');
|
|
14
|
-
const auth = await githubAuthContext(args);
|
|
15
|
-
const blockerStore = new GitHubStore(auth.client, repo, pr, auth.trustedAuthors);
|
|
8
|
+
if (!id)
|
|
9
|
+
throw new Error('blocker commands require --repo and --id for the blocker Job');
|
|
16
10
|
if (action === 'show') {
|
|
17
|
-
console.log(JSON.stringify(await
|
|
11
|
+
console.log(JSON.stringify(await relayControlJob(args, id), null, 2));
|
|
18
12
|
return;
|
|
19
13
|
}
|
|
20
14
|
if (action === 'resolve') {
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
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));
|
|
24
17
|
return;
|
|
25
18
|
}
|
|
26
|
-
const
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
if (!dependentRepo || !Number.isInteger(dependentPr) || dependentPr <= 0 || !dependentJob || !dependentTask) {
|
|
31
|
-
throw new Error('blocker add requires --dependent-repo, --dependent-pr, --dependent-job, and --dependent-task');
|
|
32
|
-
}
|
|
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');
|
|
33
23
|
const modeValue = arg(args, '--resolution', 'merge');
|
|
34
24
|
if (!['merge', 'verified'].includes(modeValue))
|
|
35
25
|
throw new Error('--resolution must be merge or verified');
|
|
36
|
-
|
|
37
|
-
blockerStore,
|
|
38
|
-
blockerJobId: id,
|
|
39
|
-
dependentStore: new GitHubStore(auth.client, dependentRepo, dependentPr, auth.trustedAuthors),
|
|
40
|
-
dependentJobId: dependentJob,
|
|
41
|
-
dependentTaskId: dependentTask,
|
|
42
|
-
resolutionMode: modeValue,
|
|
43
|
-
});
|
|
44
|
-
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));
|
|
45
27
|
}
|
package/dist/cli-help.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
export const usage = 'agents-relay <job|task|review|plan|blocker|projection|escalation|context|init|record|agent-register|agent-discover|status|reconcile|serve> [options]';
|
|
2
2
|
const help = {
|
|
3
|
-
root: `Agents Relay coordinates durable asynchronous agent jobs through
|
|
3
|
+
root: `Agents Relay coordinates durable asynchronous agent jobs through local SQLite and workflow graphs.
|
|
4
4
|
|
|
5
5
|
Usage:
|
|
6
|
-
npx agents-relay <command> [options]
|
|
6
|
+
npx agents-relay <command> [options] [--no-github]
|
|
7
|
+
|
|
8
|
+
--no-github: SQLite/Graph-only mode. GitHub auth/API, PR creation, merge and fetch are disabled. Local completion is Graph-owned.
|
|
7
9
|
|
|
8
10
|
Commands:
|
|
9
|
-
job create|update|show|list|merge|unblock|cancel Manage
|
|
11
|
+
job create|update|show|list|merge|unblock|cancel Manage SQLite-authoritative jobs with optional GitHub PRs
|
|
10
12
|
task create|update|show|list|complete|retry|unblock|cancel Manage durable tasks
|
|
11
13
|
review request Request graph-owned review (optional thread override)
|
|
12
14
|
plan Invoke the Planner for an existing managed Job
|
|
@@ -24,7 +26,7 @@ Commands:
|
|
|
24
26
|
|
|
25
27
|
Run 'npx agents-relay <command> --help' for command options and examples.
|
|
26
28
|
Use 'node dist/cli.js' only for explicit local development/demo mode.`,
|
|
27
|
-
job: `Manage
|
|
29
|
+
job: `Manage SQLite-authoritative durable jobs.
|
|
28
30
|
|
|
29
31
|
Usage:
|
|
30
32
|
npx agents-relay job <create|update|show|list|merge|unblock|cancel> [options]
|
|
@@ -34,7 +36,7 @@ Actions:
|
|
|
34
36
|
update Partially update mutable metadata on a healthy existing Job
|
|
35
37
|
show Show one durable Job
|
|
36
38
|
list List managed Jobs in a repository
|
|
37
|
-
merge
|
|
39
|
+
merge Queue a retryable external PR merge; Graph completes the Job only after confirmation
|
|
38
40
|
unblock Authoritatively invalidate current BLOCKED decisions and trigger replanning/resume
|
|
39
41
|
cancel Close the backing PR and cancel the Job
|
|
40
42
|
|
|
@@ -54,20 +56,20 @@ When provided, --thread-id persists a reviewer destination; otherwise the Job re
|
|
|
54
56
|
|
|
55
57
|
Required: --repo OWNER/REPO --pr NUMBER --id JOB_ID.
|
|
56
58
|
Optional: --thread-id THREAD_ID persists a reviewer override; omitted uses Job reviewer/creator thread or legacy route. Rejects pending reviews and wakes managed reconciliation.`,
|
|
57
|
-
'job create': `Create a
|
|
59
|
+
'job create': `Create a SQLite-authoritative Job.
|
|
58
60
|
|
|
59
61
|
Required: --repo OWNER/REPO, --id JOB_ID, and either --head BRANCH or --pr NUMBER.
|
|
60
|
-
With --head,
|
|
61
|
-
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.
|
|
62
|
-
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.
|
|
63
65
|
|
|
64
66
|
Example:
|
|
65
|
-
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`,
|
|
66
68
|
'job update': `Partially update mutable metadata on a healthy existing Job.
|
|
67
69
|
|
|
68
70
|
Required: --repo OWNER/REPO, --pr NUMBER, and --id JOB_ID.
|
|
69
|
-
Mutable: --title, --objective-file PATH|- (preferred) or --objective MARKDOWN, --project-path RELATIVE_PATH, --priority, --mode fixed|autonomous, --schedule, --tag JSON, --auto-merge|--no-auto-merge, --review|--no-review, --reviewer-thread-id ID, --creator-thread-id ID, and continuation options.
|
|
70
|
-
Unspecified fields are preserved.
|
|
71
|
+
Mutable: --title, --objective-file PATH|- (preferred) or --objective MARKDOWN, --project-path RELATIVE_PATH, --priority, --mode fixed|autonomous, --schedule, --tag JSON, --auto-merge|--no-auto-merge, --review|--no-review, --reviewer-thread-id ID, --creator-thread-id ID, and continuation options. The objective updates local SQLite; PR body changes are independent external actions.
|
|
72
|
+
Unspecified fields are preserved. Local Job/PR identity mismatch is rejected.
|
|
71
73
|
|
|
72
74
|
Example:
|
|
73
75
|
npx agents-relay job update --repo OWNER/REPO --pr 12 --id job-1 --priority P0 --no-auto-merge`,
|
|
@@ -83,7 +85,7 @@ Required: --repo OWNER/REPO.
|
|
|
83
85
|
|
|
84
86
|
Example:
|
|
85
87
|
npx agents-relay job list --repo OWNER/REPO`,
|
|
86
|
-
'job merge': `
|
|
88
|
+
'job merge': `Request an idempotent external pull-request merge; return QUEUED until confirmed, when the Graph Engine completes the Job.
|
|
87
89
|
|
|
88
90
|
Required: --repo OWNER/REPO, --pr NUMBER, and --id JOB_ID. The current PR head SHA is pinned into the merge request.`,
|
|
89
91
|
'job unblock': `Authoritatively unblock the effective BLOCKED work Tasks in one Job.\n\nRequired: --repo OWNER/REPO --pr NUMBER --id JOB_ID --reason TEXT. Autonomous Jobs transition blocked work to historical UNBLOCKED state and wake Planner with unblock facts as Plan trigger/context; fixed Jobs record UNBLOCKED first and then resume according to fixed-job retry semantics.`,
|
|
@@ -142,6 +144,9 @@ Required: --task-id ID, --summary TEXT, and the normal Job storage options. The
|
|
|
142
144
|
'task retry': `Retry one durable Task.
|
|
143
145
|
|
|
144
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.`,
|
|
145
150
|
'task cancel': `Cancel one durable Task.
|
|
146
151
|
|
|
147
152
|
Required: --task-id ID and the normal Job storage options.`,
|
|
@@ -214,7 +219,7 @@ Example:
|
|
|
214
219
|
npx agents-relay serve --repo OWNER/REPO --pr 12 --id job-1`,
|
|
215
220
|
'agent-register': `Register an agent role and its execution endpoint.
|
|
216
221
|
|
|
217
|
-
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.
|
|
218
223
|
Common: --capabilities a,b, --runtime shell|codex|chatgpt, --provider NAME, --model NAME.
|
|
219
224
|
|
|
220
225
|
Example:
|
|
@@ -222,8 +227,8 @@ Example:
|
|
|
222
227
|
projection: `Inspect and explicitly repair deterministic V2 GitHub projection conflicts.
|
|
223
228
|
|
|
224
229
|
Usage:
|
|
225
|
-
npx agents-relay projection conflicts --repo OWNER/REPO --
|
|
226
|
-
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
|
|
227
232
|
|
|
228
233
|
A deterministic same-id/different-definition append conflict is classified once, stops
|
|
229
234
|
consuming retry budget, and retains structured diagnostic evidence. It is never
|
|
@@ -232,7 +237,7 @@ auto-overwritten: an operator resolves it after reconciling the divergent defini
|
|
|
232
237
|
discard-local Drop the blocked local mutation so a fresh one can be enqueued`,
|
|
233
238
|
'agent-discover': `Find registered agents using hard filters.
|
|
234
239
|
|
|
235
|
-
Required: --file
|
|
240
|
+
Required for durable service mode: --repo OWNER/REPO --id JOB_ID. Use --file only for explicit demo mode.
|
|
236
241
|
Common: --capabilities a,b, --runtime NAME, --availability available,busy, --task-kind NAME, --explore.
|
|
237
242
|
|
|
238
243
|
Example:
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { openLocalManagedStore } from './local-managed-store.js';
|
|
2
|
+
import { createJobEntity } from './workflow/entity-creation.js';
|
|
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';
|
|
7
|
+
/**
|
|
8
|
+
* Job lifecycle bootstrap: SQLite is authoritative. No GitHub request is
|
|
9
|
+
* required; an explicit PR number is optional external metadata.
|
|
10
|
+
*/
|
|
11
|
+
export async function ensureManagedGitHubJob(_client, repositoryName, _trustedAuthors, options) {
|
|
12
|
+
if (!options.id)
|
|
13
|
+
throw new Error('--id is required for managed local Jobs');
|
|
14
|
+
const requestedNumber = options.prNumber ?? 0;
|
|
15
|
+
if (!Number.isInteger(requestedNumber) || requestedNumber < 0)
|
|
16
|
+
throw new Error('Invalid external PR binding number');
|
|
17
|
+
const placeholder = { number: requestedNumber, title: options.title ?? options.id,
|
|
18
|
+
body: options.body ?? options.objective ?? '', state: 'OPEN', head: options.head ?? '', base: options.base ?? '' };
|
|
19
|
+
const directory = options.storeDirectory ?? join(homedir(), '.agents-relay', 'stores');
|
|
20
|
+
const alreadyKnown = (await discoverLocalRelayStoreSnapshots(directory)).flatMap(snapshot => snapshot.jobs.filter(job => job.id === options.id).map(job => ({ job, path: snapshot.path })));
|
|
21
|
+
if (alreadyKnown.length > 1)
|
|
22
|
+
throw new Error('Duplicate immutable Job ID across SQLite stores: ' + options.id);
|
|
23
|
+
if (alreadyKnown.length === 1) {
|
|
24
|
+
const existing = alreadyKnown[0].job;
|
|
25
|
+
if (existing.repository !== repositoryName || (options.prNumber !== undefined && existing.prNumber !== requestedNumber))
|
|
26
|
+
throw new Error('Immutable Job ID is already bound to a different repository or external PR');
|
|
27
|
+
return { job: existing, pr: { ...placeholder, number: existing.prNumber } };
|
|
28
|
+
}
|
|
29
|
+
const pr = placeholder;
|
|
30
|
+
const number = pr.number;
|
|
31
|
+
const local = openLocalManagedStore(repositoryName, number, {
|
|
32
|
+
directory, jobId: options.id, noGitHub: true,
|
|
33
|
+
});
|
|
34
|
+
try {
|
|
35
|
+
const existing = await local.relay.loadJob(options.id);
|
|
36
|
+
if (existing)
|
|
37
|
+
return { job: existing, pr };
|
|
38
|
+
const now = new Date().toISOString();
|
|
39
|
+
const localMergeSource = options.localMergeSource ?? (options.head ? `refs/heads/${options.head}` : undefined);
|
|
40
|
+
if (options.localMergeTarget && options.localMergeTarget !== 'refs/heads/main')
|
|
41
|
+
throw new Error('Local merge target must always be main');
|
|
42
|
+
if (options.base && options.base !== 'main')
|
|
43
|
+
throw new Error('Job base must always be main');
|
|
44
|
+
const localMergeTarget = localMergeSource ? 'refs/heads/main' : undefined;
|
|
45
|
+
const job = createJobEntity({
|
|
46
|
+
id: options.id,
|
|
47
|
+
title: options.title || pr.title || 'Agents Relay job',
|
|
48
|
+
objective: options.objective || pr.body || undefined,
|
|
49
|
+
projectPath: options.projectPath,
|
|
50
|
+
description: pr.body || undefined,
|
|
51
|
+
priority: options.priority ?? 'P2',
|
|
52
|
+
executionMode: options.executionMode ?? 'fixed',
|
|
53
|
+
tag: options.tag,
|
|
54
|
+
autoMerge: true,
|
|
55
|
+
...(localMergeSource && localMergeTarget ? { localMerge: { sourceRef: localMergeSource, targetRef: localMergeTarget, status: 'pending' } } : {}),
|
|
56
|
+
reviewEnabled: options.reviewEnabled ?? true,
|
|
57
|
+
...(options.reviewerThreadId
|
|
58
|
+
? { reviewerThreadId: normalizeThreadId(options.reviewerThreadId, 'reviewer thread ID') }
|
|
59
|
+
: {}),
|
|
60
|
+
...(options.creatorThreadId
|
|
61
|
+
? { creatorThreadId: normalizeThreadId(options.creatorThreadId, 'creator thread ID') }
|
|
62
|
+
: {}),
|
|
63
|
+
prNumber: pr.number,
|
|
64
|
+
repository: repositoryName,
|
|
65
|
+
continuation: options.continuation ?? null,
|
|
66
|
+
createdAt: now,
|
|
67
|
+
updatedAt: now,
|
|
68
|
+
tasks: [],
|
|
69
|
+
...(options.schedule ? { schedule: options.schedule } : {}),
|
|
70
|
+
}, { activate: Boolean(options.schedule) });
|
|
71
|
+
await local.store.saveJob(job);
|
|
72
|
+
return { job: await local.store.load(options.id), pr };
|
|
73
|
+
}
|
|
74
|
+
finally {
|
|
75
|
+
local.relay.close();
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export function configureLocalGitMerge(job, source, target, localOnly) {
|
|
2
|
+
// The GitHub integration is observational; local merge applies in either mode.
|
|
3
|
+
void localOnly;
|
|
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');
|
|
8
|
+
if (!['ACTIVE', 'DRAFT'].includes(job.state))
|
|
9
|
+
throw new Error('Cannot reconfigure merge of a terminal Job');
|
|
10
|
+
job.localMerge = { sourceRef: source, targetRef: target, status: 'pending' };
|
|
11
|
+
}
|
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
|
}
|