agents-relay 2.0.68 → 2.0.69
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -1
- package/dist/adapters.js +39 -3
- package/dist/blockers.js +17 -4
- package/dist/cli-blocker.js +12 -28
- package/dist/cli-help.js +11 -8
- package/dist/cli-job-create.js +17 -9
- package/dist/cli-local-merge-config.js +4 -2
- package/dist/cli-projection.js +23 -55
- package/dist/cli-review.js +7 -24
- package/dist/cli-support.js +57 -1
- package/dist/cli-worker-task.js +63 -0
- package/dist/cli.js +177 -125
- package/dist/dashboard-html.js +15 -12
- package/dist/dashboard.js +26 -1
- package/dist/e2e-injection.js +76 -0
- package/dist/github-binding-order.js +15 -0
- package/dist/github-client.js +7 -2
- package/dist/github-pr-archive.js +22 -0
- package/dist/github-webhook.js +31 -6
- package/dist/job-actions.js +29 -0
- package/dist/local-authority-store.js +10 -1
- package/dist/local-commit-events.js +2 -0
- package/dist/local-git-merge.js +17 -6
- package/dist/local-job-registry.js +10 -4
- package/dist/local-merge-delivery.js +3 -2
- package/dist/local-merge-projection.js +60 -0
- package/dist/managed-worker-input.js +13 -6
- package/dist/manual-plan.js +29 -7
- package/dist/planner-runtime.js +182 -41
- package/dist/planner.js +35 -2
- package/dist/pool.js +22 -6
- package/dist/reconciler-cancellation.js +21 -0
- package/dist/reconciler-review.js +74 -18
- package/dist/reconciler-runtime.js +69 -34
- package/dist/reconciler.js +92 -26
- package/dist/relayd-control.js +184 -0
- package/dist/relayd-dashboard-actions.js +108 -0
- package/dist/relayd-utilities.js +107 -0
- package/dist/relayd.js +216 -206
- package/dist/review-generation-fence.js +12 -0
- package/dist/runtime-status-events.js +21 -0
- package/dist/runtime-status.js +13 -0
- package/dist/task-actions.js +59 -1
- package/dist/troubleshooter.js +21 -2
- package/dist/unblock.js +47 -15
- package/dist/worker-rejection.js +11 -0
- package/dist/workflow-effect-failures.js +30 -0
- package/dist/workspace.js +95 -6
- package/package.json +4 -3
- package/skills/agents-relay/agents/planner.agent.md +3 -0
- package/skills/agents-relay/contracts/orchestrator-workflows.md +4 -7
- package/workflow/review-lifecycle.mmd +3 -0
package/dist/cli.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
|
-
import { openLocalManagedStore } from './local-managed-store.js';
|
|
5
4
|
import { configureLocalGitMerge } from './cli-local-merge-config.js';
|
|
6
|
-
import {
|
|
5
|
+
import { noGitHubMode } from './no-github.js';
|
|
7
6
|
import { discoverLocalRelayStoreSnapshots } from './relay-store.js';
|
|
8
7
|
import { realpathSync } from 'node:fs';
|
|
9
8
|
import { once } from 'node:events';
|
|
@@ -26,83 +25,24 @@ import { applyJobActivateAction, applyJobCancelAction } from './job-actions.js';
|
|
|
26
25
|
import { resolveExecutionContext } from './execution-context.js';
|
|
27
26
|
import { startServiceExternalActionFlusher } from './service-external-actions.js';
|
|
28
27
|
import { handleServicePublication } from './service-publication.js';
|
|
29
|
-
import { defaultWorkspaceRoot, resolveManagedProjectPath, resolveRepositoryProjectPath } from './workspace.js';
|
|
28
|
+
import { defaultWorkspaceRoot, prepareLocalJobExecutionWorktree, resolveManagedProjectPath, resolveRepositoryProjectPath } from './workspace.js';
|
|
30
29
|
import { runBlockerCommand } from './cli-blocker.js';
|
|
31
30
|
import { runEscalationCommand } from './cli-escalation.js';
|
|
32
31
|
import { runProjectionCommand } from './cli-projection.js';
|
|
33
32
|
import { helpText, usage } from './cli-help.js';
|
|
34
33
|
import { parseAgentGraph } from './agent-graph.js';
|
|
35
|
-
import {
|
|
36
|
-
import { createJobEntity
|
|
34
|
+
import { runTaskUnblockCommand } from './cli-unblock.js';
|
|
35
|
+
import { createJobEntity } from './workflow/entity-creation.js';
|
|
37
36
|
import { runCliEntrypoint } from './cli-shutdown.js';
|
|
38
37
|
import { shutdownOwnedProcesses } from './owned-process.js';
|
|
39
38
|
export { helpText } from './cli-help.js';
|
|
40
39
|
import { runReviewCommand } from './cli-review.js';
|
|
41
40
|
import { normalizeThreadId, runtimeReviewer } from './reviewer-thread.js';
|
|
42
41
|
export { runtimeReviewer } from './reviewer-thread.js';
|
|
43
|
-
import { SERVICE_DEFAULTS, ServiceEventBus, arg, hasArg, textOption, integerArg, repository,
|
|
42
|
+
import { SERVICE_DEFAULTS, ServiceEventBus, arg, hasArg, textOption, integerArg, repository, storeFor, saveLocal, eventBus, publishWake, priority, storySize, executionMode, chatGptTabClosePolicy, requestedExecutionMode, continuation, scheduleFromArgs, jobTagFromArgs, requestManagedReconciliation, relayControlJob, relayControlCommand, relayJobList, relayJobStateCommand, relayTaskCommand, withEventBus } from './cli-support.js';
|
|
44
43
|
export { SERVICE_DEFAULTS, eventBus, executionMode, chatGptTabClosePolicy } from './cli-support.js';
|
|
45
|
-
import {
|
|
44
|
+
import { workerTaskFromArgs } from './cli-worker-task.js';
|
|
46
45
|
export { ensureManagedGitHubJob } from './cli-job-create.js';
|
|
47
|
-
async function workerTaskFromArgs(job, args, now) {
|
|
48
|
-
const model = arg(args, '--model');
|
|
49
|
-
const adapterValue = arg(args, '--adapter');
|
|
50
|
-
const adapter = (adapterValue || 'codex');
|
|
51
|
-
const outputKind = arg(args, '--output');
|
|
52
|
-
const outputPath = arg(args, '--output-path');
|
|
53
|
-
const input = await textOption(args, '--input', '--input-file');
|
|
54
|
-
const agentGraph = hasArg(args, '--agent-graph') || hasArg(args, '--agent-graph-file') ? await textOption(args, '--agent-graph', '--agent-graph-file') : undefined;
|
|
55
|
-
if (agentGraph)
|
|
56
|
-
parseAgentGraph(agentGraph);
|
|
57
|
-
if (!input.trim())
|
|
58
|
-
throw new Error('task create requires --input-file PATH|- or --input MARKDOWN');
|
|
59
|
-
if (!['codex', 'chatgpt', 'orchestrator'].includes(adapter))
|
|
60
|
-
throw new Error('task create --adapter must be codex, chatgpt, or orchestrator');
|
|
61
|
-
if (outputKind !== 'task-pr' && outputKind !== 'file')
|
|
62
|
-
throw new Error('task create requires --output task-pr|file');
|
|
63
|
-
if (outputKind === 'file' && !outputPath)
|
|
64
|
-
throw new Error('--output file requires --output-path');
|
|
65
|
-
if (adapter !== 'orchestrator' && ['--provider', '--profile', '--reasoning', '--cwd', '--chatgpt-project'].some(flag => hasArg(args, flag)) && !model)
|
|
66
|
-
throw new Error('task create explicit routing requires --model');
|
|
67
|
-
const routing = adapter === 'orchestrator' || !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 };
|
|
68
|
-
const workerRequirements = adapter === 'orchestrator' || routing ? undefined : { capabilities: [], runtime: adapterValue ? adapter : undefined };
|
|
69
|
-
const closePolicy = hasArg(args, '--chatgpt-tab-close-policy') ? chatGptTabClosePolicy(args) : undefined;
|
|
70
|
-
const size = storySize(args);
|
|
71
|
-
const dependencies = arg(args, '--deps').split(',').filter(Boolean);
|
|
72
|
-
const timeoutMs = adapter === 'orchestrator' ? 0 : hasArg(args, '--timeout') ? integerArg(args, '--timeout', taskOverallTimeoutMs(size, routing?.reasoning)) : routing ? taskOverallTimeoutMs(size, routing.reasoning) : 0;
|
|
73
|
-
return createTaskEntity(job, {
|
|
74
|
-
jobId: job.id,
|
|
75
|
-
id: arg(args, '--task-id', 'initial'),
|
|
76
|
-
priority: priority(args, job.priority ?? 'P2'),
|
|
77
|
-
storySize: size,
|
|
78
|
-
projectName: arg(args, '--project') || undefined,
|
|
79
|
-
agentName: arg(args, '--agent') || undefined,
|
|
80
|
-
agentGraph,
|
|
81
|
-
parentTaskId: arg(args, '--parent') || null,
|
|
82
|
-
dependencies,
|
|
83
|
-
capabilities: arg(args, '--capabilities').split(',').filter(Boolean),
|
|
84
|
-
workerRequirements,
|
|
85
|
-
adapter,
|
|
86
|
-
chatgptTabClosePolicy: closePolicy,
|
|
87
|
-
input: input || 'true',
|
|
88
|
-
attachments: arg(args, '--attachments').split(',').filter(Boolean),
|
|
89
|
-
output: outputKind === 'file' ? { kind: 'file', path: outputPath } : { kind: 'task_pr' },
|
|
90
|
-
routing,
|
|
91
|
-
continuation: continuation(args),
|
|
92
|
-
continuationDeliveredAt: null,
|
|
93
|
-
attempt: 0,
|
|
94
|
-
maxAttempts: Number(arg(args, '--max-attempts', '3')),
|
|
95
|
-
leaseOwner: null,
|
|
96
|
-
leaseExpiresAt: null,
|
|
97
|
-
executionId: null,
|
|
98
|
-
threadId: null,
|
|
99
|
-
result: null,
|
|
100
|
-
error: null,
|
|
101
|
-
timeoutMs,
|
|
102
|
-
createdAt: now,
|
|
103
|
-
updatedAt: now
|
|
104
|
-
}, { defer: hasArg(args, '--draft') });
|
|
105
|
-
}
|
|
106
46
|
async function runManagedTaskCommand(action, args) {
|
|
107
47
|
const jobId = arg(args, '--id');
|
|
108
48
|
if (!jobId)
|
|
@@ -133,16 +73,100 @@ async function runManagedTaskCommand(action, args) {
|
|
|
133
73
|
console.log(JSON.stringify(task, null, 2));
|
|
134
74
|
}
|
|
135
75
|
async function runTaskCommand(action, args) {
|
|
136
|
-
if (!['create', 'update', 'show', 'list', 'start', 'complete', 'retry', 'unblock', 'cancel'].includes(action))
|
|
76
|
+
if (!['create', 'update', 'show', 'list', 'start', 'complete', 'retry', 'recover', 'unblock', 'cancel'].includes(action))
|
|
137
77
|
throw new Error(usage);
|
|
138
|
-
const managed = !
|
|
139
|
-
if (managed
|
|
140
|
-
|
|
141
|
-
|
|
78
|
+
const managed = !arg(args, '--file') && Boolean(arg(args, '--repo') && arg(args, '--id'));
|
|
79
|
+
if (managed) {
|
|
80
|
+
const jobId = arg(args, '--id');
|
|
81
|
+
if (action === 'list') {
|
|
82
|
+
const job = await relayControlJob(args, jobId);
|
|
83
|
+
console.log(JSON.stringify(job.tasks, null, 2));
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
const taskId = arg(args, '--task-id', action === 'create' ? randomUUID() : '');
|
|
87
|
+
if (action === 'show') {
|
|
88
|
+
if (!taskId)
|
|
89
|
+
throw new Error('task show requires --task-id');
|
|
90
|
+
const job = await relayControlJob(args, jobId);
|
|
91
|
+
const task = job.tasks.find(item => item.id === taskId);
|
|
92
|
+
if (!task)
|
|
93
|
+
throw new Error(`Task ${taskId} not found`);
|
|
94
|
+
console.log(JSON.stringify(task, null, 2));
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
if (['create', 'start', 'complete', 'retry', 'cancel'].includes(action)) {
|
|
98
|
+
await runManagedTaskCommand(action, args);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
if (action === 'recover') {
|
|
102
|
+
if (!taskId)
|
|
103
|
+
throw new Error('task recover requires --task-id');
|
|
104
|
+
const recoveryTaskId = arg(args, '--recovery-task-id');
|
|
105
|
+
if (!recoveryTaskId)
|
|
106
|
+
throw new Error('task recover requires --recovery-task-id');
|
|
107
|
+
const job = await relayControlCommand(args, { command: 'task.recover', jobId, taskId, recoveryTaskId });
|
|
108
|
+
console.log(JSON.stringify(job.tasks.find(item => item.id === taskId), null, 2));
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (action === 'unblock') {
|
|
112
|
+
if (!taskId)
|
|
113
|
+
throw new Error('task unblock requires --task-id');
|
|
114
|
+
const reason = arg(args, '--reason');
|
|
115
|
+
if (!reason.trim())
|
|
116
|
+
throw new Error('task unblock requires --reason');
|
|
117
|
+
const job = await relayControlCommand(args, { command: 'task.unblock', jobId, taskId, reason });
|
|
118
|
+
console.log(JSON.stringify(job.tasks.find(item => item.id === taskId), null, 2));
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (action === 'update') {
|
|
122
|
+
const job = await relayControlJob(args, jobId);
|
|
123
|
+
const task = job.tasks.find(item => item.id === taskId);
|
|
124
|
+
if (!task)
|
|
125
|
+
throw new Error(`Task ${taskId} not found`);
|
|
126
|
+
const expectedUpdatedAt = task.updatedAt;
|
|
127
|
+
if (['RUNNING', 'UNBLOCKED', 'SUCCEEDED', 'CANCELLED'].includes(task.state))
|
|
128
|
+
throw new Error(`Task ${taskId} is ${task.state}; durable execution fields are immutable in this state`);
|
|
129
|
+
if (hasArg(args, '--adapter')) {
|
|
130
|
+
const adapter = arg(args, '--adapter');
|
|
131
|
+
if (!['codex', 'chatgpt', 'orchestrator'].includes(adapter))
|
|
132
|
+
throw new Error('--adapter must be codex, chatgpt, or orchestrator');
|
|
133
|
+
task.adapter = adapter;
|
|
134
|
+
}
|
|
135
|
+
if (hasArg(args, '--priority'))
|
|
136
|
+
task.priority = priority(args, task.priority ?? job.priority ?? 'P2');
|
|
137
|
+
if (hasArg(args, '--story-size')) {
|
|
138
|
+
task.storySize = storySize(args, task.storySize ?? 'M');
|
|
139
|
+
if (!hasArg(args, '--timeout'))
|
|
140
|
+
task.timeoutMs = taskOverallTimeoutMs(task.storySize, task.routing?.reasoning);
|
|
141
|
+
}
|
|
142
|
+
if (hasArg(args, '--input') || hasArg(args, '--input-file'))
|
|
143
|
+
task.input = await textOption(args, '--input', '--input-file');
|
|
144
|
+
if (hasArg(args, '--project'))
|
|
145
|
+
task.projectName = arg(args, '--project') || undefined;
|
|
146
|
+
if (hasArg(args, '--agent'))
|
|
147
|
+
task.agentName = arg(args, '--agent') || undefined;
|
|
148
|
+
if (hasArg(args, '--parent'))
|
|
149
|
+
task.parentTaskId = arg(args, '--parent') || null;
|
|
150
|
+
if (hasArg(args, '--deps'))
|
|
151
|
+
task.dependencies = arg(args, '--deps').split(',').filter(Boolean);
|
|
152
|
+
if (hasArg(args, '--capabilities'))
|
|
153
|
+
task.capabilities = arg(args, '--capabilities').split(',').filter(Boolean);
|
|
154
|
+
task.timeoutMs = integerArg(args, '--timeout', task.timeoutMs);
|
|
155
|
+
task.maxAttempts = integerArg(args, '--max-attempts', task.maxAttempts);
|
|
156
|
+
if (hasArg(args, '--draft') && hasArg(args, '--queue'))
|
|
157
|
+
throw new Error('--draft and --queue are mutually exclusive');
|
|
158
|
+
if (hasArg(args, '--draft') && task.state === 'QUEUED')
|
|
159
|
+
await applyTaskHoldAction(task);
|
|
160
|
+
if (hasArg(args, '--queue'))
|
|
161
|
+
await applyTaskQueueAction(job, task);
|
|
162
|
+
const updated = await relayTaskCommand(args, { jobId, taskId, action: 'update', task, expectedUpdatedAt });
|
|
163
|
+
console.log(JSON.stringify(updated.tasks.find(item => item.id === taskId), null, 2));
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
142
166
|
}
|
|
143
167
|
const loaded = await storeFor(args);
|
|
144
168
|
const { store, localFile } = loaded;
|
|
145
|
-
const job =
|
|
169
|
+
const job = loaded.job;
|
|
146
170
|
if (action === 'list') {
|
|
147
171
|
console.log(JSON.stringify(job.tasks, null, 2));
|
|
148
172
|
return;
|
|
@@ -196,11 +220,14 @@ async function runTaskCommand(action, args) {
|
|
|
196
220
|
console.log(JSON.stringify(task, null, 2));
|
|
197
221
|
return;
|
|
198
222
|
}
|
|
199
|
-
if (action === 'retry' || action === 'cancel') {
|
|
223
|
+
if (action === 'retry' || action === 'recover' || action === 'cancel') {
|
|
200
224
|
let outputJob = job;
|
|
201
225
|
let outputTask = task;
|
|
202
|
-
if (action === 'retry') {
|
|
203
|
-
const
|
|
226
|
+
if (action === 'retry' || action === 'recover') {
|
|
227
|
+
const recoveryTaskId = action === 'recover' ? arg(args, '--recovery-task-id') : '';
|
|
228
|
+
if (action === 'recover' && !recoveryTaskId)
|
|
229
|
+
throw new Error('task recover requires --recovery-task-id');
|
|
230
|
+
const result = await dispatchTaskAction(store, { id: action === 'recover' ? 'task.recover' : 'task.retry', jobId: job.id, taskId: id, ...(recoveryTaskId ? { recoveryTaskId } : {}) });
|
|
204
231
|
if (result.job.state === 'DRAFT') {
|
|
205
232
|
await applyJobActivateAction(result.job);
|
|
206
233
|
await store.saveJob(result.job);
|
|
@@ -338,13 +365,7 @@ async function runJobCommand(action, args) {
|
|
|
338
365
|
throw new Error('job create with --head requires --objective-file PATH|- or --objective MARKDOWN containing the complete standalone Job context');
|
|
339
366
|
}
|
|
340
367
|
if (action === 'list') {
|
|
341
|
-
|
|
342
|
-
const overviews = snapshots.flatMap(snapshot => snapshot.jobs.filter(job => job.repository === repo)
|
|
343
|
-
.map(job => ({
|
|
344
|
-
job, githubState: snapshot.facts?.state ?? 'UNKNOWN',
|
|
345
|
-
draft: false, mergedAt: null, closedAt: null, publication: snapshot.publications[job.id]?.latest ?? null, publicationFailure: snapshot.publications[job.id]?.failure ?? null,
|
|
346
|
-
})));
|
|
347
|
-
console.log(JSON.stringify(dashboardManagedJobs(overviews), null, 2));
|
|
368
|
+
console.log(JSON.stringify(await relayJobList(args), null, 2));
|
|
348
369
|
return;
|
|
349
370
|
}
|
|
350
371
|
const id = arg(args, '--id');
|
|
@@ -358,49 +379,39 @@ async function runJobCommand(action, args) {
|
|
|
358
379
|
const objective = createObjective ?? '';
|
|
359
380
|
if (hasArg(args, '--local-merge-source') !== hasArg(args, '--local-merge-target'))
|
|
360
381
|
throw new Error('Supply both --local-merge-source and --local-merge-target');
|
|
361
|
-
const
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
priority: priority(args),
|
|
370
|
-
executionMode: requestedExecutionMode(args),
|
|
371
|
-
schedule: scheduleFromArgs(args),
|
|
372
|
-
tag: jobTagFromArgs(args),
|
|
373
|
-
reviewEnabled: !hasArg(args, '--no-review'),
|
|
374
|
-
continuation: continuation(args),
|
|
375
|
-
...(hasArg(args, '--reviewer-thread-id') ? { reviewerThreadId: arg(args, '--reviewer-thread-id') } : {}),
|
|
376
|
-
...(hasArg(args, '--creator-thread-id') ? { creatorThreadId: arg(args, '--creator-thread-id') } : {}),
|
|
377
|
-
prNumber,
|
|
378
|
-
head: arg(args, '--head') || undefined,
|
|
379
|
-
base: arg(args, '--base', 'main'),
|
|
380
|
-
body: arg(args, '--body') || objective || undefined
|
|
381
|
-
});
|
|
382
|
-
console.log(JSON.stringify({ repository: repo, pr: result.pr.number, job: result.job.id, executionMode: result.job.executionMode, state: result.job.state }, null, 2));
|
|
382
|
+
const result = await relayControlCommand(args, { command: 'job.create', repository: repo, initialPlan: hasArg(args, '--initial-plan'), options: {
|
|
383
|
+
id, title: arg(args, '--title') || undefined, objective: objective || undefined, projectPath: arg(args, '--project-path') || undefined,
|
|
384
|
+
priority: priority(args), executionMode: requestedExecutionMode(args), schedule: scheduleFromArgs(args), tag: jobTagFromArgs(args), reviewEnabled: !hasArg(args, '--no-review'), continuation: continuation(args),
|
|
385
|
+
reviewerThreadId: hasArg(args, '--reviewer-thread-id') ? arg(args, '--reviewer-thread-id') : undefined, creatorThreadId: hasArg(args, '--creator-thread-id') ? arg(args, '--creator-thread-id') : undefined,
|
|
386
|
+
prNumber, head: arg(args, '--head') || undefined, base: arg(args, '--base', 'main'), body: arg(args, '--body') || objective || undefined,
|
|
387
|
+
localMergeSource: arg(args, '--local-merge-source') || undefined, localMergeTarget: arg(args, '--local-merge-target') || undefined,
|
|
388
|
+
} });
|
|
389
|
+
console.log(JSON.stringify(result, null, 2));
|
|
383
390
|
return;
|
|
384
391
|
}
|
|
385
|
-
const
|
|
386
|
-
const { store, job } = loaded;
|
|
392
|
+
const job = await relayControlJob(args, id);
|
|
387
393
|
if (action === 'unblock') {
|
|
388
|
-
|
|
394
|
+
const reason = arg(args, '--reason');
|
|
395
|
+
if (!reason.trim())
|
|
396
|
+
throw new Error('job unblock requires --reason');
|
|
397
|
+
const updated = await relayControlCommand(args, { command: 'job.unblock', jobId: id, reason });
|
|
398
|
+
console.log(JSON.stringify(updated, null, 2));
|
|
389
399
|
return;
|
|
390
400
|
}
|
|
391
401
|
if (action === 'show') {
|
|
392
402
|
console.log(JSON.stringify(job, null, 2));
|
|
393
403
|
return;
|
|
394
404
|
}
|
|
395
|
-
|
|
405
|
+
const boundPrNumber = job.externalPrNumber ?? (job.prNumber > 0 ? job.prNumber : 0);
|
|
406
|
+
if (job.repository !== repo || (prValue && boundPrNumber !== prNumber)) {
|
|
396
407
|
throw new Error('Local Job/PR binding mismatch');
|
|
397
408
|
}
|
|
398
409
|
if (action === 'merge') {
|
|
399
410
|
throw new Error('GitHub is audit-only. Configure local merge source/target and reconcile the local Job instead.');
|
|
400
411
|
}
|
|
401
412
|
if (action === 'cancel') {
|
|
402
|
-
await
|
|
403
|
-
console.log(JSON.stringify(
|
|
413
|
+
const updated = await relayJobStateCommand(args, job.id, 'CANCELLED');
|
|
414
|
+
console.log(JSON.stringify(updated, null, 2));
|
|
404
415
|
return;
|
|
405
416
|
}
|
|
406
417
|
if (hasArg(args, '--title'))
|
|
@@ -445,12 +456,13 @@ async function runJobCommand(action, args) {
|
|
|
445
456
|
throw new Error('--continue-kind and --continue-target must be supplied together');
|
|
446
457
|
job.continuation = next;
|
|
447
458
|
}
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
459
|
+
const patch = {};
|
|
460
|
+
for (const key of ['title', 'objective', 'projectPath', 'priority', 'executionMode', 'schedule', 'tag', 'autoMerge', 'reviewEnabled', 'reviewerThreadId', 'creatorThreadId', 'continuation', 'localMerge'])
|
|
461
|
+
patch[key] = job[key];
|
|
462
|
+
console.log(JSON.stringify(await relayControlCommand(args, { command: 'job.update', jobId: id, patch }), null, 2));
|
|
451
463
|
}
|
|
452
464
|
export function runtimePlanner(args) { const plannerCommand = arg([...args], '--planner-command'); const modelRuntime = arg([...args], '--codex', 'codex'); return plannerCommand ? new CommandObjectivePlanner(plannerCommand) : new AgentObjectivePlanner(modelRuntime); }
|
|
453
|
-
export function runtime(store, args, bus, usageRegistry = codexAndZaiUsageRegistry()) { const emit = (event) => { process.stderr.write(`${JSON.stringify(event)}\n`); }; const modelRuntime = arg(args, '--codex', 'codex'); const policy = chatGptTabClosePolicy(args); const planner = runtimePlanner(args); return new Reconciler(store, { noGitHub: noGitHubMode(args), owner: arg(args, '--owner', `cli-${process.pid}`), maxConcurrent: Number(arg(args, '--concurrency', '4')), leaseMs: Number(arg(args, '--lease-ms', '300000')), adapters: [new CodexAdapter(modelRuntime), new ChatGptAdapter({ tabClosePolicy: policy })], continuations: [new CodexThreadContinuation(modelRuntime), new CommandContinuation(), new WebhookContinuation()], planner, usageSnapshotProvider: () => usageRegistry.snapshot(), decisionEngine: executionDecisionEngine(), decisionMode: decisionMode(), reviewer: runtimeReviewer(args), eventBus: bus, emit, chatgptTabClosePolicy: policy, projectPathForJob: async (job) =>
|
|
465
|
+
export function runtime(store, args, bus, usageRegistry = codexAndZaiUsageRegistry()) { const emit = (event) => { process.stderr.write(`${JSON.stringify(event)}\n`); }; const modelRuntime = arg(args, '--codex', 'codex'); const policy = chatGptTabClosePolicy(args); const planner = runtimePlanner(args); return new Reconciler(store, { noGitHub: noGitHubMode(args), owner: arg(args, '--owner', `cli-${process.pid}`), maxConcurrent: Number(arg(args, '--concurrency', '4')), leaseMs: Number(arg(args, '--lease-ms', '300000')), adapters: [new CodexAdapter(modelRuntime), new ChatGptAdapter({ tabClosePolicy: policy })], continuations: [new CodexThreadContinuation(modelRuntime), new CommandContinuation(), new WebhookContinuation()], planner, usageSnapshotProvider: () => usageRegistry.snapshot(), decisionEngine: executionDecisionEngine(), decisionMode: decisionMode(), reviewer: runtimeReviewer(args), eventBus: bus, emit, chatgptTabClosePolicy: policy, projectPathForJob: async (job) => { const workspaceRoot = arg(args, '--workspace-root') || defaultWorkspaceRoot(); const canonical = await resolveManagedProjectPath(noGitHubMode(args) ? undefined : job.repository, arg(args, '--project-path'), workspaceRoot); return job.prNumber === 0 && job.localMerge?.sourceRef ? prepareLocalJobExecutionWorktree(canonical, job, workspaceRoot) : resolveRepositoryProjectPath(canonical, job.projectPath); } }); }
|
|
454
466
|
export function dashboardJobAction(store, reconciler, action) {
|
|
455
467
|
return async (jobId) => {
|
|
456
468
|
const job = await store.load(jobId);
|
|
@@ -490,7 +502,8 @@ export async function createService(args) {
|
|
|
490
502
|
const webhook = !noGitHubMode(args) && repo && webhookSecret ? {
|
|
491
503
|
secret: webhookSecret,
|
|
492
504
|
repositories: new Set([repo]),
|
|
493
|
-
pullRequest: loaded.job.prNumber,
|
|
505
|
+
pullRequest: loaded.job.externalPrNumber ?? loaded.job.prNumber,
|
|
506
|
+
jobId: loaded.job.id,
|
|
494
507
|
wake: async (event) => {
|
|
495
508
|
if (event.pullRequestFacts && loaded.store.applyPullRequestFacts) {
|
|
496
509
|
await loaded.store.applyPullRequestFacts({
|
|
@@ -606,19 +619,18 @@ async function main() {
|
|
|
606
619
|
return;
|
|
607
620
|
}
|
|
608
621
|
if (command === 'init') {
|
|
622
|
+
const file = arg(args, '--file');
|
|
609
623
|
const now = new Date().toISOString();
|
|
610
624
|
const initialSchedule = scheduleFromArgs(args);
|
|
611
|
-
const
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
file = arg(args, '--file');
|
|
618
|
-
if (!file)
|
|
619
|
-
throw new Error('init requires --repo/--pr or explicit --file demo mode');
|
|
620
|
-
store = new InMemoryStore(job);
|
|
625
|
+
const id = arg(args, '--id', randomUUID());
|
|
626
|
+
if (!file) {
|
|
627
|
+
const repo = repository(args);
|
|
628
|
+
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), executionMode: executionMode(args), schedule: initialSchedule, reviewEnabled: !hasArg(args, '--no-review'), continuation: continuation(args), prNumber: Number(arg(args, '--pr', '0')) || 0 } });
|
|
629
|
+
console.log(JSON.stringify(result, null, 2));
|
|
630
|
+
return;
|
|
621
631
|
}
|
|
632
|
+
const job = createJobEntity({ id, title: arg(args, '--title', 'Agents Relay job'), objective: arg(args, '--objective') || undefined, priority: priority(args), executionMode: executionMode(args), schedule: initialSchedule, 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: [] }, { activate: Boolean(initialSchedule) });
|
|
633
|
+
const store = new InMemoryStore(job);
|
|
622
634
|
await store.saveJob(job);
|
|
623
635
|
await saveLocal(file, job);
|
|
624
636
|
console.log(`Initialized ${job.id}`);
|
|
@@ -641,6 +653,46 @@ async function main() {
|
|
|
641
653
|
console.log(JSON.stringify(await response.json(), null, 2));
|
|
642
654
|
return;
|
|
643
655
|
}
|
|
656
|
+
if (!arg(args, '--file') && ['plan', 'reconcile', 'record', 'agent-register', 'agent-discover'].includes(command)) {
|
|
657
|
+
const jobId = arg(args, '--id');
|
|
658
|
+
if (!jobId)
|
|
659
|
+
throw new Error(`${command} requires --repo and --id`);
|
|
660
|
+
if (command === 'plan') {
|
|
661
|
+
const guidance = hasArg(args, '--prompt') || hasArg(args, '--prompt-file') ? await textOption(args, '--prompt', '--prompt-file') : undefined;
|
|
662
|
+
console.log(JSON.stringify(await relayControlCommand(args, { command: 'job.plan', jobId, ...(guidance?.trim() ? { guidance: guidance.trim() } : {}) }), null, 2));
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
if (command === 'reconcile') {
|
|
666
|
+
console.log(JSON.stringify(await relayControlCommand(args, { command: 'job.reconcile', jobId }), null, 2));
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
if (command === 'record') {
|
|
670
|
+
const now = new Date().toISOString();
|
|
671
|
+
const id = arg(args, '--task-id', randomUUID());
|
|
672
|
+
const summary = arg(args, '--summary');
|
|
673
|
+
if (!summary)
|
|
674
|
+
throw new Error('record requires --summary');
|
|
675
|
+
const commit = arg(args, '--commit');
|
|
676
|
+
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 };
|
|
677
|
+
const updated = await relayControlCommand(args, { command: 'record', jobId, task });
|
|
678
|
+
console.log(JSON.stringify(updated.tasks.find(item => item.id === id), null, 2));
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
if (command === 'agent-register') {
|
|
682
|
+
const now = new Date().toISOString();
|
|
683
|
+
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)
|
|
684
|
+
out[key] = value; return out; }, {}), trustDomains: arg(args, '--trust-domains').split(',').filter(Boolean), regions: arg(args, '--regions').split(',').filter(Boolean) }, evidence: { evaluations: [], outcomes: { succeeded: 0, failed: 0, timedOut: 0 }, lastObservedAt: null }, registeredAt: now, updatedAt: now };
|
|
685
|
+
if (!agent.name || !agent.responsibility || !agent.role || !agent.endpoint.target)
|
|
686
|
+
throw new Error('--name, --responsibility, --role, and --endpoint are required');
|
|
687
|
+
console.log(JSON.stringify(await relayControlCommand(args, { command: 'agent.register', jobId, agent }), null, 2));
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
if (command === 'agent-discover') {
|
|
691
|
+
const requirements = { capabilities: arg(args, '--capabilities').split(',').filter(Boolean), runtime: arg(args, '--runtime') || undefined, availability: arg(args, '--availability').split(',').filter(Boolean), trustDomain: arg(args, '--trust-domain') || undefined, taskKind: arg(args, '--task-kind') || undefined, explore: args.includes('--explore') };
|
|
692
|
+
console.log(JSON.stringify(await relayControlCommand(args, { command: 'agent.discover', jobId, requirements }), null, 2));
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
}
|
|
644
696
|
const loaded = await storeFor(args);
|
|
645
697
|
const { store, job, localFile } = loaded;
|
|
646
698
|
if (command === 'plan') {
|
package/dist/dashboard-html.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { createRequire } from 'node:module';
|
|
2
|
-
const
|
|
3
|
-
export const dashboardVersion = packageVersion;
|
|
2
|
+
export const dashboardVersion = createRequire(import.meta.url)('../package.json').version;
|
|
4
3
|
export const dashboardHtml = `<!doctype html>
|
|
5
4
|
<html lang="en" data-dashboard-contract="safe-dom,named-relay-events,snapshot-events,bounded-event-history">
|
|
6
5
|
<head>
|
|
@@ -87,7 +86,11 @@ export const dashboardHtml = `<!doctype html>
|
|
|
87
86
|
.agent-arrow { color:var(--muted); font-weight:800; }
|
|
88
87
|
.agent-graph-source { margin:.1rem 0 0; padding:.45rem; border-radius:.35rem; background:var(--surface); color:var(--muted); font:.7rem/1.45 ui-monospace,SFMono-Regular,Menlo,monospace; white-space:pre; overflow:auto; }
|
|
89
88
|
.event-list { display:grid; gap:.45rem; }
|
|
90
|
-
.event-item { padding
|
|
89
|
+
.event-item { padding:.34rem .42rem .42rem; border-left:3px solid transparent; border-bottom:1px solid var(--border); border-radius:.28rem; }
|
|
90
|
+
.event-item.event-error { border-left-color:var(--bad); background:#fff4f4; }
|
|
91
|
+
.event-item.event-error .event-kind,.event-item.event-error .event-message { color:var(--bad); }
|
|
92
|
+
.event-item.event-warn { border-left-color:var(--warn); background:#fff9eb; }
|
|
93
|
+
.event-item.event-warn .event-kind { color:var(--warn); }
|
|
91
94
|
.event-meta { display:flex; flex-wrap:wrap; gap:.4rem; color:var(--muted); font-size:.68rem; }
|
|
92
95
|
.event-kind { color:var(--accent); font-weight:800; }
|
|
93
96
|
.event-message { margin:.12rem 0 0; overflow-wrap:anywhere; }
|
|
@@ -176,7 +179,6 @@ export const dashboardHtml = `<!doctype html>
|
|
|
176
179
|
.tagline { display:none; }
|
|
177
180
|
}
|
|
178
181
|
@media (prefers-reduced-motion:reduce) { .skeleton { animation:none; } .usage-fill { transition:none; } }
|
|
179
|
-
|
|
180
182
|
.router-rules-workspace{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:16px;align-items:start;min-width:0}.router-rules-pane{min-width:0;max-width:100%;overflow:hidden}.rules-editor .rules-textarea{min-height:420px}.audit-log{display:flex;flex-direction:column;gap:8px;max-height:500px;overflow:auto;min-width:0}.audit-entry{border:1px solid var(--border);border-radius:10px;padding:10px;background:var(--surface)}.audit-entry-head{display:flex;justify-content:space-between;gap:8px;align-items:center}.audit-entry pre{white-space:pre-wrap;word-break:break-word;margin:8px 0 0;font-size:11px}.router-test-panel{margin-top:16px;border-top:1px solid var(--border);padding-top:12px}.router-test-results{margin-top:8px;display:flex;flex-direction:column;gap:6px;max-height:480px;overflow:auto;min-width:0}.router-test-results pre{white-space:pre;overflow:auto;max-width:100%;font-size:11px;line-height:1.45;background:var(--surface);border:1px solid var(--border);border-radius:10px;padding:12px}.router-test-row{display:grid;grid-template-columns:minmax(0,1fr) auto auto;gap:8px;padding:7px 0;border-bottom:1px solid var(--border)}@media(max-width:900px){.router-rules-workspace{grid-template-columns:1fr}.router-rules-pane{max-height:none}.router-test-results{max-height:420px}}@media(max-width:650px){#rules-view .panel-head .subtle{display:none}#rules-view .panel-body{max-height:none;overflow:visible;padding:.6rem}#rules-view .router-rules-workspace{gap:.9rem}#rules-view .rules-help{font-size:.74rem;line-height:1.4}#rules-view .rules-textarea{min-height:52vh;padding:.65rem;font-size:12px}#rules-view .rules-footer{align-items:stretch}#rules-view .rules-footer>.subtle{width:100%}#rules-view .rules-footer>div{width:100%;display:grid;grid-template-columns:1fr 1fr;gap:.5rem}#rules-view .rules-save{min-height:44px;width:100%;padding:.55rem .7rem}#rules-view .router-rules-pane>.panel-head{padding:.55rem 0}#rules-view .audit-log{max-height:none}#rules-view .audit-entry-head{align-items:flex-start}#rules-view .audit-entry-head .subtle{display:block;flex:0 0 auto}#rules-view .router-test-panel{min-width:0;max-width:100%;overflow:hidden}#rules-view .router-test-results{max-height:none;width:100%;max-width:100%;overflow-x:hidden}#rules-view .router-test-results>*{min-width:0;max-width:100%}#rules-view .router-test-results pre{width:100%;box-sizing:border-box;white-space:pre-wrap;overflow-wrap:anywhere;word-break:break-word;font-size:11px;padding:.65rem}#rules-view .router-test-row{grid-template-columns:1fr}}
|
|
181
183
|
</style>
|
|
182
184
|
</head>
|
|
@@ -263,7 +265,6 @@ export const dashboardHtml = `<!doctype html>
|
|
|
263
265
|
// link.textContent = 'PR #' + job.prNumber;
|
|
264
266
|
// while (eventFeed.children.length > MAX_EVENTS)
|
|
265
267
|
// function usageBar(label, window)
|
|
266
|
-
|
|
267
268
|
const MAX_EVENTS=60;
|
|
268
269
|
const DASHBOARD_VERSION=${JSON.stringify(dashboardVersion)};
|
|
269
270
|
const jobsTab=document.querySelector('#jobs-tab');
|
|
@@ -325,7 +326,6 @@ let eventTaskId=selectedTaskId;
|
|
|
325
326
|
const collapsedTasks=new Set();
|
|
326
327
|
const expandedTasks=new Set();
|
|
327
328
|
const displayedEventIds=new Set();
|
|
328
|
-
|
|
329
329
|
function value(input){return input===null||input===undefined||input===''?'—':String(input);}
|
|
330
330
|
function text(input){return document.createTextNode(value(input));}
|
|
331
331
|
function element(name,className,child){const node=document.createElement(name);if(className)node.className=className;if(child)node.append(child);return node;}
|
|
@@ -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 url=safePrUrl(job.repository,
|
|
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.externalPrNumber||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};}
|
|
@@ -388,7 +388,7 @@ function jobChatRef(job){return repoName(job?.repository)+'/pr#'+value(job?.prNu
|
|
|
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)+'/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.
|
|
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.externalPrNumber||job.prNumber||0)>0?'PR#'+value(job.externalPrNumber||job.prNumber):'Job '+value(job.id))+': '+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.externalPrNumber||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
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 id=element('span','task-id',text(task.id));id.style.paddingLeft=Math.min(depth,4)*6+'px';row.append(id,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;}
|
|
@@ -424,7 +424,8 @@ async function refreshStatus(){try{renderRateLimitStatus(await loadJSON('/api/st
|
|
|
424
424
|
async function refresh(force=false){jobsRefresh.disabled=true;jobsRefresh.textContent='Refreshing…';try{const discovered=await loadJSON('/api/jobs'+(force?'?refresh=1':''));jobs=Array.isArray(discovered)?discovered:[];void drawRuntimeSummary();jobs.sort(compareJobs);if(selectedJob&&!jobForSelection(selectedJob)){selectedJob=null;selectedTaskId=null;}if(!selectedJob){const match=location.hash.match(/^#\\/jobs\\/(.+)$/);const fromHash=match?decodeURIComponent(match[1]):null;const fromRoute=jobForSelection(fromHash);selectedJob=jobSelectionKey(fromRoute||jobs[0]);}rememberSelection();renderAll();void loadEventHistory();connection.textContent='';connection.setAttribute('aria-label','Live durable state connected');connection.className='status-pill ok';}catch(error){connection.textContent='';connection.setAttribute('aria-label','Durable state unavailable');connection.className='status-pill bad';jobList.replaceChildren(element('li','error-state',text('Could not load jobs: '+error.message)));}finally{jobsRefresh.disabled=false;jobsRefresh.textContent='Refresh';}}
|
|
425
425
|
function renderEventScope(){const task=eventTaskId&&selected()?.tasks?.find(candidate=>candidate.id===eventTaskId);eventsAllTasks.disabled=!task;eventsAllTasks.setAttribute('aria-pressed',String(!task));eventsScope.textContent=task?'Task '+task.id+' · newest first · 60 max':'Job-wide · newest first · 60 max';}
|
|
426
426
|
function eventLabel(type){return ({'task.process.launched':'Process launched','task.started':'Worker started','task.process.exited':'Sync process exited','task.process.async_exited':'Async process exited','task.completed':'Worker completed','task.failed':'Worker failed','task.blocked':'Worker blocked','review.started':'Review started','review.approved':'Review approved','review.changes_required':'Review changes required','review.blocked':'Review blocked'})[type]||type||'Event';}
|
|
427
|
-
function
|
|
427
|
+
function eventSeverity(event,level){const type=String(event?.type||'').toLowerCase(),eventLevel=String(event?.level||level||'').toLowerCase(),outcome=String(event?.outcome||event?.status||'').toLowerCase();if(eventLevel==='error'||outcome==='failed'||type.endsWith('.failed')||type==='task.failed')return 'error';if(eventLevel==='warn'||outcome==='blocked'||type.endsWith('.blocked')||type==='task.blocked'||type==='review.blocked')return 'warn';return 'normal';}
|
|
428
|
+
function addEvent(event,level){const current=selected();if(!event||current&&event.job_id&&event.job_id!==current.id||eventTaskId&&event.task_id!==eventTaskId)return;if(event.event_id&&displayedEventIds.has(event.event_id))return;if(event.event_id)displayedEventIds.add(event.event_id);const severity=eventSeverity(event,level);const item=element('li','event-item'+(severity==='error'?' event-error':severity==='warn'?' event-warn':''));const meta=element('div','event-meta');meta.append(element('span','event-kind',text(eventLabel(event.type))),element('span','event-type',text(event.type||'event')),element('span','',text(relativeTime(event.timestamp))),element('span','',text('task: '+value(event.task_id))));item.append(meta);const message=element('p','event-message',text(event.message||'Event received'));item.append(message);eventFeed.querySelector('.empty')?.remove();eventFeed.prepend(item);while(eventFeed.children.length>MAX_EVENTS)eventFeed.lastElementChild?.remove();}
|
|
428
429
|
function resetText(windowData){if(!Number.isFinite(windowData.resetsAt))return '';const at=new Date(windowData.resetsAt*1000);const minutes=Math.max(0,Math.ceil((at.getTime()-Date.now())/60000));const relative=minutes<60?minutes+'m':minutes<1440?Math.floor(minutes/60)+'h '+minutes%60+'m':Math.floor(minutes/1440)+'d '+Math.floor((minutes%1440)/60)+'h';return 'Resets '+at.toLocaleString()+' · in '+relative;}
|
|
429
430
|
function usageBar(label,windowData){const percent=Math.max(0,Math.min(100,100-windowData.usedPercent));const row=element('div','usage-row');const heading=element('div','usage-label');heading.append(element('span','',text(label)),element('span','',text(Math.round(percent)+'%')));const track=element('div','usage-track');track.setAttribute('role','progressbar');track.setAttribute('aria-label',label+' available');track.setAttribute('aria-valuemin','0');track.setAttribute('aria-valuemax','100');track.setAttribute('aria-valuenow',String(percent));const fill=element('div','usage-fill');fill.style.width=percent+'%';track.append(fill);row.append(heading,track);const reset=resetText(windowData);if(reset)row.append(element('div','subtle',text(reset)));return row;}
|
|
430
431
|
function usageProvider(provider){const card=element('section','usage-provider');card.append(element('h3','',text(provider.label)));const usage=provider.usage||{};if(!usage.available||!usage.fiveHour||!usage.weekly){card.append(element('p','empty',text(usage.reason||'Account usage telemetry is unavailable.')));return card;}const bars=element('div','usage-bars');bars.append(usageBar('5-hour remaining',usage.fiveHour),usageBar('Weekly remaining',usage.weekly));card.append(bars);return card;}
|
|
@@ -481,12 +482,14 @@ auditRefresh.addEventListener('click',()=>void drawAudit());
|
|
|
481
482
|
shortcutsSave.addEventListener('click',saveShortcuts);
|
|
482
483
|
window.addEventListener('keydown',event=>{if(event.key==='Tab'&&event.altKey){event.preventDefault();const views=['jobs','usage','graphs','rules','shortcuts'];const current=location.hash==='#/usage'?'usage':location.hash==='#/graphs'?'graphs':location.hash==='#/execution-router-rules'?'rules':location.hash==='#/shortcuts'?'shortcuts':'jobs';const next=views[(views.indexOf(current)+1)%views.length];location.hash=next==='usage'?'#/usage':next==='graphs'?'#/graphs':next==='rules'?'#/execution-router-rules':next==='shortcuts'?'#/shortcuts':(selectedJob?'#/jobs/'+encodeURIComponent(selectedJob):'#/jobs');showView(next);}});
|
|
483
484
|
function parseMessage(message){try{return JSON.parse(message.data);}catch{return null;}}
|
|
484
|
-
function
|
|
485
|
+
function isFreshEnough(incoming,current){const a=Date.parse(incoming||''),b=Date.parse(current||'');return !Number.isFinite(b)||!Number.isFinite(a)||a>=b;}
|
|
486
|
+
function applyStatePatch(patch){if(!patch?.jobId)return false;const job=jobs.find(candidate=>candidate.id===patch.jobId);if(!job)return false;if(patch.jobState&&isFreshEnough(patch.jobUpdatedAt,job.updatedAt)){job['state']=patch.jobState;if(patch.jobUpdatedAt)job.updatedAt=patch.jobUpdatedAt;}if(patch.taskId&&patch.taskState){const task=(job.tasks||[]).find(candidate=>candidate.id===patch.taskId);if(task&&isFreshEnough(patch.taskUpdatedAt,task.updatedAt)){task['state']=patch.taskState;if(patch.taskUpdatedAt)task.updatedAt=patch.taskUpdatedAt;}}return true;}
|
|
487
|
+
function upsertFreshJob(incoming){if(!incoming)return;const index=jobs.findIndex(job=>jobSelectionKey(job)===jobSelectionKey(incoming));if(index<0){jobs.push(incoming);return;}if(isFreshEnough(incoming.updatedAt,jobs[index].updatedAt))jobs[index]=incoming;}
|
|
485
488
|
const stream=new EventSource('/events');
|
|
486
489
|
stream.onopen=()=>{connection.textContent='';connection.setAttribute('aria-label','Event stream connected');connection.className='status-pill ok';};
|
|
487
490
|
stream.onerror=()=>{connection.textContent='';connection.setAttribute('aria-label','Event stream reconnecting');connection.className='status-pill warn';};
|
|
488
|
-
stream.onmessage=message=>{const payload=parseMessage(message);if(payload?.job){
|
|
489
|
-
stream.addEventListener('relay',message=>{const payload=parseMessage(message);if(payload?.job)
|
|
491
|
+
stream.onmessage=message=>{const payload=parseMessage(message);if(payload?.job){upsertFreshJob(payload.job);renderAll();void drawRuntimeSummary();}};
|
|
492
|
+
stream.addEventListener('relay',message=>{const payload=parseMessage(message);if(payload?.job)upsertFreshJob(payload.job);else if(payload?.statePatch)applyStatePatch(payload.statePatch);if(payload?.event)addEvent(payload.event);renderAll();void drawRuntimeSummary();});
|
|
490
493
|
if(location.hash==='#/usage')showView('usage');else if(location.hash==='#/graphs')showView('graphs');else if(location.hash==='#/execution-router-rules')showView('rules');else if(location.hash==='#/shortcuts')showView('shortcuts');else showView('jobs');
|
|
491
494
|
setInterval(()=>{if(location.hash==='#/usage')void drawUsage();},30000);
|
|
492
495
|
setInterval(()=>void refreshVersion(),30000);
|
package/dist/dashboard.js
CHANGED
|
@@ -202,7 +202,7 @@ export function serveDashboard(store, port = 8787, bus, refreshMs = 300000, jobI
|
|
|
202
202
|
res.end('Ignored');
|
|
203
203
|
return;
|
|
204
204
|
}
|
|
205
|
-
if (!event.repository || !webhook.repositories.has(event.repository) || (webhook.pullRequest !== undefined && event.pullRequest !== null && event.pullRequest !== webhook.pullRequest)) {
|
|
205
|
+
if (!event.repository || !webhook.repositories.has(event.repository) || (webhook.jobId !== undefined && event.jobId !== undefined && event.jobId !== null && event.jobId !== webhook.jobId) || (webhook.pullRequest !== undefined && event.pullRequest !== null && event.pullRequest !== webhook.pullRequest)) {
|
|
206
206
|
res.writeHead(202);
|
|
207
207
|
res.end('Ignored');
|
|
208
208
|
return;
|
|
@@ -238,6 +238,31 @@ export function serveDashboard(store, port = 8787, bus, refreshMs = 300000, jobI
|
|
|
238
238
|
}
|
|
239
239
|
return;
|
|
240
240
|
}
|
|
241
|
+
if (new URL(req.url ?? '/', 'http://127.0.0.1').pathname === '/api/control') {
|
|
242
|
+
if (req.method !== 'POST' || !resolvedActions?.control) {
|
|
243
|
+
res.writeHead(404);
|
|
244
|
+
res.end('Control API unavailable');
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
try {
|
|
248
|
+
const chunks = [];
|
|
249
|
+
for await (const chunk of req)
|
|
250
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
251
|
+
const body = JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
252
|
+
const result = await resolvedActions.control(body);
|
|
253
|
+
if (!res.headersSent)
|
|
254
|
+
res.writeHead(200, { 'content-type': 'application/json' });
|
|
255
|
+
if (!res.writableEnded)
|
|
256
|
+
res.end(JSON.stringify(result));
|
|
257
|
+
}
|
|
258
|
+
catch (error) {
|
|
259
|
+
if (!res.headersSent)
|
|
260
|
+
res.writeHead(409);
|
|
261
|
+
if (!res.writableEnded)
|
|
262
|
+
res.end(error instanceof Error ? error.message : String(error));
|
|
263
|
+
}
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
241
266
|
if (new URL(req.url ?? '/', 'http://127.0.0.1').pathname === '/api/job-action') {
|
|
242
267
|
const url = new URL(req.url ?? '/', 'http://127.0.0.1');
|
|
243
268
|
const requestedJobId = url.searchParams.get('id') ?? '';
|