agents-relay 2.0.169 → 2.0.171
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 +13 -19
- package/dist/adapters.js +2 -8
- package/dist/blockers.js +3 -2
- package/dist/cli-help.js +3 -3
- package/dist/cli-job-create.js +2 -4
- package/dist/cli-support.js +5 -16
- package/dist/cli.js +9 -11
- package/dist/dashboard-html.js +5 -9
- package/dist/dashboard-style.js +0 -1
- package/dist/execution-recovery.js +1 -1
- package/dist/force-restate-validation.js +11 -1
- package/dist/interactive-resume.js +1 -1
- package/dist/job-actions.js +1 -1
- package/dist/job-pause.js +1 -1
- package/dist/legacy-blocked-task-migration.js +1 -1
- package/dist/local-authority-store.js +0 -57
- package/dist/local-git-merge.js +1 -2
- package/dist/local-merge-delivery.js +1 -1
- package/dist/markers.js +5 -6
- package/dist/planned-review-head.js +2 -2
- package/dist/planner-runtime.js +3 -2
- package/dist/planner.js +2 -7
- package/dist/pool.js +7 -36
- package/dist/reconciler-review.js +4 -5
- package/dist/reconciler.js +11 -36
- package/dist/relay-create.js +3 -3
- package/dist/relay-job-snapshot-reads.js +6 -2
- package/dist/relay-store.js +2 -2
- package/dist/relay-workflow-operations.js +2 -2
- package/dist/relayd-control.js +9 -7
- package/dist/relayd-utilities.js +0 -4
- package/dist/relayd.js +7 -24
- package/dist/runtime-status.js +0 -19
- package/dist/single-job-task-control.js +1 -1
- package/dist/store.js +1 -1
- package/dist/task-actions.js +3 -3
- package/dist/task-recovery.js +2 -2
- package/dist/task-transitions.js +61 -0
- package/dist/troubleshooter-fallback.js +1 -1
- package/dist/troubleshooter.js +1 -4
- package/dist/types.js +1 -2
- package/dist/unblock.js +5 -9
- package/dist/worker-rejection.js +1 -1
- package/dist/workflow/authoritative-conditions.js +1 -1
- package/dist/workflow/blocker-resolution-v2.js +2 -4
- package/dist/workflow/contracts.js +0 -1
- package/dist/workflow/definitions-v2.js +3 -6
- package/dist/workflow/durable-transition.js +1 -1
- package/dist/workflow/force-restate-validation-v2.js +2 -1
- package/dist/workflow/orchestrator-rescue.js +1 -1
- package/dist/workflow/planner-orchestration-v2.js +2 -5
- package/dist/workflow/planner-trigger-v2.js +1 -2
- package/dist/workflow/review-v2.js +0 -1
- package/dist/workflow/task-reconcile.js +1 -7
- package/dist/workflow-effect-failures.js +1 -1
- package/dist/workspace.js +1 -1
- package/package.json +3 -4
- package/skills/agents-relay/SKILL.md +6 -6
- package/skills/agents-relay/agents/planner.agent.md +3 -3
- package/skills/agents-relay/contracts/event-contract.md +10 -0
- package/skills/agents-relay/contracts/orchestrator-workflows.md +2 -2
- package/skills/agents-relay/contracts/task-job-contract.md +5 -5
- package/workflow/blocker-resolution.mmd +1 -7
- package/workflow/force-restate-validation.mmd +7 -2
- package/workflow/planner-orchestration.mmd +2 -5
- package/workflow/planner-trigger.mmd +2 -9
- package/workflow/review-lifecycle.mmd +2 -8
- package/dist/e2e-injection.js +0 -76
- package/dist/scheduled-jobs.js +0 -67
- package/dist/scheduler.js +0 -214
- package/dist/workflow/schedule-cycle-v2.js +0 -25
- package/dist/workflow/scheduled-run-v2.js +0 -30
- package/workflow/schedule-cycle.mmd +0 -23
- package/workflow/scheduled-run.mmd +0 -59
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
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
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;
|
|
5
|
+
V2 uses a read-only local SQLite Job Registry for discovery. The immutable jobId selects the Job Store even when prNumber is zero; lifecycle transitions 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.
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
The canonical V2 architecture contract is
|
|
@@ -23,7 +23,7 @@ flowchart LR
|
|
|
23
23
|
Coordinator --> Graphs[Canonical Mermaid Graph Engine]
|
|
24
24
|
Graphs --> Effects[Stateless actions / effects]
|
|
25
25
|
Effects --> Store
|
|
26
|
-
Store --> Views[Dashboard /
|
|
26
|
+
Store --> Views[Dashboard / Recovery]
|
|
27
27
|
Effects --> Outbox[Durable external-action outbox]
|
|
28
28
|
Outbox --> GH[GitHub Commit / PR / Review / Merge API]
|
|
29
29
|
GH --> Facts[Confirmed external facts]
|
|
@@ -46,8 +46,6 @@ it cannot replace the durable SQLite record.
|
|
|
46
46
|
| Planner orchestration | [`workflow/planner-orchestration.mmd`](workflow/planner-orchestration.mmd) |
|
|
47
47
|
| Blocker resolution | [`workflow/blocker-resolution.mmd`](workflow/blocker-resolution.mmd) |
|
|
48
48
|
| Authoritative unblock | [`workflow/authoritative-unblock.mmd`](workflow/authoritative-unblock.mmd) |
|
|
49
|
-
| Scheduled run | [`workflow/scheduled-run.mmd`](workflow/scheduled-run.mmd) |
|
|
50
|
-
| Schedule cycle | [`workflow/schedule-cycle.mmd`](workflow/schedule-cycle.mmd) |
|
|
51
49
|
| Troubleshooter | [`workflow/troubleshooter.mmd`](workflow/troubleshooter.mmd) |
|
|
52
50
|
| Escalation | [`workflow/escalation.mmd`](workflow/escalation.mmd) |
|
|
53
51
|
| Related-task action policy | [`workflow/task-related-action.mmd`](workflow/task-related-action.mmd) |
|
|
@@ -70,7 +68,7 @@ Standalone Job context. Put a Mermaid block first when a workflow/state/architec
|
|
|
70
68
|
EOF
|
|
71
69
|
|
|
72
70
|
npx agents-relay job create --repo OWNER/REPO \
|
|
73
|
-
--id job-1 --title "Objective" --objective-file job.md
|
|
71
|
+
--id job-1 --title "Objective" --objective-file job.md
|
|
74
72
|
|
|
75
73
|
# The orchestrator decides the initial batch separately
|
|
76
74
|
cat > task.md <<'EOF'
|
|
@@ -102,24 +100,23 @@ npx agents-relay task update --repo OWNER/REPO --pr 12 --id job-1 --task-id chil
|
|
|
102
100
|
npx agents-relay task update --repo OWNER/REPO --pr 12 --id job-1 --task-id child --queue
|
|
103
101
|
~~~
|
|
104
102
|
|
|
105
|
-
Jobs separate durable lifecycle from derived runtime status. Durable lifecycle is `DRAFT | ACTIVE | COMPLETED | CANCELLED`: a newly created
|
|
103
|
+
Jobs separate durable lifecycle from derived runtime status. Durable lifecycle is `DRAFT | ACTIVE | COMPLETED | CANCELLED`: a newly created Job is DRAFT, and creating executable work or starting the Planner activates it. Runtime status (`IDLE`, `WAITING`, `QUEUED`, `RUNNING`, `BLOCKED`, `FAILED`, `REVIEWING`, `MERGE_PENDING`, `MERGED`, or `CANCELLED`) is computed from Tasks, Review, and locally confirmed merge facts and is never persisted as lifecycle truth.
|
|
106
104
|
|
|
107
105
|
Lifecycle transition authority lives in the directly renderable canonical flowchart sources linked in [Workflow graph navigation](#workflow-graph-navigation). Git history owns prior graph versions; the working tree contains only the current graph set.
|
|
108
106
|
|
|
109
|
-
The canonical Mermaid Graph Engine is the authoritative durable workflow decision path. Job/Task lifecycle transitions plus Review, Planner, Blocker/Unblock,
|
|
107
|
+
The canonical Mermaid Graph Engine is the authoritative durable workflow decision path. Job/Task lifecycle transitions plus Review, Planner, Blocker/Unblock, and Troubleshooter orchestration branches are defined by the canonical workflow graphs and executed through the shared graph runtime. TypeScript supplies facts and primitive action/effect implementations; it must not duplicate those workflow branches as a second procedural state machine.
|
|
110
108
|
|
|
111
109
|
Job objectives and Task inputs are Markdown. Prefer `--objective-file` / `--input-file` (use `-` for stdin) for durable long-form context; inline `--objective` / `--input` remain for short/simple text and are mutually exclusive with the file variants. When a diagram materially clarifies the work, put Mermaid first and then Markdown detail. A newly created Job must capture enough context to finish independently of the originating chat. Its PR body contains exactly one hidden `agents-relay:job:v1` metadata marker followed by the objective Markdown; there is no separate Job comment duplicating the objective. Objective and Job metadata updates rewrite that same PR body. Legacy Job-comment markers remain readable and are migrated into the PR body on the next Job save.
|
|
112
110
|
|
|
113
|
-
|
|
111
|
+
Every Job is objective-driven. `job create` persists only the durable Job and never
|
|
114
112
|
creates executable tasks. The caller can either create work directly with
|
|
115
113
|
`task create` or invoke the Planner explicitly with `plan`. Direct
|
|
116
114
|
orchestrator-created tasks are persisted directly and are not wrapped in a
|
|
117
115
|
synthetic Plan. `agents-relay plan` works immediately on a zero-task DRAFT Job,
|
|
118
116
|
activates it, persists a real `plan-N` planning round, and materializes validated
|
|
119
|
-
`next_tasks`.
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
another planning round automatically.
|
|
117
|
+
`next_tasks`. Reconciliation does not invent the first round for an untouched
|
|
118
|
+
zero-task DRAFT Job; once work starts, every Job automatically creates later
|
|
119
|
+
Planner rounds until its latest Plan is satisfied.
|
|
123
120
|
|
|
124
121
|
Manual planning syntax:
|
|
125
122
|
|
|
@@ -130,8 +127,7 @@ npx agents-relay plan --repo OWNER/REPO --pr 12 --id job-1 --prompt-file guidanc
|
|
|
130
127
|
|
|
131
128
|
The optional prompt is additional round guidance; it does not replace the durable Job objective. New planning rounds use `plan-N`; historical `planner-N` records remain readable.
|
|
132
129
|
|
|
133
|
-
The durable status JSON and dashboard show
|
|
134
|
-
autonomous runtime uses the bundled `skills/agents-relay/agents/planner.agent.md` by default; `--planner-command` is an optional override
|
|
130
|
+
The durable status JSON and dashboard show planning provenance. The runtime uses the bundled `skills/agents-relay/agents/planner.agent.md` by default; `--planner-command` is an optional override
|
|
135
131
|
snapshot as JSON on stdin and must return `{ "objective_status": "in_progress"|"satisfied", "assessment": "...", "next_tasks": [...] }`.
|
|
136
132
|
Planner-created tasks use stable IDs and the existing parent/subtask tree, so
|
|
137
133
|
retries and daemon restarts do not create duplicate work.
|
|
@@ -140,9 +136,7 @@ Task producers must follow `skills/agents-relay/contracts/task-job-contract.md`;
|
|
|
140
136
|
Execution routing is code-driven: Relay builds complete executable worker+model routes from authoritative runtime/model/resource facts, merges profile defaults with the user's explicit `[choices]`, deduplicates them, removes only impossible routes via hard gates, then calls the generic typed `decide(request, contract)` boundary exactly once with one merged choice list and the `[routing-instruction]` text forwarded verbatim. Task/resource/capability signals and the routing instruction are soft decision facts; decision failure falls back deterministically to a usable Codex default route. See [Execution Router and typed decisions](docs/worker-model-routers.md).
|
|
141
137
|
All task producers use the same story-size contract: `S`≈5 minutes, `M`≈15 minutes, and `L`≈45 minutes of inactivity, with `M` as the default. `high` reasoning doubles that inactivity window. Accepted `task.started` and newer `task.progress` events from the exact current execution reset it. ChatGPT Browser Worker start-ack timeout is derived as one third of the effective inactivity timeout, clamped between 1 and 5 minutes. Orchestrators and planners should choose `storySize`, not raw timeout milliseconds; `--timeout` remains only as an explicit compatibility/debug override.
|
|
142
138
|
|
|
143
|
-
|
|
144
|
-
[Scheduled autonomous jobs](docs/scheduled-autonomous-jobs.md) for the exact
|
|
145
|
-
operational contract.
|
|
139
|
+
There is no Job scheduling or cron support. Reconciliation resumes objective planning from durable state, including after a missed wake-up or restart. See [objective-driven Jobs](docs/objective-driven-jobs.md).
|
|
146
140
|
|
|
147
141
|
GitHub projection creates or reuses a display PR after a meaningful source-branch commit; `job adopt` is reserved for existing legacy PR bindings. Re-running it is idempotent: it reuses the PR and maintains exactly one trusted agents-relay:job:v1 marker. Job creation is always create-only and always enables auto-merge; `--no-auto-merge` is not a `job create` option. Existing Jobs can still change that setting with `job update`. Task execution options belong to `task create`, and the orchestrator may create zero, one, or multiple initial Tasks according to user intent.
|
|
148
142
|
|
|
@@ -294,14 +288,14 @@ flowchart LR
|
|
|
294
288
|
Unblock --> Repair
|
|
295
289
|
```
|
|
296
290
|
|
|
297
|
-
The escalation graph is the authority for what happens after Neo returns: `resolved` selects resume recovery, `failed` selects technical-failure handling, and only `needs_user` selects the block-for-user action. Notification mechanics stay caller-described through `--on-needs-user`; Relay does not bind WeChat, Discord, iMessage, or another channel. Only after the failure cause is repaired does Troubleshooter retry the original FAILED task and verify durable progress. Historical failures
|
|
291
|
+
The escalation graph is the authority for what happens after Neo returns: `resolved` selects resume recovery, `failed` selects technical-failure handling, and only `needs_user` selects the block-for-user action. Notification mechanics stay caller-described through `--on-needs-user`; Relay does not bind WeChat, Discord, iMessage, or another channel. Only after the failure cause is repaired does Troubleshooter retry the original FAILED task and verify durable progress. Historical resolved failures do not spawn duplicate recovery work, and deterministic reconciliation does not duplicate a Troubleshooter task for the same failed attempt.
|
|
298
292
|
|
|
299
293
|
|
|
300
294
|
### Authoritative unblock and Planner history
|
|
301
295
|
|
|
302
296
|
Dependency release and runtime unblock are separate concepts. Dependency completion simply releases DAG work. A runtime/external blocker remains `BLOCKED` until `task unblock`, `job unblock`, or blocker-Job resolution records the blocked baseline/current evidence and emits `task.unblocked`, transitioning the Task to `UNBLOCKED`.
|
|
303
297
|
|
|
304
|
-
For
|
|
298
|
+
For every Job, unblock evidence becomes durable recovery context. Relay retries the same authorized Troubleshooter, while ordinary work uses Planner-owned recovery when necessary. Every Planner round is a first-class `agents-relay:plan:v1` record. It is persisted before generated Tasks, then updated with the Planner assessment and generated Task IDs. Legacy Job `changeCycle` data is accepted only as compatibility input and is not written by the current model.
|
|
305
299
|
|
|
306
300
|
|
|
307
301
|
### Browser Workspace product boundary
|
package/dist/adapters.js
CHANGED
|
@@ -3,8 +3,6 @@ import { join, relative, resolve, sep, basename } from 'node:path';
|
|
|
3
3
|
import { DEFAULT_PROMPT_REFERENCE_DIRECTORY } from './agent-prompt-builder.js';
|
|
4
4
|
import { pathToFileURL } from 'node:url';
|
|
5
5
|
import { createHash, randomUUID } from 'node:crypto';
|
|
6
|
-
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
7
|
-
import { withTestInjection } from './e2e-injection.js';
|
|
8
6
|
import { taskReferencePrompt } from './task-context.js';
|
|
9
7
|
import { spawnOwnedProcess, terminateOwnedProcess, ownedProcessTerminationReason } from './owned-process.js';
|
|
10
8
|
import { PreSubmissionRejection } from './worker-rejection.js';
|
|
@@ -62,8 +60,6 @@ export class CodexAdapter {
|
|
|
62
60
|
this.command = command;
|
|
63
61
|
}
|
|
64
62
|
launch(task, signal, context = {}) {
|
|
65
|
-
if (context.testInjection?.method === 'worker.codex.launch')
|
|
66
|
-
return withTestInjection('worker.codex.launch', context.testInjection, () => this.launch(task, signal, { ...context, testInjection: undefined }));
|
|
67
63
|
if (!task.routing)
|
|
68
64
|
throw new Error(`Task ${task.id} requires routing metadata before Codex launch`);
|
|
69
65
|
const child = spawnOwnedProcess(this.command, buildCodexArgs(task.routing, task.input), {
|
|
@@ -174,9 +170,8 @@ export function parseDriverJson(output) {
|
|
|
174
170
|
function browserWorkspaceCli(override = process.env.AGENTS_RELAY_WEBHELM_COMMAND) {
|
|
175
171
|
return override ? { command: override, prefix: [] } : { command: 'uvx', prefix: ['--from', 'browser-workspace', 'webhelm'] };
|
|
176
172
|
}
|
|
177
|
-
const browserWorkspaceInjection = new AsyncLocalStorage();
|
|
178
173
|
function browserWorkspaceCommand(cli, args, stdin, signal) {
|
|
179
|
-
return
|
|
174
|
+
return rawBrowserWorkspaceCommand(cli, args, stdin, signal);
|
|
180
175
|
}
|
|
181
176
|
function rawBrowserWorkspaceCommand(cli, args, stdin, signal) {
|
|
182
177
|
return new Promise((resolve, reject) => {
|
|
@@ -409,8 +404,7 @@ export class ChatGptAdapter {
|
|
|
409
404
|
tabClosePolicy: policy,
|
|
410
405
|
}, route, controller.signal);
|
|
411
406
|
};
|
|
412
|
-
const run = await (
|
|
413
|
-
? browserWorkspaceInjection.run(context.testInjection, submit) : submit());
|
|
407
|
+
const run = await submit();
|
|
414
408
|
try {
|
|
415
409
|
// Start acknowledgement may expire while browser submission is still pending.
|
|
416
410
|
// Race its rejection with delivery so a hung browser cannot hold a QUEUED
|
package/dist/blockers.js
CHANGED
|
@@ -103,7 +103,6 @@ export async function resolveBlockerDependencies(options) {
|
|
|
103
103
|
const decision = await dispatchBlockerResolution({
|
|
104
104
|
resolutionAllowed: !(mode === 'verified' && options.resolution !== 'verified'),
|
|
105
105
|
targetMatches,
|
|
106
|
-
autonomousJob: targetJob?.executionMode === 'autonomous',
|
|
107
106
|
effectivelyBlocked: !!task && isEffectivelyBlocked(task),
|
|
108
107
|
alreadyUnblocked: Boolean(task?.unblock && ['DRAFT', 'QUEUED'].includes(task.state)),
|
|
109
108
|
isDraft: task?.state === 'DRAFT', isBlocked: Boolean(task && isEffectivelyBlocked(task)),
|
|
@@ -139,7 +138,9 @@ export async function resolveBlockerDependencies(options) {
|
|
|
139
138
|
relation.resolution = options.resolution;
|
|
140
139
|
relation.note = note;
|
|
141
140
|
unblocked.push({ repository: relation.repository, prNumber: relation.prNumber, jobId: relation.jobId, taskId: relation.taskId });
|
|
142
|
-
|
|
141
|
+
// A durable unblock may have committed before this relation was acknowledged.
|
|
142
|
+
if (!(task?.unblock && ['DRAFT', 'QUEUED'].includes(task.state)))
|
|
143
|
+
resumed += 1;
|
|
143
144
|
}
|
|
144
145
|
const remaining = relations.filter(relation => relation.state === 'waiting');
|
|
145
146
|
const consumed = remaining.length !== relations.length;
|
package/dist/cli-help.js
CHANGED
|
@@ -62,7 +62,7 @@ Optional: --thread-id THREAD_ID persists a reviewer override; omitted uses Job r
|
|
|
62
62
|
'job create': `Create a SQLite-authoritative Job (objective-only).
|
|
63
63
|
|
|
64
64
|
Required: --repo OWNER/REPO and a standalone Job objective. Job ID defaults to an auto-incrementing store-global number; --id may only reference an existing numeric Job. Existing PR adoption is an explicit separate command: job adopt.
|
|
65
|
-
Common: --id JOB_ID (optional), --title TEXT, --objective-file PATH|- (preferred), --objective MARKDOWN (short/simple only), --project-path RELATIVE_PATH, --priority P0|P1|P2|P3, --
|
|
65
|
+
Common: --id JOB_ID (optional), --title TEXT, --objective-file PATH|- (preferred), --objective MARKDOWN (short/simple only), --project-path RELATIVE_PATH, --priority P0|P1|P2|P3, --initial-plan, --no-review, --tag JSON, --reviewer-thread-id ID, --creator-thread-id ID, --implementation-thread-id ID. --objective and --objective-file are mutually exclusive.
|
|
66
66
|
Job creation always enables auto-merge. By default it is create-only; --initial-plan validates the first real Planner round before atomically creating the durable Job and Plan, then 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.
|
|
67
67
|
|
|
68
68
|
Example:
|
|
@@ -73,7 +73,7 @@ Required: --repo OWNER/REPO and --pr NUMBER. The Job ID is allocated atomically
|
|
|
73
73
|
'job update': `Partially update mutable metadata on a healthy existing Job.
|
|
74
74
|
|
|
75
75
|
Required: --repo OWNER/REPO, --pr NUMBER, and --id JOB_ID.
|
|
76
|
-
Mutable: --title, --objective-file PATH|- (preferred) or --objective MARKDOWN, --project-path RELATIVE_PATH, --priority, --
|
|
76
|
+
Mutable: --title, --objective-file PATH|- (preferred) or --objective MARKDOWN, --project-path RELATIVE_PATH, --priority, --tag JSON, --auto-merge|--no-auto-merge, --review|--no-review, --reviewer-thread-id ID, --creator-thread-id ID, --implementation-thread-id ID, and continuation options. The objective updates local SQLite; PR body changes are independent external actions.
|
|
77
77
|
Unspecified fields are preserved. Local Job/PR identity mismatch is rejected.
|
|
78
78
|
|
|
79
79
|
Example:
|
|
@@ -226,7 +226,7 @@ Example:
|
|
|
226
226
|
npx agents-relay reconcile --repo OWNER/REPO --pr 12 --id 101`,
|
|
227
227
|
init: `Initialize a new job. GitHub mode requires --repo and --pr; local mode requires explicit --file.
|
|
228
228
|
|
|
229
|
-
Common: --id ID, --title TEXT, --objective TEXT, --
|
|
229
|
+
Common: --id ID, --title TEXT, --objective TEXT, --priority P0|P1|P2|P3. All Jobs replan to an explicit satisfied objective; execution modes and schedules are unsupported.
|
|
230
230
|
|
|
231
231
|
Example:
|
|
232
232
|
npx agents-relay init --repo OWNER/REPO --pr 12 --id 101 --title "Objective"`,
|
package/dist/cli-job-create.js
CHANGED
|
@@ -56,7 +56,6 @@ export async function ensureManagedGitHubJob(_client, repositoryName, _trustedAu
|
|
|
56
56
|
projectPath: options.projectPath,
|
|
57
57
|
description: pr.body || undefined,
|
|
58
58
|
priority: options.priority ?? 'P2',
|
|
59
|
-
executionMode: options.executionMode ?? 'fixed',
|
|
60
59
|
tag: options.tag,
|
|
61
60
|
autoMerge: true,
|
|
62
61
|
...(localMergeSource ? { localMerge: { sourceRef: localMergeSource, targetRef: 'refs/heads/main', status: 'pending' } } : {}),
|
|
@@ -76,8 +75,7 @@ export async function ensureManagedGitHubJob(_client, repositoryName, _trustedAu
|
|
|
76
75
|
createdAt: now,
|
|
77
76
|
updatedAt: now,
|
|
78
77
|
tasks: [],
|
|
79
|
-
|
|
80
|
-
}, { activate: Boolean(options.schedule) });
|
|
78
|
+
});
|
|
81
79
|
if (options.persist !== false) {
|
|
82
80
|
if (local.store.createJobIfAbsent)
|
|
83
81
|
await local.store.createJobIfAbsent(job);
|
|
@@ -117,7 +115,7 @@ export async function adoptExistingPullRequestJob(_client, repositoryName, _trus
|
|
|
117
115
|
return { job: existing, pr: { number: existing.prNumber, title: existing.title, body: existing.objective ?? '', state: 'OPEN', head: '', base: 'main' } };
|
|
118
116
|
const now = new Date().toISOString();
|
|
119
117
|
const job = createJobEntity({ id, title: options.title ?? id, objective: options.objective, projectPath: options.projectPath,
|
|
120
|
-
description: options.objective, priority: 'P2',
|
|
118
|
+
description: options.objective, priority: 'P2', autoMerge: true, reviewEnabled: true,
|
|
121
119
|
prNumber: options.prNumber, repository: repositoryName, continuation: null, createdAt: now, updatedAt: now, tasks: [] });
|
|
122
120
|
const created = local.store.createJobIfAbsent ? await local.store.createJobIfAbsent(job) : (await local.store.saveJob(job), job);
|
|
123
121
|
return { job: created, pr: { number: options.prNumber, title: created.title, body: created.objective ?? '', state: 'OPEN', head: '', base: 'main' } };
|
package/dist/cli-support.js
CHANGED
|
@@ -6,7 +6,6 @@ import { basename } from 'node:path';
|
|
|
6
6
|
import { createJobIdRegistryResolver } from './local-job-registry.js';
|
|
7
7
|
import { relayStoreDirectory } from './relay-store.js';
|
|
8
8
|
import { NatsEventBus, eventFor } from './events.js';
|
|
9
|
-
import { parseJobSchedule } from './scheduler.js';
|
|
10
9
|
import { parseJobTagJson } from './task-tag.js';
|
|
11
10
|
import { resolveEventsBusRuntime } from './events-bus-runtime.js';
|
|
12
11
|
export const SERVICE_DEFAULTS = { port: 8787, watchdogMs: 300000, webhookWatchdogMs: 300000, dashboardRefreshMs: 300000, projectionFlushMs: 1000 };
|
|
@@ -277,27 +276,17 @@ export function priority(args, fallback = 'P2') { const value = arg(args, '--pri
|
|
|
277
276
|
throw new Error('--priority must be P0, P1, P2, or P3'); return value; }
|
|
278
277
|
export function storySize(args, fallback = 'M') { const value = arg(args, '--story-size', fallback).toUpperCase(); if (!['S', 'M', 'L'].includes(value))
|
|
279
278
|
throw new Error('--story-size must be S, M, or L'); return value; }
|
|
280
|
-
export function executionMode(args, fallback = 'fixed') { const value = arg(args, '--mode', arg(args, '--execution-mode', fallback)); if (value !== 'fixed' && value !== 'autonomous')
|
|
281
|
-
throw new Error('--mode must be fixed or autonomous'); return value; }
|
|
282
279
|
export function chatGptTabClosePolicy(args) {
|
|
283
280
|
const value = arg(args, '--chatgpt-tab-close-policy', process.env.AGENTS_RELAY_CHATGPT_TAB_CLOSE_POLICY || 'after-start');
|
|
284
281
|
if (!['after-start', 'never', 'after-terminal'].includes(value))
|
|
285
282
|
throw new Error('--chatgpt-tab-close-policy must be after-start, never, or after-terminal');
|
|
286
283
|
return value;
|
|
287
284
|
}
|
|
288
|
-
export function requestedExecutionMode(args) { const value = arg(args, '--mode', arg(args, '--execution-mode')); return value ? executionMode(args) : undefined; }
|
|
289
285
|
export function jobTagFromArgs(args) { return parseJobTagJson(arg(args, '--tag')); }
|
|
290
286
|
export function continuation(args) { const kind = arg(args, '--continue-kind'); const target = arg(args, '--continue-target'); return kind && target ? { kind: kind, target, input: arg(args, '--continue-input') || undefined } : null; }
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
return parseJobSchedule({
|
|
297
|
-
cron,
|
|
298
|
-
timezone: arg(args, '--schedule-timezone', 'UTC'),
|
|
299
|
-
nextRunAt,
|
|
300
|
-
missedRunPolicy: arg(args, '--missed-run-policy', 'skip'),
|
|
301
|
-
concurrencyPolicy: arg(args, '--schedule-concurrency', 'skip'),
|
|
302
|
-
});
|
|
287
|
+
/** Removed Job options fail closed rather than silently accepting obsolete scheduling. */
|
|
288
|
+
export function rejectRemovedJobOptions(args) {
|
|
289
|
+
const obsolete = ['--mode', '--execution-mode', '--schedule', '--schedule-next-run', '--schedule-timezone', '--missed-run-policy', '--schedule-concurrency', '--cron', '--schedule-cron'].find(option => hasArg(args, option));
|
|
290
|
+
if (obsolete)
|
|
291
|
+
throw new Error(`Unsupported Job option ${obsolete}: every Job replans to objective satisfaction and scheduling is unavailable`);
|
|
303
292
|
}
|
package/dist/cli.js
CHANGED
|
@@ -41,8 +41,8 @@ export { helpText } from './cli-help.js';
|
|
|
41
41
|
import { runReviewCommand } from './cli-review.js';
|
|
42
42
|
import { normalizeThreadId, runtimeReviewer } from './reviewer-thread.js';
|
|
43
43
|
export { runtimeReviewer } from './reviewer-thread.js';
|
|
44
|
-
import { SERVICE_DEFAULTS, ServiceEventBus, arg, hasArg, textOption, integerArg, repository, storeFor, saveLocal, eventBus, publishWake, priority, storySize,
|
|
45
|
-
export { SERVICE_DEFAULTS, eventBus,
|
|
44
|
+
import { SERVICE_DEFAULTS, ServiceEventBus, arg, hasArg, textOption, integerArg, repository, storeFor, saveLocal, eventBus, publishWake, priority, storySize, chatGptTabClosePolicy, continuation, rejectRemovedJobOptions, jobTagFromArgs, requestManagedReconciliation, relayControlJob, relayControlCommand, relayJobList, relayJobStateCommand, relayTaskCommand, withEventBus } from './cli-support.js';
|
|
45
|
+
export { SERVICE_DEFAULTS, eventBus, chatGptTabClosePolicy } from './cli-support.js';
|
|
46
46
|
import { recoveryDiagnosisFromArgs, workerTaskFromArgs } from './cli-worker-task.js';
|
|
47
47
|
import { taskExecutionContext } from './task-context.js';
|
|
48
48
|
import { requestTroubleshooterEscalation, resumeTroubleshooter } from './task-recovery.js';
|
|
@@ -470,12 +470,13 @@ async function runJobCommand(action, args) {
|
|
|
470
470
|
if (prValue && (!Number.isInteger(prNumber) || (prNumber ?? 0) <= 0))
|
|
471
471
|
throw new Error('--pr must be a positive integer');
|
|
472
472
|
if (action === 'create') {
|
|
473
|
+
rejectRemovedJobOptions(args);
|
|
473
474
|
const objective = createObjective ?? '';
|
|
474
475
|
if (hasArg(args, '--local-merge-source') !== hasArg(args, '--local-merge-target'))
|
|
475
476
|
throw new Error('Supply both --local-merge-source and --local-merge-target');
|
|
476
477
|
const result = await relayControlCommand(args, { command: 'job.create', repository: repo, initialPlan: hasArg(args, '--initial-plan'), options: {
|
|
477
478
|
id: id || undefined, idempotencyKey: arg(args, '--idempotency-key') || undefined, title: arg(args, '--title') || undefined, objective: objective || undefined, projectPath: arg(args, '--project-path') || undefined,
|
|
478
|
-
priority: priority(args),
|
|
479
|
+
priority: priority(args), tag: jobTagFromArgs(args), reviewEnabled: !hasArg(args, '--no-review'), continuation: continuation(args),
|
|
479
480
|
reviewerThreadId: hasArg(args, '--reviewer-thread-id') ? arg(args, '--reviewer-thread-id') : undefined, creatorThreadId: hasArg(args, '--creator-thread-id') ? arg(args, '--creator-thread-id') : undefined, implementationThreadId: hasArg(args, '--implementation-thread-id') ? arg(args, '--implementation-thread-id') : undefined,
|
|
480
481
|
} });
|
|
481
482
|
console.log(JSON.stringify(result, null, 2));
|
|
@@ -520,10 +521,7 @@ async function runJobCommand(action, args) {
|
|
|
520
521
|
configureLocalGitMerge(job, arg(args, '--local-merge-source'), arg(args, '--local-merge-target'), noGitHubMode(args));
|
|
521
522
|
if (hasArg(args, '--priority'))
|
|
522
523
|
job.priority = priority(args, job.priority ?? 'P2');
|
|
523
|
-
|
|
524
|
-
job.executionMode = executionMode(args, job.executionMode ?? 'fixed');
|
|
525
|
-
if (hasArg(args, '--schedule'))
|
|
526
|
-
job.schedule = scheduleFromArgs(args);
|
|
524
|
+
rejectRemovedJobOptions(args);
|
|
527
525
|
if (hasArg(args, '--tag'))
|
|
528
526
|
job.tag = jobTagFromArgs(args);
|
|
529
527
|
if (hasArg(args, '--auto-merge') && hasArg(args, '--no-auto-merge'))
|
|
@@ -551,7 +549,7 @@ async function runJobCommand(action, args) {
|
|
|
551
549
|
job.continuation = next;
|
|
552
550
|
}
|
|
553
551
|
const patch = {};
|
|
554
|
-
for (const key of ['title', 'objective', 'projectPath', 'priority', '
|
|
552
|
+
for (const key of ['title', 'objective', 'projectPath', 'priority', 'tag', 'autoMerge', 'reviewEnabled', 'reviewerThreadId', 'creatorThreadId', 'implementationThreadId', 'continuation', 'localMerge'])
|
|
555
553
|
patch[key] = job[key];
|
|
556
554
|
console.log(JSON.stringify(await relayControlCommand(args, { command: 'job.update', jobId: id, patch }), null, 2));
|
|
557
555
|
}
|
|
@@ -771,16 +769,16 @@ async function main() {
|
|
|
771
769
|
}
|
|
772
770
|
if (command === 'init') {
|
|
773
771
|
const file = arg(args, '--file');
|
|
772
|
+
rejectRemovedJobOptions(args);
|
|
774
773
|
const now = new Date().toISOString();
|
|
775
|
-
const initialSchedule = scheduleFromArgs(args);
|
|
776
774
|
const id = arg(args, '--id', randomUUID());
|
|
777
775
|
if (!file) {
|
|
778
776
|
const repo = repository(args);
|
|
779
|
-
const result = await relayControlCommand(args, { command: 'job.create', repository: repo, options: { id, title: arg(args, '--title', 'Agents Relay job'), objective: arg(args, '--objective') || undefined, priority: priority(args),
|
|
777
|
+
const result = await relayControlCommand(args, { command: 'job.create', repository: repo, options: { id, title: arg(args, '--title', 'Agents Relay job'), objective: arg(args, '--objective') || undefined, priority: priority(args), reviewEnabled: !hasArg(args, '--no-review'), continuation: continuation(args), prNumber: Number(arg(args, '--pr', '0')) || 0 } });
|
|
780
778
|
console.log(JSON.stringify(result, null, 2));
|
|
781
779
|
return;
|
|
782
780
|
}
|
|
783
|
-
const job = createJobEntity({ id, title: arg(args, '--title', 'Agents Relay job'), objective: arg(args, '--objective') || undefined, priority: priority(args),
|
|
781
|
+
const job = createJobEntity({ id, title: arg(args, '--title', 'Agents Relay job'), objective: arg(args, '--objective') || undefined, priority: priority(args), autoMerge: !hasArg(args, '--no-auto-merge'), reviewEnabled: !hasArg(args, '--no-review'), prNumber: Number(arg(args, '--pr', '0')), repository: arg(args, '--repo'), continuation: continuation(args), createdAt: now, updatedAt: now, tasks: [] });
|
|
784
782
|
const store = new InMemoryStore(job);
|
|
785
783
|
await store.saveJob(job);
|
|
786
784
|
await saveLocal(file, job);
|
package/dist/dashboard-html.js
CHANGED
|
@@ -41,7 +41,7 @@ ${dashboardStyles}
|
|
|
41
41
|
</header>
|
|
42
42
|
<main>
|
|
43
43
|
<section id="jobs-view" role="tabpanel" aria-labelledby="jobs-tab" class="panel jobs-panel">
|
|
44
|
-
<div class="panel-head"><h2>Jobs</h2><div class="panel-actions"><div class="job-filters"><input id="job-search" class="job-search" type="search" placeholder="Search title or PR #" aria-label="Search jobs by title or PR number"><select id="quick-status" class="job-filter" aria-label="Filter jobs by derived status"><option value="">STATUS</option></select><button id="quick-
|
|
44
|
+
<div class="panel-head"><h2>Jobs</h2><div class="panel-actions"><div class="job-filters"><input id="job-search" class="job-search" type="search" placeholder="Search title or PR #" aria-label="Search jobs by title or PR number"><select id="quick-status" class="job-filter" aria-label="Filter jobs by derived status"><option value="">STATUS</option></select><button id="quick-draft" class="quick-filter" type="button" aria-pressed="false">DRAFT</button><select id="repo-filter" class="job-filter" aria-label="Filter jobs by repository"><option value="">All repos</option></select></div><button id="jobs-refresh" class="refresh-button" type="button">Refresh</button></div></div>
|
|
45
45
|
<div class="panel-body" aria-live="polite"><ul id="job-list" class="job-list"><li class="skeleton" aria-hidden="true"></li></ul></div>
|
|
46
46
|
</section>
|
|
47
47
|
<div class="workspace" id="jobs-workspace">
|
|
@@ -144,7 +144,6 @@ const jobsRefresh=document.querySelector('#jobs-refresh');
|
|
|
144
144
|
const jobSearch=document.querySelector('#job-search');
|
|
145
145
|
const repoFilter=document.querySelector('#repo-filter');
|
|
146
146
|
const quickStatus=document.querySelector('#quick-status');
|
|
147
|
-
const quickAutonomous=document.querySelector('#quick-autonomous');
|
|
148
147
|
const quickDraft=document.querySelector('#quick-draft');
|
|
149
148
|
const selectedJobRefresh=document.querySelector('#selected-job-refresh');
|
|
150
149
|
const mobileJobsBack=document.querySelector('#mobile-jobs-back');
|
|
@@ -203,7 +202,6 @@ function selected(){return jobForSelection(selectedJob)||jobs[0]||null;}
|
|
|
203
202
|
function counts(tasks){const out={};for(const task of Array.isArray(tasks)?tasks:[])out[task.state]=(out[task.state]||0)+1;return out;}
|
|
204
203
|
function compactCounts(input){const order=['RUNNING','BLOCKED','FAILED','QUEUED','DRAFT','SUCCEEDED','CANCELLED'];return order.filter(state=>input[state]).map(state=>input[state]+' '+state).join(', ')||'No tasks';}
|
|
205
204
|
function relativeTime(timestamp){if(!timestamp)return '—';const parsed=Date.parse(timestamp);if(!Number.isFinite(parsed))return value(timestamp);const seconds=Math.max(0,Math.round((Date.now()-parsed)/1000));if(seconds<60)return 'just now';if(seconds<3600)return Math.floor(seconds/60)+'m ago';if(seconds<86400)return Math.floor(seconds/3600)+'h ago';return Math.floor(seconds/86400)+'d ago';}
|
|
206
|
-
function cadence(job){const schedule=job?.schedule;if(!schedule?.cron)return '—';const cron=String(schedule.cron).trim();const timezone=value(schedule.timezone||'UTC');const fields=cron.split(/\\s+/);let label=cron;if(fields.length===5){const [minute,hour,day,month,weekday]=fields;if(day==='*'&&month==='*'&&weekday==='*'){const everyMinute=minute.match(/^\\*\\/(\\d+)$/);if(everyMinute&&hour==='*')label='Every '+everyMinute[1]+' min';else if(minute==='0'&&hour==='*')label='Hourly';else if(/^\\d+$/.test(minute)&&/^\\d+$/.test(hour)){const hh=String(Number(hour)).padStart(2,'0');const mm=String(Number(minute)).padStart(2,'0');label='Daily '+hh+':'+mm;}}}return label+' · '+cron+' · '+timezone;}
|
|
207
205
|
function safePrUrl(repository,prNumber){if(typeof repository!=='string'||!Number.isInteger(prNumber)||prNumber<=0)return null;const parts=repository.split('/');if(parts.length!==2)return null;const [owner,repo]=parts;if(!owner||!repo||owner==='.'||owner==='..'||repo==='.'||repo==='..')return null;if(!/^[A-Za-z0-9-]+$/.test(owner)||!/^[A-Za-z0-9._-]+$/.test(repo))return null;return 'https://github.com/'+owner+'/'+repo+'/pull/'+prNumber;}
|
|
208
206
|
function safeCommitUrl(repository,sha){if(typeof repository!=='string'||typeof sha!=='string'||!/^[0-9a-f]{7,64}$/i.test(sha))return null;const parts=repository.split('/');if(parts.length!==2||!parts[0]||!parts[1])return null;return 'https://github.com/'+parts[0]+'/'+parts[1]+'/commit/'+sha;}
|
|
209
207
|
function commitField(label,job,sha){const box=element('div','detail-group');box.append(element('p','detail-label',text(label)));const line=element('p','detail-value');const url=safeCommitUrl(job?.repository,sha);if(url){const link=document.createElement('a');link.href=url;link.target='_blank';link.rel='noopener noreferrer';link.textContent=sha.slice(0,7);line.append(link);}else line.append(text(sha||'—'));box.append(line);return box;}
|
|
@@ -216,7 +214,7 @@ function clearEvents(){displayedEventIds.clear();eventFeed.replaceChildren(eleme
|
|
|
216
214
|
async function loadEventHistory(){const job=selected();if(!job){clearEvents();return;}const params=new URLSearchParams({job:job.id,limit:String(MAX_EVENTS)});if(eventTaskId)params.set('task',eventTaskId);try{const history=await loadJSON('/api/events?'+params.toString());clearEvents();for(const event of Array.isArray(history)?history.slice().reverse():[])addEvent(event);}catch{clearEvents();}}
|
|
217
215
|
function copySelectedJobIdentity(job){if(!job||!navigator.clipboard?.writeText)return;void navigator.clipboard.writeText(repoName(job.repository)+'/'+value(job.id)).catch(()=>{});}
|
|
218
216
|
function selectJob(job){selectedJob=jobSelectionKey(job);selectedTaskId=null;eventTaskId=null;rememberSelection();clearEvents();location.hash=selectedJob?'#/jobs/'+encodeURIComponent(selectedJob):'#/jobs';copySelectedJobIdentity(job);renderAll();void loadEventHistory();}
|
|
219
|
-
function matchesQuickFilter(job){if(quickMode==='
|
|
217
|
+
function matchesQuickFilter(job){if(quickMode==='draft')return job.draft||job.state==='DRAFT';if(quickMode==='status')return !!selectedRuntimeStatus&&(job.runtimeStatus||'IDLE')===selectedRuntimeStatus;return true;}
|
|
220
218
|
function matchesSearch(job){const query=searchQuery.trim().toLowerCase();if(!query)return true;const pr=String(job.prNumber||'');const normalized=query.replace(/^pr\s*#?/,'').replace(/^#/,'');return String(job.title||job.id||'').toLowerCase().includes(query)||String(job.id||'').toLowerCase().includes(query)||shortManagedId(job.id,job.repository,'j').toLowerCase().includes(query)||pr.includes(normalized);}
|
|
221
219
|
function filteredJobs(){return jobs.filter(job=>(!selectedRepo||job.repository===selectedRepo)&&matchesQuickFilter(job)&&matchesSearch(job));}
|
|
222
220
|
function setQuickMode(mode,status=''){quickMode=mode;selectedRuntimeStatus=mode==='status'?status:'';sessionStorage.setItem('agents-relay:quick-filter',quickMode);if(selectedRuntimeStatus)sessionStorage.setItem('agents-relay:runtime-status-filter',selectedRuntimeStatus);else sessionStorage.removeItem('agents-relay:runtime-status-filter');syncFilters();renderJobList();}
|
|
@@ -233,7 +231,6 @@ function syncFilters(){
|
|
|
233
231
|
selectedRuntimeStatus=statuses.includes(previousStatus)?previousStatus:'';
|
|
234
232
|
if(quickMode==='status'&&!selectedRuntimeStatus)quickMode='jobs';
|
|
235
233
|
quickStatus.value=quickMode==='status'?selectedRuntimeStatus:'';
|
|
236
|
-
quickAutonomous.setAttribute('aria-pressed',String(quickMode==='autonomous'));
|
|
237
234
|
quickDraft.setAttribute('aria-pressed',String(quickMode==='draft'));
|
|
238
235
|
jobSearch.value=searchQuery;
|
|
239
236
|
}
|
|
@@ -250,7 +247,7 @@ function jobChatRef(job){const pr=Number(job?.prNumber||0);return repoName(job?.
|
|
|
250
247
|
function chatShortcutUrl(prompt){return 'https://chatgpt.com/?prompt='+encodeURIComponent(prompt);}
|
|
251
248
|
const CHAT_SHORTCUT_TARGET='agents-relay-shortcut';
|
|
252
249
|
function chatShortcutField(job){const box=element('div','detail-group');box.append(element('p','detail-label',text('ChatGPT shortcut')));const select=document.createElement('select');select.className='job-filter job-chat-shortcut-select';select.setAttribute('aria-label','ChatGPT shortcut');select.append(new Option('',''));for(const item of parseChatShortcuts(chatShortcutText()))select.append(new Option(item.name,item.prompt));select.value='';select.addEventListener('change',()=>{if(!select.value)return;const prompt=select.value.replaceAll('%1',jobChatRef(job));window.open(chatShortcutUrl(prompt),CHAT_SHORTCUT_TARGET,'noopener');select.value='';});box.append(select);return box;}
|
|
253
|
-
function renderJobList(){jobList.replaceChildren();const visible=filteredJobs();if(!jobs.length){jobList.append(element('li','empty',text('No durable jobs discovered.')));return;}if(!visible.length){jobList.append(element('li','empty',text('No jobs match the selected repo.')));return;}for(const job of visible){const row=element('div','job-row'+(isCompleteUnmerged(job)?' complete-unmerged':''));row.setAttribute('aria-current',String(jobSelectionKey(job)===selectedJob));row.addEventListener('click',()=>selectJob(job));const content=element('div','job-content');const select=document.createElement('button');select.type='button';select.className='job-button';select.setAttribute('aria-current',String(jobSelectionKey(job)===selectedJob));select.title=job.title||job.id;select.addEventListener('click',event=>{event.stopPropagation();selectJob(job);});select.append(element('span','job-title',text(repoName(job.repository)+'/'+value(job.id)+(Number(job.prNumber||0)>0?'/PR#'+value(job.prNumber):'')+': '+value(job.title||job.id))));const states=element('span','job-status-line');states.append(element('span','badge '+value(job.state),text(value(job.state))),element('span','badge '+value(job.runtimeStatus||'IDLE'),text(value(job.runtimeStatus||'IDLE'))));if(job.publicationFailure)states.append(element('span','badge FAILED',text('PUBLISH FAILED: '+value(job.publicationFailure.stage))));else if(job.publication?.version)states.append(element('span','badge SUCCEEDED',text('v'+job.publication.version)));else if(Number(job.prNumber||0)>0&&job.githubState==='MERGED')states.append(element('span','badge MERGED',text('PR Merged')));else if(job.state==='COMPLETED'&&job.githubState==='CLOSED')states.append(element('span','badge SUCCEEDED',text('PR Archived')));if(job.
|
|
250
|
+
function renderJobList(){jobList.replaceChildren();const visible=filteredJobs();if(!jobs.length){jobList.append(element('li','empty',text('No durable jobs discovered.')));return;}if(!visible.length){jobList.append(element('li','empty',text('No jobs match the selected repo.')));return;}for(const job of visible){const row=element('div','job-row'+(isCompleteUnmerged(job)?' complete-unmerged':''));row.setAttribute('aria-current',String(jobSelectionKey(job)===selectedJob));row.addEventListener('click',()=>selectJob(job));const content=element('div','job-content');const select=document.createElement('button');select.type='button';select.className='job-button';select.setAttribute('aria-current',String(jobSelectionKey(job)===selectedJob));select.title=job.title||job.id;select.addEventListener('click',event=>{event.stopPropagation();selectJob(job);});select.append(element('span','job-title',text(repoName(job.repository)+'/'+value(job.id)+(Number(job.prNumber||0)>0?'/PR#'+value(job.prNumber):'')+': '+value(job.title||job.id))));const states=element('span','job-status-line');states.append(element('span','badge '+value(job.state),text(value(job.state))),element('span','badge '+value(job.runtimeStatus||'IDLE'),text(value(job.runtimeStatus||'IDLE'))));if(job.publicationFailure)states.append(element('span','badge FAILED',text('PUBLISH FAILED: '+value(job.publicationFailure.stage))));else if(job.publication?.version)states.append(element('span','badge SUCCEEDED',text('v'+job.publication.version)));else if(Number(job.prNumber||0)>0&&job.githubState==='MERGED')states.append(element('span','badge MERGED',text('PR Merged')));else if(job.state==='COMPLETED'&&job.githubState==='CLOSED')states.append(element('span','badge SUCCEEDED',text('PR Archived')));if(job.state==='ACTIVE'&&job.reviewEnabled!==false)states.append(element('span','badge flag-review',text('REVIEW REQUIRED')));if(job.localMerge?.status==='failed')states.append(element('span','badge FAILED',text('MERGE FAILED: '+value(job.localMerge.error||'Unknown error'))));else if(isCompleteUnmerged(job))states.append(element('span','badge MERGE_PENDING',text('MERGE PENDING')));const primary=element('div','job-primary');primary.append(states);const lastLine=element('div','job-last-line');lastLine.append(element('span','subtle',text(compactCounts(counts(job.tasks)))),element('span','subtle',text(relativeTime(job.updatedAt))));content.append(select,primary,lastLine);row.append(content);jobList.append(element('li','',row));}}
|
|
254
251
|
function taskTagDisplay(job,task){const display=task.tag&&job?.tag?.values?.[task.tag];return typeof display==='string'&&display?display:null;}
|
|
255
252
|
function taskVerdict(job,task){if(task.state!=="SUCCEEDED")return null;const data=task.result?.data||{};if(task.agentName==="reviewer"){const verdict=data.verdict||((job?.review?.taskId===task.id)?job.review.outcome:null);return ({approved:"APPROVED",pass:"APPROVED","changes-required":"CHANGES REQUIRED",changes_required:"CHANGES REQUIRED"})[verdict]||null;}if(task.agentName==="troubleshooter"){const original=job?.tasks?.find(t=>t.id===task.parentTaskId);const resolution=original?.resolution;if(resolution?.state!=="resolved"||resolution.owner!=="troubleshooter")return null;return ({exact_original_retry:"RETRY VERIFIED",same_job_dependency:"DEPENDENCY REGISTERED",blocker_job_dependency:"BLOCKER REGISTERED",cancel_supersede:"SUPERSEDED"})[resolution.path]||null;}return null;}function taskSubtreeComplete(task,children){return ['SUCCEEDED','CANCELLED'].includes(task.state)&&(children.get(task.id)||[]).every(child=>taskSubtreeComplete(child,children));}
|
|
256
253
|
function taskStateLabel(task){const resolution=task?.resolution?.state;return task?.state==='FAILED'&&typeof resolution==='string'&&resolution.trim()?task.state+' / '+resolution:task.state;}
|
|
@@ -265,10 +262,10 @@ async function setTaskState(task,state,select){const job=selected();if(!job||sta
|
|
|
265
262
|
function taskStateField(task){const box=element('div','detail-group');box.append(element('p','detail-label',text('State')));const select=document.createElement('select');select.className='job-filter task-state-select';select.setAttribute('aria-label','Task state');for(const state of TASK_STATES)select.append(new Option(state,state));select.value=task.state;select.addEventListener('change',()=>void setTaskState(task,select.value,select));box.append(select);return box;}
|
|
266
263
|
function manualOverrideField(task){const rows=Array.isArray(task.manualStateOverrides)?task.manualStateOverrides:[];const last=rows.at(-1);return field('Manual override',last?last.from+' → '+last.to+' · '+relativeTime(last.at):'—');}
|
|
267
264
|
function showDetailsDialog(title,sections){const dialog=element('dialog','details-dialog');dialog.setAttribute('aria-label',title);const head=element('div','details-dialog-head');const name=element('strong','',text(title));const close=element('button','refresh-button',text('Close'));close.type='button';close.addEventListener('click',()=>dialog.close());head.append(name,close);dialog.append(head);for(const [label,value] of sections){if(value===undefined||value===null||value==='')continue;dialog.append(element('h3','',text(label)));const content=element('pre');content.textContent=typeof value==='string'?value:JSON.stringify(value,null,2);dialog.append(content);}dialog.addEventListener('close',()=>dialog.remove());document.body.append(dialog);dialog.showModal();}function openJobDetails(){const job=selected();if(!job)return;showDetailsDialog('Job '+job.id+' · '+value(job.title),[['Objective',job.objective],['Description',job.description],['State',job.state],['Plans',job.plans?.map(p=>({id:p.id,status:p.status,assessment:p.assessment,error:p.error,generatedTaskIds:p.generatedTaskIds}))],['Review',job.review],['Outcome',job.result],['Publication',job.publication],['Failure',job.error]]);}function openTaskDetails(task){const result=task.result?.data??task.result;showDetailsDialog('Task '+task.id+' · '+value(task.title),[['Input / task objective',task.input],['State',task.state],['Conclusion',result],['Result summary',task.result?.summary],['Planner assessment',task.plannerResult?.assessment],['Resolution',task.resolution],['Resolution classification',task.resolutionClassification],['Failure',task.failureReason||task.error],['Dependencies',task.dependencies]]);}document.querySelector('#job-content-open').addEventListener('click',openJobDetails);function renderTaskDetail(task){taskDetailRoot.replaceChildren();if(!task){taskDetailRoot.append(element('p','empty',text('Select a task to inspect execution details.')));return;}const routing=task.routing||{};const groups=element('div','detail-groups');groups.append(field('Title',task.title||'—'),field('Task ID',task.id),field('Kind',task.kind||'work'),taskStateField(task),...(task.state==='FAILED'&&task.resolution?.state?[field('Resolution state',task.resolution.state),field('Resolution path',task.resolution.path||'—'),field('Resolution target',task.resolution.targetTaskId||'—')]:[]),manualOverrideField(task),field('Planning decision',task.planningDecisionId||'—'),field('Planning source',task.planningSource||'—'),field('Planning round',task.planningRound===undefined?'—':String(task.planningRound)),field('Project / agent',value(task.projectName)+' / '+value(task.agentName)),field('Parent task',task.parentTaskId||'root'),...(task.resolutionClassification?[field('Resolution classification',task.resolutionClassification.kind),field('Resolution target',task.resolutionClassification.targetTaskId),field('Resolution evidence',task.resolutionClassification.evidence),field('Resolution reason',task.resolutionClassification.reason)]:[]),field('Adapter',task.adapter),field('Provider / model',value(routing.provider)+' / '+value(routing.model)),field('Reasoning / profile',value(routing.reasoning||routing.profile)),field('Attempt',attemptBudget(task)),field('Dependencies',Array.isArray(task.dependencies)&&task.dependencies.length?task.dependencies.join(', '):'none'),taskCommentField(task),chatField(task));const job=selected();if(task.reviewHeadSha&&job)groups.append(commitField('HEAD commit',job,task.reviewHeadSha));const graph=agentGraphField(task);if(graph)groups.append(graph);if(task.plannerResult)groups.append(field('Objective status',task.plannerResult.objective_status),markdownField('Assessment',task.plannerResult.assessment));if(task.error)groups.append(field('Error',task.error));const result=task.result?.data??task.result;if(result){groups.append(field('Conclusion',result.verdict||result.outcome?.verdict||task.result?.summary||'Available'));}taskDetailRoot.append(groups);const details=element('button','refresh-button',text('Task details / conclusion'));details.type='button';details.addEventListener('click',()=>openTaskDetails(task));taskDetailRoot.append(details);}
|
|
268
|
-
function reviewStatus(job){const review=job?.review;if(!review)return 'Not started';if(review.outcome==='approved'&&review.headSha===job.headSha)return 'MERGE READY · approved';const stale=Array.isArray(job?.tasks)&&job.tasks.some(task=>task.agentName==='reviewer'&&task.reviewHeadSha&&job.headSha&&task.reviewHeadSha!==job.headSha);if(stale)return 'STALE · awaiting review';return String(review.outcome||'pending').replace('_',' ').toUpperCase();}function renderSelectedJob(){jobDetail.replaceChildren();const job=selected();if(!job){selectedJobTitle.textContent='';jobDetail.append(element('div','empty',text('No durable jobs discovered.')));buildTree([]);renderTaskDetail(null);return;}selectedJobTitle.textContent=value(job.title||job.id);if(jobSelectionKey(job)!==selectedJob){selectedJob=jobSelectionKey(job);selectedTaskId=null;eventTaskId=null;clearEvents();rememberSelection();}const summary=element('div','job-summary');summary.append(field('PR state',job.githubState||'UNKNOWN'),jobStateField(job),field('Runtime status',job.runtimeStatus||'IDLE'),field('
|
|
265
|
+
function reviewStatus(job){const review=job?.review;if(!review)return 'Not started';if(review.outcome==='approved'&&review.headSha===job.headSha)return 'MERGE READY · approved';const stale=Array.isArray(job?.tasks)&&job.tasks.some(task=>task.agentName==='reviewer'&&task.reviewHeadSha&&job.headSha&&task.reviewHeadSha!==job.headSha);if(stale)return 'STALE · awaiting review';return String(review.outcome||'pending').replace('_',' ').toUpperCase();}function renderSelectedJob(){jobDetail.replaceChildren();const job=selected();if(!job){selectedJobTitle.textContent='';jobDetail.append(element('div','empty',text('No durable jobs discovered.')));buildTree([]);renderTaskDetail(null);return;}selectedJobTitle.textContent=value(job.title||job.id);if(jobSelectionKey(job)!==selectedJob){selectedJob=jobSelectionKey(job);selectedTaskId=null;eventTaskId=null;clearEvents();rememberSelection();}const summary=element('div','job-summary');summary.append(field('PR state',job.githubState||'UNKNOWN'),jobStateField(job),field('Runtime status',job.runtimeStatus||'IDLE'),field('Review',reviewStatus(job)),field('Creator thread',job.creatorThreadId||'—'),threadField('Implementation thread',job.implementationThreadId),threadField('Reviewer thread',job.reviewerThreadId),commitField('Review head SHA',job,job.review?.headSha||job.headSha||''),prField(job),field('Repository',job.repository||'—'),field('Last activity',relativeTime(job.updatedAt)),field('Task states',compactCounts(job.tasks)),chatShortcutField(job));jobDetail.append(summary);const selectedTask=(job.tasks||[]).find(task=>task.id===selectedTaskId)||null;if(selectedTaskId&&!selectedTask){selectedTaskId=null;eventTaskId=null;clearEvents();rememberSelection();}buildTree(job.tasks||[]);renderTaskDetail(selectedTask);renderEventScope();}function renderAll(){jobs.sort(compareJobs);syncFilters();renderJobList();renderSelectedJob();syncMobileJobsView();}
|
|
269
266
|
function isCompleteUnmerged(job){return job.runtimeStatus==='MERGE_PENDING';}
|
|
270
267
|
function isTerminalJob(job){return ['COMPLETED','CANCELLED'].includes(job.state)||['MERGED','CLOSED'].includes(job.githubState);}
|
|
271
|
-
function jobGroup(job){if(isTerminalJob(job))return 3;if(job.
|
|
268
|
+
function jobGroup(job){if(isTerminalJob(job))return 3;if(job.draft||job.state==='DRAFT')return 2;return 0;}
|
|
272
269
|
function compareJobs(a,b){const groupDelta=jobGroup(a)-jobGroup(b);if(groupDelta)return groupDelta;const mergePendingDelta=Number(isCompleteUnmerged(b))-Number(isCompleteUnmerged(a));return mergePendingDelta||String(b.updatedAt).localeCompare(String(a.updatedAt));}
|
|
273
270
|
async function loadJSON(url){const response=await fetch(url);if(!response.ok)throw new Error(response.status+' '+response.statusText);return response.json();}
|
|
274
271
|
async function refreshVersion(){try{const live=await loadJSON('/api/version');if(live?.version&&live.version!==DASHBOARD_VERSION)location.reload();}catch{}}
|
|
@@ -343,7 +340,6 @@ jobsRefresh.addEventListener('click',()=>void refresh(true));
|
|
|
343
340
|
eventsAllTasks.addEventListener('click',()=>{eventTaskId=null;clearEvents();renderEventScope();void loadEventHistory();});
|
|
344
341
|
repoFilter.addEventListener('change',()=>{selectedRepo=repoFilter.value;sessionStorage.setItem('agents-relay:repo-filter',selectedRepo);renderJobList();});
|
|
345
342
|
jobSearch.addEventListener('input',()=>{searchQuery=jobSearch.value;sessionStorage.setItem('agents-relay:job-search',searchQuery);renderJobList();});
|
|
346
|
-
quickAutonomous.addEventListener('click',()=>toggleQuickMode('autonomous'));
|
|
347
343
|
quickDraft.addEventListener('click',()=>toggleQuickMode('draft'));
|
|
348
344
|
quickStatus.addEventListener('change',()=>setQuickMode(quickStatus.value?'status':'jobs',quickStatus.value));
|
|
349
345
|
selectedJobRefresh.addEventListener('click',()=>void refreshSelected());
|
package/dist/dashboard-style.js
CHANGED
|
@@ -149,7 +149,6 @@ export const dashboardStyles = ` <style>
|
|
|
149
149
|
#jobs-view .job-filters { width:auto; }
|
|
150
150
|
#jobs-view .job-search,
|
|
151
151
|
#jobs-view #quick-status,
|
|
152
|
-
#jobs-view #quick-autonomous,
|
|
153
152
|
#jobs-view #quick-draft { display:none; }
|
|
154
153
|
#jobs-view .panel-body { max-height:none; }
|
|
155
154
|
#jobs-workspace { gap:.65rem; }
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { transitionTaskEvent } from './
|
|
1
|
+
import { transitionTaskEvent } from './task-transitions.js';
|
|
2
2
|
import { reclaimStaleExecutionProcess } from './execution-process-tracker.js';
|
|
3
3
|
async function currentGeneration(store, job, stale) {
|
|
4
4
|
const task = (await store.load(job.id)).tasks.find(item => item.id === stale.id);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { classifyTaskResolution } from './workflow/task-resolution.js';
|
|
2
|
-
import { tryTransitionTaskEvent } from './
|
|
2
|
+
import { tryTransitionTaskEvent } from './task-transitions.js';
|
|
3
3
|
import { satisfiesWorker } from './worker-routing.js';
|
|
4
4
|
function dependencySatisfied(task, byId) {
|
|
5
5
|
return task.dependencies.every(id => {
|
|
@@ -68,6 +68,16 @@ export function forceRestateConverged(job, planId, candidates) {
|
|
|
68
68
|
&& job.tasks.every(task => task.kind === 'planner' || !['DRAFT', 'QUEUED', 'RUNNING', 'BLOCKED'].includes(task.state));
|
|
69
69
|
return completed || forceRestateRunnable(job, candidates) || forceRestateDependencyWaiting(job, candidates) || forceRestateBlocked(job, planId);
|
|
70
70
|
}
|
|
71
|
+
/** A satisfied objective can enter Relay-owned Review before Job completion.
|
|
72
|
+
* Never treat completed Planner execution alone as implementation evidence. */
|
|
73
|
+
export function objectiveSatisfiedForReview(job) {
|
|
74
|
+
const latest = (job.plans ?? []).at(-1);
|
|
75
|
+
if (latest?.status !== 'SUCCEEDED' || latest.objectiveStatus !== 'satisfied')
|
|
76
|
+
return false;
|
|
77
|
+
const implementation = job.tasks.filter(task => task.kind !== 'planner' && task.agentName !== 'reviewer' && task.agentName !== 'troubleshooter'
|
|
78
|
+
&& task.resolutionClassification?.kind !== 'superseded');
|
|
79
|
+
return implementation.length > 0 && implementation.every(task => task.state === 'SUCCEEDED');
|
|
80
|
+
}
|
|
71
81
|
/** Apply Planner-proposed history dispositions to a private candidate snapshot.
|
|
72
82
|
* The caller invokes this before the storage mutator returns the candidate. */
|
|
73
83
|
export function applyPlannerHistoryCleanup(job, expectedTaskIds, cleanup, now) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { shouldRecoverInteractiveDecision } from './interactive-invocation.js';
|
|
2
|
-
import { tryTransitionTaskEvent } from './
|
|
2
|
+
import { tryTransitionTaskEvent } from './task-transitions.js';
|
|
3
3
|
import { applyTaskUnblockRetrySnapshot } from './task-actions.js';
|
|
4
4
|
export async function resumeInteractiveTask(store, jobId, taskId, invocationId, revision, decision) {
|
|
5
5
|
if (!store.mutateJob)
|
package/dist/job-actions.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { transitionJobEvent } from './
|
|
1
|
+
import { transitionJobEvent } from './task-transitions.js';
|
|
2
2
|
import { tryApplyTaskCancelAction } from './task-actions.js';
|
|
3
3
|
import { decideJobLifecycle, dispatchJobLifecycleSync } from './workflow/lifecycle.js';
|
|
4
4
|
export const JOB_ACTIVATE_ACTION = 'job.activate';
|
package/dist/job-pause.js
CHANGED
|
@@ -7,7 +7,7 @@ export function migrateLegacyBlockedTasks(db) {
|
|
|
7
7
|
for (const row of rows) {
|
|
8
8
|
const job = JSON.parse(row.json);
|
|
9
9
|
const before = JSON.stringify(job.tasks);
|
|
10
|
-
job.tasks = job.tasks.map(task => normalizeLegacyTaskLifecycle(task
|
|
10
|
+
job.tasks = job.tasks.map(task => normalizeLegacyTaskLifecycle(task));
|
|
11
11
|
if (JSON.stringify(job.tasks) !== before)
|
|
12
12
|
update.run(JSON.stringify(job), row.job_id);
|
|
13
13
|
}
|