agents-relay 2.0.80 → 2.0.82
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 +3 -3
- package/dist/cli-help.js +5 -8
- package/dist/cli-review.js +1 -1
- package/dist/cli-support.js +2 -22
- package/dist/cli-worker-task.js +7 -6
- package/dist/cli.js +22 -59
- package/dist/dashboard-actions.js +2 -1
- package/dist/dashboard-html.js +5 -5
- package/dist/dashboard.js +0 -4
- package/dist/github-binding-order.js +1 -1
- package/dist/job-pr-number-migration.js +24 -0
- package/dist/job-task-id-sequence.js +16 -9
- package/dist/legacy-orchestrator-repair.js +51 -0
- package/dist/local-job-registry.js +2 -3
- package/dist/local-merge-delivery.js +1 -1
- package/dist/managed-worker-input.js +3 -4
- package/dist/planner.js +14 -8
- package/dist/pool.js +3 -1
- package/dist/reconciler-review.js +7 -4
- package/dist/reconciler-runtime.js +7 -0
- package/dist/reconciler.js +2 -8
- package/dist/relay-store.js +6 -4
- package/dist/relayd-control.js +10 -0
- package/dist/relayd-dashboard-actions.js +3 -33
- package/dist/relayd.js +17 -20
- package/dist/review-generation-fence.js +8 -0
- package/dist/review-terminal-normalization.js +27 -0
- package/dist/reviewer-thread.js +1 -1
- package/dist/runtime-status.js +2 -1
- package/dist/task-actions.js +47 -42
- package/dist/task-title.js +5 -0
- package/dist/troubleshooter.js +2 -2
- package/dist/worker-routing.js +1 -1
- package/dist/workflow/entity-creation.js +11 -1
- package/dist/workflow/orchestrator-rescue.js +62 -18
- package/dist/workflow/troubleshooter-v2.js +1 -1
- package/dist/workspace.js +5 -4
- package/package.json +1 -1
- package/skills/agents-relay/contracts/orchestrator-workflows.md +13 -12
- package/workflow/orchestrator-rescue.mmd +16 -1
- package/workflow/troubleshooter.mmd +4 -4
package/README.md
CHANGED
|
@@ -266,11 +266,11 @@ npx agents-relay task create --repo OWNER/REPO --pr 5 --id job-1 \
|
|
|
266
266
|
|
|
267
267
|
GitHub PR state is authoritative for terminal lifecycle: a merged PR reconciles its managed job to `COMPLETED`; a closed, unmerged PR reconciles to `CANCELLED` and cannot launch queued work. The dashboard exposes `/api/jobs` and shows PR state, durable Job lifecycle, and derived runtime status separately. A merged PR reconciles lifecycle to `COMPLETED`; a closed, unmerged PR reconciles lifecycle to `CANCELLED`.
|
|
268
268
|
|
|
269
|
-
|
|
269
|
+
Relay has no orchestrator worker adapter for new work. Orchestrators are external clients that create/plan durable Tasks; Execution Router selects a real worker route (`codex` or `chatgpt`). The CLI and dashboard do not expose manual orchestrator-only `task start`, `task complete`, or synthetic `record` paths.
|
|
270
270
|
|
|
271
|
-
|
|
271
|
+
Historical stores may still contain `adapter: orchestrator`. Reconciliation treats that value only as legacy migration evidence: QUEUED work that never started can be converted in place to normal worker requirements; RUNNING work with an unknown outcome is quarantined as BLOCKED instead of replayed; only explicit durable `externalCompletionVerified` evidence can complete legacy work without execution. New Task creation and Task update reject that adapter.
|
|
272
272
|
|
|
273
|
-
For
|
|
273
|
+
For managed jobs, the GitHub PR body is the canonical collaboration surface while SQLite remains lifecycle authority.
|
|
274
274
|
|
|
275
275
|
### Automatic task troubleshooting
|
|
276
276
|
|
package/dist/cli-help.js
CHANGED
|
@@ -116,7 +116,7 @@ Actions:
|
|
|
116
116
|
'task create': `Create one durable Task under an existing Job.
|
|
117
117
|
|
|
118
118
|
Required: normal Job storage options plus --input-file PATH|- (preferred) or --input MARKDOWN, and --output task-pr|file. --input and --input-file are mutually exclusive.
|
|
119
|
-
Common: --task-id ID (optional; Relay auto-assigns
|
|
119
|
+
Common: --task-id ID (optional; Relay auto-assigns a global increasing integer Task ID), --title TITLE (optional; otherwise derived from the first non-empty input line), --idempotency-key KEY, --priority P0|P1|P2|P3, --story-size S|M|L, --agent NAME, --agent-graph-file PATH|-, --deps a,b, --parent ID, --draft. Normal model-backed Tasks omit adapter/provider/model so Relay runs one Execution Router decision over complete worker+model routes. New Tasks are worker-routed; legacy adapter=orchestrator records are recovery-only and cannot be created or manually executed.\nTasks with dependencies start DRAFT; tasks without dependencies start QUEUED unless --draft is given.
|
|
120
120
|
Directly-created Tasks are not wrapped in a synthetic Plan. Planner-generated Tasks are created internally by Relay from planner output.
|
|
121
121
|
|
|
122
122
|
Example:
|
|
@@ -124,7 +124,7 @@ Example:
|
|
|
124
124
|
'task update': `Update mutable durable Task fields in place.
|
|
125
125
|
|
|
126
126
|
Required: --task-id ID and the normal Job storage options.
|
|
127
|
-
Mutable while not RUNNING/SUCCEEDED/CANCELLED: --adapter, --priority, --story-size, --input-file PATH|- or --input MARKDOWN, --project, --agent, --agent-graph-file PATH|-, --parent, --deps, --capabilities, --timeout, --max-attempts, --provider, --model, --profile, --reasoning, --cwd, --chatgpt-project, --chatgpt-tab-close-policy, --draft, --queue.
|
|
127
|
+
Mutable while not RUNNING/SUCCEEDED/CANCELLED: --title, --adapter, --priority, --story-size, --input-file PATH|- or --input MARKDOWN, --project, --agent, --agent-graph-file PATH|-, --parent, --deps, --capabilities, --timeout, --max-attempts, --provider, --model, --profile, --reasoning, --cwd, --chatgpt-project, --chatgpt-tab-close-policy, --draft, --queue.
|
|
128
128
|
Use --clear-routing to remove routing metadata.
|
|
129
129
|
|
|
130
130
|
Example:
|
|
@@ -135,12 +135,6 @@ Required: --task-id ID and the normal Job storage options.`,
|
|
|
135
135
|
'task list': `List durable Tasks for one Job.
|
|
136
136
|
|
|
137
137
|
Required: the normal Job storage options.`,
|
|
138
|
-
'task start': `Mark one orchestrator-owned self Task RUNNING when the creating orchestrator actually begins the work.
|
|
139
|
-
|
|
140
|
-
Required: --task-id ID and the normal Job storage options. The Task must use adapter=orchestrator and be QUEUED. Start must occur before task complete.`,
|
|
141
|
-
'task complete': `Complete one orchestrator-owned Task after the creating orchestrator has finished the work.
|
|
142
|
-
|
|
143
|
-
Required: --task-id ID, --summary TEXT, and the normal Job storage options. The Task must use adapter=orchestrator and already be RUNNING. This command is for the same orchestrator turn that created the self-task; it is not a deferred handoff mechanism.`,
|
|
144
138
|
'task retry': `Retry one durable Task.
|
|
145
139
|
|
|
146
140
|
Required: --task-id ID and the normal Job storage options.`,
|
|
@@ -150,6 +144,9 @@ Required: --task-id ID, --recovery-task-id TROUBLESHOOTER_ID, and the normal Job
|
|
|
150
144
|
'task cancel': `Cancel one durable Task.
|
|
151
145
|
|
|
152
146
|
Required: --task-id ID and the normal Job storage options.`,
|
|
147
|
+
'task supersede': `Mark an already-CANCELLED durable Task as superseded by later corrected work through the Task Resolution Graph.
|
|
148
|
+
|
|
149
|
+
Required: --task-id ID, --reason TEXT, and the normal managed Job storage options. This does not change Task state and cannot supersede FAILED/BLOCKED/runnable work.`,
|
|
153
150
|
plan: `Invoke the Planner against the current durable Job state.
|
|
154
151
|
|
|
155
152
|
Required: --repo OWNER/REPO, --pr NUMBER, and --id JOB_ID.
|
package/dist/cli-review.js
CHANGED
|
@@ -14,5 +14,5 @@ export async function runReviewCommand(action, reviewArgs) {
|
|
|
14
14
|
throw new Error('review request requires --repo and --id');
|
|
15
15
|
const threadId = reviewRequestThreadOverride(reviewArgs);
|
|
16
16
|
const job = await relayControlCommand(reviewArgs, { command: 'review.request', jobId, ...(threadId !== undefined ? { threadId } : {}) });
|
|
17
|
-
console.log(JSON.stringify({ jobId: job.id, pr: job.
|
|
17
|
+
console.log(JSON.stringify({ jobId: job.id, pr: job.prNumber ?? 0, reviewerThreadId: job.reviewerThreadId ?? null, status: 'requested', note: 'Relay graph will create the review Task once eligible; this is not a delivery receipt.' }, null, 2));
|
|
18
18
|
}
|
package/dist/cli-support.js
CHANGED
|
@@ -77,9 +77,9 @@ export async function storeFor(args) {
|
|
|
77
77
|
if (!binding || binding.job.repository !== repo)
|
|
78
78
|
throw new Error('Immutable local Job not found for repository: ' + jobId);
|
|
79
79
|
const suppliedPr = arg(args, '--pr');
|
|
80
|
-
const boundPr = binding.job.
|
|
80
|
+
const boundPr = binding.job.prNumber > 0 ? binding.job.prNumber : 0;
|
|
81
81
|
if (suppliedPr && boundPr !== Number(suppliedPr))
|
|
82
|
-
throw new Error('Local Job
|
|
82
|
+
throw new Error('Local Job/PR binding mismatch');
|
|
83
83
|
return { store: binding.store, job: await binding.store.load(jobId), localFile: null };
|
|
84
84
|
}
|
|
85
85
|
export async function saveLocal(file, job) { if (file)
|
|
@@ -247,26 +247,6 @@ export async function publishTaskUnblocked(bus, job, taskId, reason) { if (!bus)
|
|
|
247
247
|
catch (error) {
|
|
248
248
|
process.stderr.write(`warning: unblock event unavailable: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
249
249
|
} }
|
|
250
|
-
export async function publishOrchestratorTaskStarted(bus, job, task) {
|
|
251
|
-
if (!bus)
|
|
252
|
-
return;
|
|
253
|
-
try {
|
|
254
|
-
await bus.publish(eventFor(job.id, task.id, task.parentTaskId, 'task.started', 'running', `Task ${task.id} started by orchestrator`, 'orchestrator', { adapter: 'orchestrator' }, { agent: 'relay', component: 'agents-relay' }));
|
|
255
|
-
}
|
|
256
|
-
catch (error) {
|
|
257
|
-
process.stderr.write(`warning: orchestrator task start event unavailable for ${job.id}/${task.id}: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
export async function publishOrchestratorTaskCompletion(bus, job, task, summary) {
|
|
261
|
-
if (!bus)
|
|
262
|
-
return;
|
|
263
|
-
try {
|
|
264
|
-
await bus.publish(eventFor(job.id, task.id, task.parentTaskId, 'task.completed', 'succeeded', `Task ${task.id} completed by orchestrator`, 'orchestrator', { adapter: 'orchestrator', summary }, { agent: 'relay', component: 'agents-relay' }));
|
|
265
|
-
}
|
|
266
|
-
catch (error) {
|
|
267
|
-
process.stderr.write(`warning: orchestrator task completion event unavailable for ${job.id}/${task.id}: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
250
|
export async function publishDependentUnblocked(bus, targets, reason) { if (!bus)
|
|
271
251
|
return; for (const target of targets) {
|
|
272
252
|
try {
|
package/dist/cli-worker-task.js
CHANGED
|
@@ -14,23 +14,24 @@ export async function workerTaskFromArgs(job, args, now) {
|
|
|
14
14
|
parseAgentGraph(agentGraph);
|
|
15
15
|
if (!input.trim())
|
|
16
16
|
throw new Error('task create requires --input-file PATH|- or --input MARKDOWN');
|
|
17
|
-
if (!['codex', 'chatgpt'
|
|
18
|
-
throw new Error('task create --adapter must be codex
|
|
17
|
+
if (!['codex', 'chatgpt'].includes(adapter))
|
|
18
|
+
throw new Error('task create --adapter must be codex or chatgpt; orchestrator is reserved for legacy recovery only');
|
|
19
19
|
if (outputKind !== 'task-pr' && outputKind !== 'file')
|
|
20
20
|
throw new Error('task create requires --output task-pr|file');
|
|
21
21
|
if (outputKind === 'file' && !outputPath)
|
|
22
22
|
throw new Error('--output file requires --output-path');
|
|
23
|
-
if (
|
|
23
|
+
if (['--provider', '--profile', '--reasoning', '--cwd', '--chatgpt-project'].some(flag => hasArg(args, flag)) && !model)
|
|
24
24
|
throw new Error('task create explicit routing requires --model');
|
|
25
|
-
const routing =
|
|
26
|
-
const workerRequirements =
|
|
25
|
+
const routing = !model ? undefined : { provider: arg(args, '--provider', 'openai'), model, profile: arg(args, '--profile') || undefined, reasoning: arg(args, '--reasoning') || undefined, cwd: arg(args, '--cwd') || undefined, projectId: arg(args, '--chatgpt-project') || undefined, decidedBy: 'cli', decidedAt: now };
|
|
26
|
+
const workerRequirements = routing ? undefined : { capabilities: [], runtime: adapterValue ? adapter : undefined };
|
|
27
27
|
const closePolicy = hasArg(args, '--chatgpt-tab-close-policy') ? chatGptTabClosePolicy(args) : undefined;
|
|
28
28
|
const size = storySize(args);
|
|
29
29
|
const dependencies = arg(args, '--deps').split(',').filter(Boolean);
|
|
30
|
-
const timeoutMs =
|
|
30
|
+
const timeoutMs = hasArg(args, '--timeout') ? integerArg(args, '--timeout', taskOverallTimeoutMs(size, routing?.reasoning)) : routing ? taskOverallTimeoutMs(size, routing.reasoning) : 0;
|
|
31
31
|
return createTaskEntity(job, {
|
|
32
32
|
jobId: job.id,
|
|
33
33
|
id: arg(args, '--task-id', 'initial'),
|
|
34
|
+
title: arg(args, '--title') || undefined,
|
|
34
35
|
priority: priority(args, job.priority ?? 'P2'),
|
|
35
36
|
storySize: size,
|
|
36
37
|
projectName: arg(args, '--project') || undefined,
|
package/dist/cli.js
CHANGED
|
@@ -18,7 +18,7 @@ import { codexAndZaiUsageRegistry } from './usage.js';
|
|
|
18
18
|
import { decisionMode, executionDecisionEngine } from './decision.js';
|
|
19
19
|
import { discoverAgents } from './registry.js';
|
|
20
20
|
import { AgentObjectivePlanner, CommandObjectivePlanner } from './planner.js';
|
|
21
|
-
import {
|
|
21
|
+
import { applyTaskHoldAction, applyTaskQueueAction, dispatchTaskAction } from './task-actions.js';
|
|
22
22
|
import { applyJobActivateAction, applyJobCancelAction } from './job-actions.js';
|
|
23
23
|
import { resolveExecutionContext } from './execution-context.js';
|
|
24
24
|
import { startServiceExternalActionFlusher } from './service-external-actions.js';
|
|
@@ -59,12 +59,6 @@ async function runManagedTaskCommand(action, args) {
|
|
|
59
59
|
if (!id)
|
|
60
60
|
id = result.createdTaskId ?? '';
|
|
61
61
|
}
|
|
62
|
-
else if (action === 'complete') {
|
|
63
|
-
const summary = arg(args, '--summary');
|
|
64
|
-
if (!summary)
|
|
65
|
-
throw new Error('task complete requires --summary');
|
|
66
|
-
result = await relayTaskCommand(args, { jobId, taskId: id, action, summary });
|
|
67
|
-
}
|
|
68
62
|
else
|
|
69
63
|
result = await relayTaskCommand(args, { jobId, taskId: id, action });
|
|
70
64
|
const task = result.tasks.find(item => item.id === id);
|
|
@@ -73,7 +67,7 @@ async function runManagedTaskCommand(action, args) {
|
|
|
73
67
|
console.log(JSON.stringify(task, null, 2));
|
|
74
68
|
}
|
|
75
69
|
async function runTaskCommand(action, args) {
|
|
76
|
-
if (!['create', 'update', 'show', 'list', '
|
|
70
|
+
if (!['create', 'update', 'show', 'list', 'retry', 'recover', 'unblock', 'cancel', 'supersede'].includes(action))
|
|
77
71
|
throw new Error(usage);
|
|
78
72
|
const managed = !arg(args, '--file') && Boolean(arg(args, '--repo') && arg(args, '--id'));
|
|
79
73
|
if (managed) {
|
|
@@ -94,10 +88,20 @@ async function runTaskCommand(action, args) {
|
|
|
94
88
|
console.log(JSON.stringify(task, null, 2));
|
|
95
89
|
return;
|
|
96
90
|
}
|
|
97
|
-
if (['create', '
|
|
91
|
+
if (['create', 'retry', 'cancel'].includes(action)) {
|
|
98
92
|
await runManagedTaskCommand(action, args);
|
|
99
93
|
return;
|
|
100
94
|
}
|
|
95
|
+
if (action === 'supersede') {
|
|
96
|
+
if (!taskId)
|
|
97
|
+
throw new Error('task supersede requires --task-id');
|
|
98
|
+
const reason = arg(args, '--reason');
|
|
99
|
+
if (!reason.trim())
|
|
100
|
+
throw new Error('task supersede requires --reason');
|
|
101
|
+
const job = await relayControlCommand(args, { command: 'task.supersede', jobId, taskId, reason });
|
|
102
|
+
console.log(JSON.stringify(job.tasks.find(item => item.id === taskId), null, 2));
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
101
105
|
if (action === 'recover') {
|
|
102
106
|
if (!taskId)
|
|
103
107
|
throw new Error('task recover requires --task-id');
|
|
@@ -126,10 +130,12 @@ async function runTaskCommand(action, args) {
|
|
|
126
130
|
const expectedUpdatedAt = task.updatedAt;
|
|
127
131
|
if (['RUNNING', 'UNBLOCKED', 'SUCCEEDED', 'CANCELLED'].includes(task.state))
|
|
128
132
|
throw new Error(`Task ${taskId} is ${task.state}; durable execution fields are immutable in this state`);
|
|
133
|
+
if (hasArg(args, '--title'))
|
|
134
|
+
task.title = arg(args, '--title');
|
|
129
135
|
if (hasArg(args, '--adapter')) {
|
|
130
136
|
const adapter = arg(args, '--adapter');
|
|
131
|
-
if (!['codex', 'chatgpt'
|
|
132
|
-
throw new Error('--adapter must be codex
|
|
137
|
+
if (!['codex', 'chatgpt'].includes(adapter))
|
|
138
|
+
throw new Error('--adapter must be codex or chatgpt');
|
|
133
139
|
task.adapter = adapter;
|
|
134
140
|
}
|
|
135
141
|
if (hasArg(args, '--priority'))
|
|
@@ -203,23 +209,6 @@ async function runTaskCommand(action, args) {
|
|
|
203
209
|
await runTaskUnblockCommand(args, store, job, task, localFile);
|
|
204
210
|
return;
|
|
205
211
|
}
|
|
206
|
-
if (action === 'start') {
|
|
207
|
-
await applyOrchestratorStartAction(task);
|
|
208
|
-
await store.saveTask(task);
|
|
209
|
-
await saveLocal(localFile, await store.load(job.id));
|
|
210
|
-
console.log(JSON.stringify(task, null, 2));
|
|
211
|
-
return;
|
|
212
|
-
}
|
|
213
|
-
if (action === 'complete') {
|
|
214
|
-
const summary = arg(args, '--summary');
|
|
215
|
-
if (!summary)
|
|
216
|
-
throw new Error('task complete requires --summary');
|
|
217
|
-
await applyOrchestratorCompleteAction(task, summary);
|
|
218
|
-
await store.saveTask(task);
|
|
219
|
-
await saveLocal(localFile, await store.load(job.id));
|
|
220
|
-
console.log(JSON.stringify(task, null, 2));
|
|
221
|
-
return;
|
|
222
|
-
}
|
|
223
212
|
if (action === 'retry' || action === 'recover' || action === 'cancel') {
|
|
224
213
|
let outputJob = job;
|
|
225
214
|
let outputTask = task;
|
|
@@ -250,8 +239,8 @@ async function runTaskCommand(action, args) {
|
|
|
250
239
|
throw new Error(`Task ${id} is ${task.state}; durable execution fields are immutable in this state`);
|
|
251
240
|
if (hasArg(args, '--adapter')) {
|
|
252
241
|
const adapter = arg(args, '--adapter');
|
|
253
|
-
if (!['codex', 'chatgpt'
|
|
254
|
-
throw new Error('--adapter must be codex
|
|
242
|
+
if (!['codex', 'chatgpt'].includes(adapter))
|
|
243
|
+
throw new Error('--adapter must be codex or chatgpt');
|
|
255
244
|
task.adapter = adapter;
|
|
256
245
|
}
|
|
257
246
|
if (hasArg(args, '--priority'))
|
|
@@ -402,7 +391,7 @@ async function runJobCommand(action, args) {
|
|
|
402
391
|
console.log(JSON.stringify(job, null, 2));
|
|
403
392
|
return;
|
|
404
393
|
}
|
|
405
|
-
const boundPrNumber = job.
|
|
394
|
+
const boundPrNumber = job.prNumber > 0 ? job.prNumber : 0;
|
|
406
395
|
if (job.repository !== repo || (prValue && boundPrNumber !== prNumber)) {
|
|
407
396
|
throw new Error('Local Job/PR binding mismatch');
|
|
408
397
|
}
|
|
@@ -504,7 +493,7 @@ export async function createService(args) {
|
|
|
504
493
|
const webhook = !noGitHubMode(args) && repo && webhookSecret ? {
|
|
505
494
|
secret: webhookSecret,
|
|
506
495
|
repositories: new Set([repo]),
|
|
507
|
-
pullRequest: loaded.job.
|
|
496
|
+
pullRequest: loaded.job.prNumber,
|
|
508
497
|
jobId: loaded.job.id,
|
|
509
498
|
wake: async (event) => {
|
|
510
499
|
if (event.pullRequestFacts && loaded.store.applyPullRequestFacts) {
|
|
@@ -655,7 +644,7 @@ async function main() {
|
|
|
655
644
|
console.log(JSON.stringify(await response.json(), null, 2));
|
|
656
645
|
return;
|
|
657
646
|
}
|
|
658
|
-
if (!arg(args, '--file') && ['plan', 'reconcile', '
|
|
647
|
+
if (!arg(args, '--file') && ['plan', 'reconcile', 'agent-register', 'agent-discover'].includes(command)) {
|
|
659
648
|
const jobId = arg(args, '--id');
|
|
660
649
|
if (!jobId)
|
|
661
650
|
throw new Error(`${command} requires --repo and --id`);
|
|
@@ -668,18 +657,6 @@ async function main() {
|
|
|
668
657
|
console.log(JSON.stringify(await relayControlCommand(args, { command: 'job.reconcile', jobId }), null, 2));
|
|
669
658
|
return;
|
|
670
659
|
}
|
|
671
|
-
if (command === 'record') {
|
|
672
|
-
const now = new Date().toISOString();
|
|
673
|
-
const id = arg(args, '--task-id', randomUUID());
|
|
674
|
-
const summary = arg(args, '--summary');
|
|
675
|
-
if (!summary)
|
|
676
|
-
throw new Error('record requires --summary');
|
|
677
|
-
const commit = arg(args, '--commit');
|
|
678
|
-
const task = { jobId, id, projectName: arg(args, '--project') || undefined, agentName: arg(args, '--agent', 'orchestrator'), parentTaskId: arg(args, '--parent') || null, dependencies: arg(args, '--deps').split(',').filter(Boolean), capabilities: [], adapter: 'orchestrator', input: arg(args, '--input', summary), continuation: null, continuationDeliveredAt: null, state: 'SUCCEEDED', attempt: 1, maxAttempts: 1, leaseOwner: null, leaseExpiresAt: null, executionId: commit || null, threadId: null, result: { summary, data: commit ? { commit } : undefined }, error: null, timeoutMs: 0, createdAt: now, updatedAt: now };
|
|
679
|
-
const updated = await relayControlCommand(args, { command: 'record', jobId, task });
|
|
680
|
-
console.log(JSON.stringify(updated.tasks.find(item => item.id === id), null, 2));
|
|
681
|
-
return;
|
|
682
|
-
}
|
|
683
660
|
if (command === 'agent-register') {
|
|
684
661
|
const now = new Date().toISOString();
|
|
685
662
|
const agent = { id: arg(args, '--agent-id', randomUUID()), name: arg(args, '--name'), responsibility: arg(args, '--responsibility'), role: arg(args, '--role'), capabilities: arg(args, '--capabilities').split(',').filter(Boolean), boundaries: arg(args, '--boundaries').split('|').filter(Boolean), endpoint: { kind: arg(args, '--endpoint-kind', 'adapter'), target: arg(args, '--endpoint') }, runtime: { adapter: arg(args, '--runtime', arg(args, '--adapter', 'shell')), provider: arg(args, '--provider') || undefined, model: arg(args, '--model') || undefined }, availability: arg(args, '--availability', 'available'), routing: { labels: arg(args, '--labels').split(',').filter(Boolean).reduce((out, item) => { const [key, value] = item.split('=', 2); if (key && value)
|
|
@@ -729,20 +706,6 @@ async function main() {
|
|
|
729
706
|
console.log(JSON.stringify(candidates, null, 2));
|
|
730
707
|
return;
|
|
731
708
|
}
|
|
732
|
-
if (command === 'record') {
|
|
733
|
-
const now = new Date().toISOString();
|
|
734
|
-
const id = arg(args, '--task-id', randomUUID());
|
|
735
|
-
const summary = arg(args, '--summary');
|
|
736
|
-
if (!summary)
|
|
737
|
-
throw new Error('record requires --summary');
|
|
738
|
-
const commit = arg(args, '--commit');
|
|
739
|
-
const task = { jobId: job.id, id, projectName: arg(args, '--project') || undefined, agentName: arg(args, '--agent', 'orchestrator'), parentTaskId: arg(args, '--parent') || null, dependencies: arg(args, '--deps').split(',').filter(Boolean), capabilities: [], adapter: 'orchestrator', input: arg(args, '--input', summary), continuation: null, continuationDeliveredAt: null, state: 'SUCCEEDED', attempt: 1, maxAttempts: 1, leaseOwner: null, leaseExpiresAt: null, executionId: commit || null, threadId: null, result: { summary, data: commit ? { commit } : undefined }, error: null, timeoutMs: 0, createdAt: now, updatedAt: now };
|
|
740
|
-
await store.appendTask(task);
|
|
741
|
-
job.tasks.push(task);
|
|
742
|
-
await saveLocal(localFile, job);
|
|
743
|
-
console.log(`Recorded ${id}`);
|
|
744
|
-
return;
|
|
745
|
-
}
|
|
746
709
|
if (command === 'serve') {
|
|
747
710
|
await startService(args);
|
|
748
711
|
return;
|
|
@@ -24,7 +24,8 @@ export function dashboardJobSetState(store) {
|
|
|
24
24
|
if (state === 'CANCELLED') {
|
|
25
25
|
if (job.state !== 'CANCELLED')
|
|
26
26
|
await applyJobCancelAction(store, job);
|
|
27
|
-
|
|
27
|
+
if (!job.localMerge?.sourceRef)
|
|
28
|
+
await store.closePullRequest?.();
|
|
28
29
|
return await store.load(job.id);
|
|
29
30
|
}
|
|
30
31
|
if (job.state === state)
|
package/dist/dashboard-html.js
CHANGED
|
@@ -345,7 +345,7 @@ function cadence(job){const schedule=job?.schedule;if(!schedule?.cron)return '
|
|
|
345
345
|
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;}
|
|
346
346
|
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;}
|
|
347
347
|
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;}
|
|
348
|
-
function prField(job){const box=element('div','detail-group');box.append(element('p','detail-label',text('GitHub PR')));const line=element('p','detail-value');const prNumber=Number(job.
|
|
348
|
+
function prField(job){const box=element('div','detail-group');box.append(element('p','detail-label',text('GitHub PR')));const line=element('p','detail-value');const prNumber=Number(job.prNumber||0);const url=safePrUrl(job.repository,prNumber);if(url){const link=document.createElement('a');link.href=url;link.target='_blank';link.rel='noopener noreferrer';link.textContent='PR #'+prNumber;line.append(link);}else line.append(text('unavailable'));box.append(line);return box;}
|
|
349
349
|
function chatField(task){const box=element('div','detail-group');box.append(element('p','detail-label',text('Chat')));const line=element('p','detail-value');if(task.adapter==='chatgpt'&&typeof task.threadId==='string'&&task.threadId){const link=document.createElement('a');link.href='https://chatgpt.com/c/'+encodeURIComponent(task.threadId);link.target='_blank';link.rel='noopener noreferrer';link.textContent='Open chat';line.append(link);}else line.append(text('—'));box.append(line);return box;}
|
|
350
350
|
function taskCommentField(task){const box=element('div','detail-group');box.append(element('p','detail-label',text('PR comment')));const line=element('p','detail-value');if(typeof task.taskUrl==='string'&&task.taskUrl.startsWith('https://github.com/')){const link=document.createElement('a');link.className='task-link';link.href=task.taskUrl;link.target='_blank';link.rel='noopener noreferrer';link.textContent='Open task comment';line.append(link);}else line.append(text('—'));box.append(line);return box;}
|
|
351
351
|
function parseAgentGraphSource(source){if(typeof source!=='string'||!source.trim())return null;const agents=new Map(),edges=[];for(const raw of source.split(/\\r?\\n/)){const line=raw.trim();if(!line||line.startsWith('%%')||line.startsWith('flowchart '))continue;let match=line.match(/^([A-Za-z_][A-Za-z0-9_.-]*)(?:\\[agent:([A-Za-z_][A-Za-z0-9_.-]*)\\])?\\s*-->\\s*([A-Za-z_][A-Za-z0-9_.-]*)(?:\\[agent:([A-Za-z_][A-Za-z0-9_.-]*)\\])?$/);if(match){agents.set(match[1],match[2]||agents.get(match[1])||match[1]);agents.set(match[3],match[4]||agents.get(match[3])||match[3]);edges.push([match[1],match[3]]);continue;}match=line.match(/^([A-Za-z_][A-Za-z0-9_.-]*)\\[agent:([A-Za-z_][A-Za-z0-9_.-]*)\\]$/);if(match){agents.set(match[1],match[2]);continue;}return null;}const incoming=new Map([...agents.keys()].map(id=>[id,0]));for(const [,to] of edges)incoming.set(to,(incoming.get(to)||0)+1);const remaining=new Map(incoming),waves=[];let ready=[...remaining].filter(([,degree])=>degree===0).map(([id])=>id).sort();while(ready.length){waves.push(ready);const next=[];for(const id of ready)for(const [from,to] of edges)if(from===id){const degree=(remaining.get(to)||0)-1;remaining.set(to,degree);if(degree===0)next.push(to);}ready=[...new Set(next)].sort();}return {agents,waves};}
|
|
@@ -384,14 +384,14 @@ const DEFAULT_CHAT_SHORTCUTS=[
|
|
|
384
384
|
].join('\\n');
|
|
385
385
|
function chatShortcutText(){return localStorage.getItem(CHAT_SHORTCUTS_KEY)||DEFAULT_CHAT_SHORTCUTS;}
|
|
386
386
|
function parseChatShortcuts(source){return String(source||'').split(/\\r?\\n/).map(line=>line.trim()).filter(Boolean).map(line=>{const split=line.indexOf(':');return split>0?{name:line.slice(0,split).trim(),prompt:line.slice(split+1).trim()}:null;}).filter(item=>item&&item.name&&item.prompt);}
|
|
387
|
-
function jobChatRef(job){const pr=Number(job?.
|
|
387
|
+
function jobChatRef(job){const pr=Number(job?.prNumber||0);return repoName(job?.repository)+"/job:"+value(job?.id)+(pr>0?"/pr#"+pr:"")+": "+value(job?.title||job?.id);}
|
|
388
388
|
function chatShortcutUrl(prompt){return 'https://chatgpt.com/?prompt='+encodeURIComponent(prompt);}
|
|
389
389
|
const CHAT_SHORTCUT_TARGET='agents-relay-shortcut';
|
|
390
390
|
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;}
|
|
391
|
-
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)+'/'+(Number(job.
|
|
391
|
+
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)+'/'+(Number(job.prNumber||0)>0?'PR#'+value(job.prNumber):'Job '+shortManagedId(job.id,job.repository,'j'))+': '+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.executionMode==='autonomous')states.append(element('span','badge flag-autonomous',text('AUTONOMOUS')));if(job.state==='ACTIVE'&&job.reviewEnabled!==false)states.append(element('span','badge flag-review',text('REVIEW REQUIRED')));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));}}
|
|
392
392
|
function taskTagDisplay(job,task){const display=task.tag&&job?.tag?.values?.[task.tag];return typeof display==='string'&&display?display:null;}
|
|
393
393
|
function taskSubtreeComplete(task,children){return ['SUCCEEDED','CANCELLED'].includes(task.state)&&(children.get(task.id)||[]).every(child=>taskSubtreeComplete(child,children));}
|
|
394
|
-
function taskNode(task,children,depth){const li=element('li');const nested=children.get(task.id)||[];const line=element('div','task-line');if(nested.length){const key=(selectedJob||'')+':'+task.id;const collapsed=collapsedTasks.has(key)||(!expandedTasks.has(key)&&taskSubtreeComplete(task,children));const fold=document.createElement('button');fold.type='button';fold.className='task-fold';fold.setAttribute('aria-expanded',String(!collapsed));fold.setAttribute('aria-label',(collapsed?'Expand ':'Collapse ')+task.id);fold.textContent=collapsed?'▸':'▾';line.append(fold);const ul=element('ul','tree');ul.hidden=collapsed;for(const child of nested)ul.append(taskNode(child,children,depth+1));fold.addEventListener('click',()=>{const next=!ul.hidden;ul.hidden=next;fold.setAttribute('aria-expanded',String(!next));fold.setAttribute('aria-label',(next?'Expand ':'Collapse ')+task.id);fold.textContent=next?'▸':'▾';if(next){collapsedTasks.add(key);expandedTasks.delete(key);}else{collapsedTasks.delete(key);expandedTasks.add(key);}});li.append(line,ul);}else li.append(line);const row=document.createElement('button');row.type='button';row.className='task-row';row.setAttribute('aria-current',String(task.id===selectedTaskId));row.title=task.id;row.addEventListener('click',()=>{const deselect=selectedTaskId===task.id;selectedTaskId=deselect?null:task.id;eventTaskId=deselect?null:task.id;rememberSelection();clearEvents();renderAll();void loadEventHistory();});const
|
|
394
|
+
function taskNode(task,children,depth){const li=element('li');const nested=children.get(task.id)||[];const line=element('div','task-line');if(nested.length){const key=(selectedJob||'')+':'+task.id;const collapsed=collapsedTasks.has(key)||(!expandedTasks.has(key)&&taskSubtreeComplete(task,children));const fold=document.createElement('button');fold.type='button';fold.className='task-fold';fold.setAttribute('aria-expanded',String(!collapsed));fold.setAttribute('aria-label',(collapsed?'Expand ':'Collapse ')+task.id);fold.textContent=collapsed?'▸':'▾';line.append(fold);const ul=element('ul','tree');ul.hidden=collapsed;for(const child of nested)ul.append(taskNode(child,children,depth+1));fold.addEventListener('click',()=>{const next=!ul.hidden;ul.hidden=next;fold.setAttribute('aria-expanded',String(!next));fold.setAttribute('aria-label',(next?'Expand ':'Collapse ')+task.id);fold.textContent=next?'▸':'▾';if(next){collapsedTasks.add(key);expandedTasks.delete(key);}else{collapsedTasks.delete(key);expandedTasks.add(key);}});li.append(line,ul);}else li.append(line);const row=document.createElement('button');row.type='button';row.className='task-row';row.setAttribute('aria-current',String(task.id===selectedTaskId));row.title=task.id;row.addEventListener('click',()=>{const deselect=selectedTaskId===task.id;selectedTaskId=deselect?null:task.id;eventTaskId=deselect?null:task.id;rememberSelection();clearEvents();renderAll();void loadEventHistory();});const label=element('span','task-id',text(task.title||task.input?.split(/\\r?\\n/).find(line=>line.trim())?.trim()||shortManagedId(task.id,selected()?.repository,'t')));label.style.paddingLeft=Math.min(depth,4)*6+'px';row.append(label,element('span','badge '+value(task.state),text(task.state)));const tag=taskTagDisplay(selected(),task);if(tag){const badge=element('span','task-tag',text(tag));badge.title=(selected()?.tag?.description||'Task tag')+': '+task.tag;row.append(badge);}line.append(row);return li;}
|
|
395
395
|
function buildTree(tasks){taskTree.replaceChildren();if(!Array.isArray(tasks)||!tasks.length){taskTree.append(element('li','empty',text('No tasks in durable state.')));return;}const work=tasks.filter(task=>task.kind!=='planner');if(!work.length){taskTree.append(element('li','empty',text('No work tasks in durable state.')));return;}const known=new Set(work.map(task=>task.id));const children=new Map();const roots=[];for(const task of work){if(!task.parentTaskId||!known.has(task.parentTaskId))roots.push(task);else{const siblings=children.get(task.parentTaskId)||[];siblings.push(task);children.set(task.parentTaskId,siblings);}}for(const task of roots)taskTree.append(taskNode(task,children,0));}
|
|
396
396
|
function attemptBudget(task){const attempt=Number(task?.attempt)||0;const max=Number(task?.maxAttempts)||0;if(max>=3&&max%3===0){const effectiveMax=Math.max(max,Math.ceil(attempt/3)*3);return attempt+' / 3×'+(effectiveMax/3);}return attempt+' / '+max;}
|
|
397
397
|
const JOB_STATES=['DRAFT','ACTIVE','COMPLETED','CANCELLED'];
|
|
@@ -401,7 +401,7 @@ const TASK_STATES=['DRAFT','QUEUED','RUNNING','BLOCKED','UNBLOCKED','SUCCEEDED',
|
|
|
401
401
|
async function setTaskState(task,state,select){const job=selected();if(!job||state===task.state)return;select.disabled=true;try{const params=new URLSearchParams({id:job.id,task:task.id,state,repo:job.repository,pr:String(job.prNumber)});const response=await fetch('/api/task-state?'+params.toString(),{method:'POST'});if(!response.ok)throw new Error(response.status+' '+response.statusText);const updated=await response.json();const index=jobs.findIndex(candidate=>jobSelectionKey(candidate)===jobSelectionKey(job));if(index>=0)jobs[index]=updated;else jobs.push(updated);renderAll();void loadEventHistory();}catch(error){select.title='Set task state: '+error.message;select.value=task.state;}finally{select.disabled=false;}}
|
|
402
402
|
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){const option=new Option(state,state);if(state==='UNBLOCKED'&&task.state!=='UNBLOCKED')option.disabled=true;select.append(option);}select.value=task.state;select.addEventListener('change',()=>void setTaskState(task,select.value,select));box.append(select);return box;}
|
|
403
403
|
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):'—');}
|
|
404
|
-
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('Task',task.id),field('Kind',task.kind||'work'),taskStateField(task),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'),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));taskDetailRoot.append(groups);}
|
|
404
|
+
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),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'),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));taskDetailRoot.append(groups);}
|
|
405
405
|
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();}
|
|
406
406
|
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('Execution mode',job.executionMode||'fixed'),field('Cadence',cadence(job)),field('Review',reviewStatus(job)),field('Creator thread',job.creatorThreadId||'—'),field('Implementation thread',job.implementationThreadId||'—'),field('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();}
|
|
407
407
|
function renderAll(){jobs.sort(compareJobs);syncFilters();renderJobList();renderSelectedJob();syncMobileJobsView();}
|
package/dist/dashboard.js
CHANGED
|
@@ -308,14 +308,10 @@ export function serveDashboard(store, port = 8787, bus, refreshMs = 300000, jobI
|
|
|
308
308
|
result = await resolvedActions.taskCreate(requestedJobId, body.task, body.idempotencyKey);
|
|
309
309
|
else if (body.action === 'update' && isTaskId(requestedTaskId) && body.task && body.expectedUpdatedAt && resolvedActions?.taskUpdate)
|
|
310
310
|
result = await resolvedActions.taskUpdate(requestedJobId, requestedTaskId, body.task, body.expectedUpdatedAt);
|
|
311
|
-
else if (body.action === 'start' && isTaskId(requestedTaskId) && resolvedActions?.taskStart)
|
|
312
|
-
result = await resolvedActions.taskStart(requestedJobId, requestedTaskId);
|
|
313
311
|
else if (body.action === 'retry' && isTaskId(requestedTaskId) && resolvedActions?.taskRetry)
|
|
314
312
|
result = await resolvedActions.taskRetry(requestedJobId, requestedTaskId);
|
|
315
313
|
else if (body.action === 'cancel' && isTaskId(requestedTaskId) && resolvedActions?.taskCancel)
|
|
316
314
|
result = await resolvedActions.taskCancel(requestedJobId, requestedTaskId);
|
|
317
|
-
else if (body.action === 'complete' && isTaskId(requestedTaskId) && typeof body.summary === 'string' && body.summary.trim() && resolvedActions?.taskComplete)
|
|
318
|
-
result = await resolvedActions.taskComplete(requestedJobId, requestedTaskId, body.summary);
|
|
319
315
|
else
|
|
320
316
|
throw new Error('Task command unavailable');
|
|
321
317
|
if (!res.headersSent)
|
|
@@ -3,7 +3,7 @@ export function orderGitHubBindingCandidates(items) {
|
|
|
3
3
|
return [...items].sort((a, b) => score(a.job) - score(b.job) || a.job.createdAt.localeCompare(b.job.createdAt));
|
|
4
4
|
}
|
|
5
5
|
function score(job) {
|
|
6
|
-
const bound = (job.
|
|
6
|
+
const bound = (job.prNumber) > 0;
|
|
7
7
|
const terminal = ['COMPLETED', 'CANCELLED'].includes(job.state);
|
|
8
8
|
if (!terminal && !bound)
|
|
9
9
|
return 0;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { deriveTaskTitle } from './task-title.js';
|
|
2
|
+
export function normalizeLegacyJobPrNumber(input) {
|
|
3
|
+
const record = input;
|
|
4
|
+
const legacyKey = 'external' + 'PrNumber';
|
|
5
|
+
const legacy = record[legacyKey];
|
|
6
|
+
if ((!Number.isInteger(input.prNumber) || input.prNumber <= 0) && Number.isInteger(legacy) && Number(legacy) > 0)
|
|
7
|
+
input.prNumber = Number(legacy);
|
|
8
|
+
delete record[legacyKey];
|
|
9
|
+
for (const task of input.tasks ?? []) {
|
|
10
|
+
if (!task.title?.trim())
|
|
11
|
+
task.title = deriveTaskTitle(task.input, task.agentName ? `${task.agentName} task` : 'Task');
|
|
12
|
+
}
|
|
13
|
+
return input;
|
|
14
|
+
}
|
|
15
|
+
export function migrateLegacyJobPrNumbers(db) {
|
|
16
|
+
const rows = db.prepare('SELECT job_id,json FROM job_snapshot').all();
|
|
17
|
+
const update = db.prepare('UPDATE job_snapshot SET json=? WHERE job_id=?');
|
|
18
|
+
for (const row of rows) {
|
|
19
|
+
const normalized = normalizeLegacyJobPrNumber(JSON.parse(row.json));
|
|
20
|
+
const json = JSON.stringify(normalized);
|
|
21
|
+
if (json !== row.json)
|
|
22
|
+
update.run(json, row.job_id);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -8,12 +8,20 @@ export function generatedEntityPrefix(repository, kind) {
|
|
|
8
8
|
return `${kind === 'job' ? 'j' : 't'}-${owner.length}-${owner}-${repo}-`;
|
|
9
9
|
}
|
|
10
10
|
export function isGeneratedEntityId(id, repository, kind) {
|
|
11
|
+
if (kind === 'task') {
|
|
12
|
+
if (/^[1-9][0-9]*$/.test(id))
|
|
13
|
+
return true;
|
|
14
|
+
const legacyPrefix = generatedEntityPrefix(repository, kind);
|
|
15
|
+
return id.startsWith(legacyPrefix) && /^[0-9]+$/.test(id.slice(legacyPrefix.length));
|
|
16
|
+
}
|
|
11
17
|
const prefix = generatedEntityPrefix(repository, kind);
|
|
12
18
|
return id.startsWith(prefix) && /^[0-9]+$/.test(id.slice(prefix.length));
|
|
13
19
|
}
|
|
14
|
-
/**
|
|
20
|
+
/** Jobs remain repository-scoped. Task IDs are one Relay-global monotonically increasing integer sequence. Allocation is serialized by SQLite. */
|
|
15
21
|
export function allocateEntityId(directory, repository, kind, requestKey, occupied = new Set()) {
|
|
16
|
-
const prefix = generatedEntityPrefix(repository, kind);
|
|
22
|
+
const prefix = kind === 'job' ? generatedEntityPrefix(repository, kind) : '';
|
|
23
|
+
const scope = kind === 'task' ? '*' : repository;
|
|
24
|
+
const scopedRequestKey = requestKey && kind === 'task' ? `${repository}:${requestKey}` : requestKey;
|
|
17
25
|
if (requestKey !== undefined && (!requestKey || requestKey.length > 512))
|
|
18
26
|
throw new Error('Invalid idempotency key');
|
|
19
27
|
mkdirSync(directory, { recursive: true });
|
|
@@ -23,22 +31,21 @@ export function allocateEntityId(directory, repository, kind, requestKey, occupi
|
|
|
23
31
|
db.exec('CREATE TABLE IF NOT EXISTS sequences (repository TEXT NOT NULL, kind TEXT NOT NULL, value INTEGER NOT NULL, PRIMARY KEY(repository,kind)); CREATE TABLE IF NOT EXISTS idempotency (repository TEXT NOT NULL, kind TEXT NOT NULL, request_key TEXT NOT NULL, entity_id TEXT NOT NULL, PRIMARY KEY(repository,kind,request_key));');
|
|
24
32
|
db.exec('BEGIN IMMEDIATE');
|
|
25
33
|
try {
|
|
26
|
-
if (
|
|
27
|
-
const row = db.prepare('SELECT entity_id FROM idempotency WHERE repository=? AND kind=? AND request_key=?').get(
|
|
34
|
+
if (scopedRequestKey) {
|
|
35
|
+
const row = db.prepare('SELECT entity_id FROM idempotency WHERE repository=? AND kind=? AND request_key=?').get(scope, kind, scopedRequestKey);
|
|
28
36
|
if (row) {
|
|
29
37
|
db.exec('COMMIT');
|
|
30
38
|
return row.entity_id;
|
|
31
39
|
}
|
|
32
40
|
}
|
|
33
|
-
const old = db.prepare('SELECT value FROM sequences WHERE repository=? AND kind=?').get(
|
|
41
|
+
const old = db.prepare('SELECT value FROM sequences WHERE repository=? AND kind=?').get(scope, kind);
|
|
34
42
|
let next = (old?.value ?? 0) + 1;
|
|
35
43
|
while (occupied.has(prefix + next))
|
|
36
44
|
next++;
|
|
37
|
-
db.prepare('INSERT INTO sequences VALUES (?,?,?) ON CONFLICT(repository,kind) DO UPDATE SET value=excluded.value').run(
|
|
38
|
-
// Length prefix keeps repository names unambiguous while satisfying dashboard ID validation.
|
|
45
|
+
db.prepare('INSERT INTO sequences VALUES (?,?,?) ON CONFLICT(repository,kind) DO UPDATE SET value=excluded.value').run(scope, kind, next);
|
|
39
46
|
const id = prefix + next;
|
|
40
|
-
if (
|
|
41
|
-
db.prepare('INSERT INTO idempotency VALUES (?,?,?,?)').run(
|
|
47
|
+
if (scopedRequestKey)
|
|
48
|
+
db.prepare('INSERT INTO idempotency VALUES (?,?,?,?)').run(scope, kind, scopedRequestKey, id);
|
|
42
49
|
db.exec('COMMIT');
|
|
43
50
|
return id;
|
|
44
51
|
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { eventFor } from './events.js';
|
|
2
|
+
import { orchestratorRescueFacts, prepareOrchestratorRepair } from './workflow/orchestrator-rescue.js';
|
|
3
|
+
import { reclaimStaleExecutionProcess } from './execution-process-tracker.js';
|
|
4
|
+
/** Durable, race-safe repair effect selected by workflow/orchestrator-rescue.mmd. */
|
|
5
|
+
export async function repairLegacyOrchestrators(jobId, now, ports) {
|
|
6
|
+
const { store } = ports;
|
|
7
|
+
const repairOne = async (taskId) => {
|
|
8
|
+
if (store.mutateJob) {
|
|
9
|
+
let outcome = { repaired: false, externallyCompleted: false, blocked: false, ageMs: 0 };
|
|
10
|
+
return await store.mutateJob(jobId, current => {
|
|
11
|
+
const task = current.tasks.find(item => item.id === taskId);
|
|
12
|
+
if (!task)
|
|
13
|
+
return current;
|
|
14
|
+
const repair = prepareOrchestratorRepair(task, now);
|
|
15
|
+
if (repair.repaired)
|
|
16
|
+
outcome = repair;
|
|
17
|
+
return current;
|
|
18
|
+
}).then(() => outcome);
|
|
19
|
+
}
|
|
20
|
+
const job = await store.load(jobId);
|
|
21
|
+
const task = job.tasks.find(item => item.id === taskId);
|
|
22
|
+
if (!task)
|
|
23
|
+
return null;
|
|
24
|
+
const repair = prepareOrchestratorRepair(task, now);
|
|
25
|
+
if (repair.repaired)
|
|
26
|
+
await store.saveTask(task);
|
|
27
|
+
return repair;
|
|
28
|
+
};
|
|
29
|
+
for (const stale of (await store.load(jobId)).tasks) {
|
|
30
|
+
const facts = orchestratorRescueFacts(stale, now);
|
|
31
|
+
if (facts.legacyActive !== true)
|
|
32
|
+
continue;
|
|
33
|
+
if (facts.verifiedExternalCompletion !== true && facts.unknownStartedOutcome !== true && facts.rescuable !== true)
|
|
34
|
+
continue;
|
|
35
|
+
if (stale.executionId && !await reclaimStaleExecutionProcess(store, stale, ports.owner))
|
|
36
|
+
continue;
|
|
37
|
+
const repair = await repairOne(stale.id);
|
|
38
|
+
if (!repair?.repaired)
|
|
39
|
+
continue;
|
|
40
|
+
const job = await store.load(jobId);
|
|
41
|
+
const task = job.tasks.find(item => item.id === stale.id);
|
|
42
|
+
const type = repair.externallyCompleted ? 'task.external_completion_applied'
|
|
43
|
+
: repair.blocked ? 'task.orchestrator_quarantined' : 'task.orchestrator_rescued';
|
|
44
|
+
const status = repair.externallyCompleted ? 'succeeded' : repair.blocked ? 'blocked' : 'queued';
|
|
45
|
+
await ports.emit(eventFor(job.id, task.id, task.parentTaskId, type, status, repair.externallyCompleted
|
|
46
|
+
? `Applied durable external completion for legacy orchestrator task ${task.id}`
|
|
47
|
+
: repair.blocked
|
|
48
|
+
? `Quarantined legacy RUNNING orchestrator task ${task.id}; external outcome must be verified before replacement work`
|
|
49
|
+
: `Recovered legacy orchestrator task ${task.id} through Execution Router`, 'orchestrator', { ageMs: repair.ageMs, adapter: repair.externallyCompleted || repair.blocked ? undefined : 'execution-router' }, { agent: 'relay', component: 'agents-relay' }));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -19,10 +19,9 @@ export function createJobIdRegistryResolver(directory, openStores, options = {})
|
|
|
19
19
|
}
|
|
20
20
|
// SQLite remains lifecycle authority. A positive PR binding only enables
|
|
21
21
|
// the external collaboration adapter for projection/merge side effects.
|
|
22
|
-
const
|
|
23
|
-
const githubEnabled = !options.noGitHub && externalPrNumber > 0 && options.client;
|
|
22
|
+
const githubEnabled = !options.noGitHub && job.prNumber > 0 && options.client;
|
|
24
23
|
const github = githubEnabled
|
|
25
|
-
? new GitHubStore(options.client, job.repository,
|
|
24
|
+
? new GitHubStore(options.client, job.repository, job.prNumber, options.trustedAuthors ?? new Set())
|
|
26
25
|
: new InMemoryStore();
|
|
27
26
|
return { job, relay, store: new LocalAuthorityDurableStore(relay, github, undefined, !githubEnabled) };
|
|
28
27
|
};
|
|
@@ -28,7 +28,7 @@ export async function reconcileLocalGitCompletion(store, finalState, headSha, no
|
|
|
28
28
|
throw new Error('Local Git merge requires an explicit project path');
|
|
29
29
|
const repoPath = await projectPathForJob(finalState);
|
|
30
30
|
const delivered = deliverLocalMerge(repoPath, { ...activeReceipt, sourceSha: activeReceipt.sourceSha ?? headSha ?? undefined,
|
|
31
|
-
correlationJobId: finalState.id, displayPrNumber: finalState.
|
|
31
|
+
correlationJobId: finalState.id, displayPrNumber: finalState.prNumber > 0 ? finalState.prNumber : undefined });
|
|
32
32
|
finalState.localMerge = { ...activeReceipt, ...delivered, status: 'succeeded', projectionStrategy: 'target_ref_push',
|
|
33
33
|
error: undefined, nextAttemptAt: undefined, updatedAt: new Date().toISOString() };
|
|
34
34
|
}
|