agents-relay 1.0.132 → 1.0.134
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 +4 -5
- package/dist/cli-help.js +3 -3
- package/dist/cli.js +9 -30
- package/dist/dashboard-actions.js +58 -0
- package/dist/dashboard-html.js +12 -8
- package/dist/dashboard.js +27 -1
- package/dist/github-webhook.js +2 -2
- package/dist/relayd.js +14 -3
- package/dist/store.js +13 -2
- package/package.json +1 -1
- package/skills/agents-relay/SKILL.md +5 -4
package/README.md
CHANGED
|
@@ -23,11 +23,10 @@ cat > task.md <<'EOF'
|
|
|
23
23
|
Implement this phase using the Job objective as context.
|
|
24
24
|
EOF
|
|
25
25
|
npx agents-relay task create --repo OWNER/REPO --pr 12 --id job-1 \
|
|
26
|
-
--task-id initial --
|
|
27
|
-
--input-file task.md
|
|
26
|
+
--task-id initial --output task-pr --input-file task.md
|
|
28
27
|
|
|
29
28
|
npx agents-relay task create --repo OWNER/REPO --pr 12 --id job-1 \
|
|
30
|
-
--task-id child --
|
|
29
|
+
--task-id child --output task-pr \
|
|
31
30
|
--input "Implement the described child task and publish a terminal event."
|
|
32
31
|
|
|
33
32
|
npx agents-relay reconcile --repo OWNER/REPO --pr 12 --id job-1
|
|
@@ -132,7 +131,7 @@ For explicit local demo/test mode:
|
|
|
132
131
|
npm install
|
|
133
132
|
npm run build
|
|
134
133
|
node dist/cli.js init --file .agents-relay.json --title "Dogfood" --id demo
|
|
135
|
-
node dist/cli.js task create --file .agents-relay.json --task-id hello --
|
|
134
|
+
node dist/cli.js task create --file .agents-relay.json --task-id hello --output task-pr --input "Complete the described task"
|
|
136
135
|
node dist/cli.js reconcile --file .agents-relay.json
|
|
137
136
|
npx agents-relay serve --file .agents-relay.json
|
|
138
137
|
~~~
|
|
@@ -163,7 +162,7 @@ For automation, Agents Relay can use GitHub App installation authentication via
|
|
|
163
162
|
|
|
164
163
|
V1 assumes one active runner/daemon per job. Use a distributed atomic lease before running multiple reconcilers. Do not put secrets, credentials, private prompts, or large private payloads in the PR-body Job marker or PR-comment Task markers; store only summaries and artifact references.
|
|
165
164
|
|
|
166
|
-
|
|
165
|
+
Model-backed tasks are routed by Relay before launch. Normal task producers omit adapter/provider/model; explicit routing flags remain compatibility/debug constraints. See [docs/architecture.md](docs/architecture.md) and [skills/agents-relay/SKILL.md](skills/agents-relay/SKILL.md).
|
|
167
166
|
|
|
168
167
|
XChat and other orchestrators can consume Relay's exact shared discovery implementation without duplicating filesystem search logic:
|
|
169
168
|
|
package/dist/cli-help.js
CHANGED
|
@@ -98,12 +98,12 @@ Actions:
|
|
|
98
98
|
cancel Cancel a Task`,
|
|
99
99
|
'task create': `Create one durable Task under an existing Job.
|
|
100
100
|
|
|
101
|
-
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.
|
|
102
|
-
Common: --task-id ID, --
|
|
101
|
+
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.
|
|
102
|
+
Common: --task-id ID, --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 worker-router then model-router. Use --adapter orchestrator only for synchronous self-work; explicit adapter/provider/model remain compatibility/debug constraints.\nTasks with dependencies start DRAFT; tasks without dependencies start QUEUED unless --draft is given.
|
|
103
103
|
Directly-created Tasks are not wrapped in a synthetic Plan. Planner-generated Tasks are created internally by Relay from planner output.
|
|
104
104
|
|
|
105
105
|
Example:
|
|
106
|
-
npx agents-relay task create --repo OWNER/REPO --pr 12 --id job-1 --task-id build --
|
|
106
|
+
npx agents-relay task create --repo OWNER/REPO --pr 12 --id job-1 --task-id build --output task-pr --input-file task.md`,
|
|
107
107
|
'task update': `Update mutable durable Task fields in place.
|
|
108
108
|
|
|
109
109
|
Required: --task-id ID and the normal Job storage options.
|
package/dist/cli.js
CHANGED
|
@@ -7,6 +7,8 @@ import { dashboardManagedJobs, InMemoryStore, GitHubStore, resolvePullRequest, l
|
|
|
7
7
|
import { githubAuthContext } from './github-auth.js';
|
|
8
8
|
import { taskOverallTimeoutMs } from './types.js';
|
|
9
9
|
import { Reconciler } from './reconciler.js';
|
|
10
|
+
import { dashboardTaskAction, dashboardTaskSetState } from './dashboard-actions.js';
|
|
11
|
+
export { dashboardTaskAction, dashboardTaskSetState } from './dashboard-actions.js';
|
|
10
12
|
import { CodexAdapter, ChatGptAdapter } from './adapters.js';
|
|
11
13
|
import { CodexThreadContinuation, CommandContinuation, WebhookContinuation } from './continuation.js';
|
|
12
14
|
import { eventFor } from './events.js';
|
|
@@ -77,17 +79,18 @@ async function workerTaskFromArgs(job, args, now) {
|
|
|
77
79
|
throw new Error('task create requires --input-file PATH|- or --input MARKDOWN');
|
|
78
80
|
if (!['codex', 'chatgpt', 'orchestrator'].includes(adapter))
|
|
79
81
|
throw new Error('task create --adapter must be codex, chatgpt, or orchestrator');
|
|
80
|
-
if (adapter !== 'orchestrator' && !model)
|
|
81
|
-
throw new Error('task create requires --model for codex/chatgpt tasks');
|
|
82
82
|
if (outputKind !== 'task-pr' && outputKind !== 'file')
|
|
83
83
|
throw new Error('task create requires --output task-pr|file');
|
|
84
84
|
if (outputKind === 'file' && !outputPath)
|
|
85
85
|
throw new Error('--output file requires --output-path');
|
|
86
|
-
|
|
86
|
+
if (adapter !== 'orchestrator' && ['--provider', '--profile', '--reasoning', '--cwd', '--chatgpt-project'].some(flag => hasArg(args, flag)) && !model)
|
|
87
|
+
throw new Error('task create explicit routing requires --model');
|
|
88
|
+
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 };
|
|
89
|
+
const workerRequirements = adapter === 'orchestrator' || routing ? undefined : { capabilities: [], runtime: adapterValue ? adapter : undefined };
|
|
87
90
|
const closePolicy = hasArg(args, '--chatgpt-tab-close-policy') ? chatGptTabClosePolicy(args) : undefined;
|
|
88
91
|
const size = storySize(args);
|
|
89
92
|
const dependencies = arg(args, '--deps').split(',').filter(Boolean);
|
|
90
|
-
const timeoutMs = adapter === 'orchestrator' ? 0 : hasArg(args, '--timeout') ? integerArg(args, '--timeout', taskOverallTimeoutMs(size, routing?.reasoning)) : taskOverallTimeoutMs(size, routing
|
|
93
|
+
const timeoutMs = adapter === 'orchestrator' ? 0 : hasArg(args, '--timeout') ? integerArg(args, '--timeout', taskOverallTimeoutMs(size, routing?.reasoning)) : routing ? taskOverallTimeoutMs(size, routing.reasoning) : 0;
|
|
91
94
|
return {
|
|
92
95
|
jobId: job.id,
|
|
93
96
|
id: arg(args, '--task-id', 'initial'),
|
|
@@ -99,6 +102,7 @@ async function workerTaskFromArgs(job, args, now) {
|
|
|
99
102
|
parentTaskId: arg(args, '--parent') || null,
|
|
100
103
|
dependencies,
|
|
101
104
|
capabilities: arg(args, '--capabilities').split(',').filter(Boolean),
|
|
105
|
+
workerRequirements,
|
|
102
106
|
adapter,
|
|
103
107
|
chatgptTabClosePolicy: closePolicy,
|
|
104
108
|
input: input || 'true',
|
|
@@ -422,32 +426,6 @@ async function runJobCommand(action, args) {
|
|
|
422
426
|
export function runtimePlanner(args) { const plannerCommand = arg([...args], '--planner-command'); const modelRuntime = arg([...args], '--codex', 'codex'); return plannerCommand ? new CommandObjectivePlanner(plannerCommand) : new AgentObjectivePlanner(modelRuntime); }
|
|
423
427
|
export function runtimeReviewer() { return { requirements: { capabilities: ['model'] } }; }
|
|
424
428
|
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, { 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: new LayaCliDecisionEngine(), decisionMode: decisionMode(), reviewer: runtimeReviewer(), eventBus: bus, emit, chatgptTabClosePolicy: policy, projectPathForJob: async (job) => resolveRepositoryProjectPath(await resolveManagedProjectPath(job.repository, arg(args, '--project-path'), arg(args, '--workspace-root') || defaultWorkspaceRoot()), job.projectPath) }); }
|
|
425
|
-
export function dashboardTaskAction(store, reconciler, action) {
|
|
426
|
-
return async (jobId, taskId) => {
|
|
427
|
-
const job = await store.load(jobId);
|
|
428
|
-
const task = job.tasks.find(item => item.id === taskId);
|
|
429
|
-
if (!task)
|
|
430
|
-
throw new Error(`Task ${taskId} not found`);
|
|
431
|
-
if (action === 'retry') {
|
|
432
|
-
if (!['FAILED', 'BLOCKED'].includes(task.state))
|
|
433
|
-
throw new Error(`Task ${task.id} is not restartable (${task.state})`);
|
|
434
|
-
task.maxAttempts = extendRetryBudget(task);
|
|
435
|
-
await transitionTaskEvent(task, 'retry');
|
|
436
|
-
task.error = null;
|
|
437
|
-
}
|
|
438
|
-
else {
|
|
439
|
-
if (!['DRAFT', 'QUEUED', 'RUNNING', 'BLOCKED', 'FAILED'].includes(task.state))
|
|
440
|
-
throw new Error(`Task ${task.id} is not cancellable (${task.state})`);
|
|
441
|
-
await transitionTaskEvent(task, task.state === 'RUNNING' ? 'task.cancelled' : 'cancel');
|
|
442
|
-
task.error = 'Cancelled by user';
|
|
443
|
-
}
|
|
444
|
-
task.leaseOwner = null;
|
|
445
|
-
task.leaseExpiresAt = null;
|
|
446
|
-
task.executionId = null;
|
|
447
|
-
await store.saveTask(task);
|
|
448
|
-
return await reconciler.wake(job.id);
|
|
449
|
-
};
|
|
450
|
-
}
|
|
451
429
|
export function dashboardJobAction(store, reconciler, action) {
|
|
452
430
|
return async (jobId) => {
|
|
453
431
|
const job = await store.load(jobId);
|
|
@@ -501,6 +479,7 @@ export async function createService(args) {
|
|
|
501
479
|
cancel: dashboardJobAction(loaded.store, reconciler, 'cancel'),
|
|
502
480
|
taskRetry: dashboardTaskAction(loaded.store, reconciler, 'retry'),
|
|
503
481
|
taskCancel: dashboardTaskAction(loaded.store, reconciler, 'cancel'),
|
|
482
|
+
taskSetState: dashboardTaskSetState(loaded.store),
|
|
504
483
|
});
|
|
505
484
|
await once(dashboardServer, 'listening');
|
|
506
485
|
const stopWatcher = await reconciler.watch(loaded.job.id);
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { extendRetryBudget, transitionTaskEvent } from './scheduler.js';
|
|
2
|
+
import { applyTaskWorkflowDecision } from './workflow/runtime.js';
|
|
3
|
+
export function dashboardTaskAction(store, reconciler, action) {
|
|
4
|
+
return async (jobId, taskId) => {
|
|
5
|
+
const job = await store.load(jobId);
|
|
6
|
+
const task = job.tasks.find(item => item.id === taskId);
|
|
7
|
+
if (!task)
|
|
8
|
+
throw new Error(`Task ${taskId} not found`);
|
|
9
|
+
if (action === 'retry') {
|
|
10
|
+
if (!['FAILED', 'BLOCKED'].includes(task.state))
|
|
11
|
+
throw new Error(`Task ${task.id} is not restartable (${task.state})`);
|
|
12
|
+
task.maxAttempts = extendRetryBudget(task);
|
|
13
|
+
await transitionTaskEvent(task, 'retry');
|
|
14
|
+
task.error = null;
|
|
15
|
+
}
|
|
16
|
+
else {
|
|
17
|
+
if (!['DRAFT', 'QUEUED', 'RUNNING', 'BLOCKED', 'FAILED'].includes(task.state))
|
|
18
|
+
throw new Error(`Task ${task.id} is not cancellable (${task.state})`);
|
|
19
|
+
await transitionTaskEvent(task, task.state === 'RUNNING' ? 'task.cancelled' : 'cancel');
|
|
20
|
+
task.error = 'Cancelled by user';
|
|
21
|
+
}
|
|
22
|
+
task.leaseOwner = null;
|
|
23
|
+
task.leaseExpiresAt = null;
|
|
24
|
+
task.executionId = null;
|
|
25
|
+
await store.saveTask(task);
|
|
26
|
+
return await reconciler.wake(job.id);
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
export function dashboardTaskSetState(store) {
|
|
30
|
+
return async (jobId, taskId, state) => {
|
|
31
|
+
const job = await store.load(jobId);
|
|
32
|
+
const task = job.tasks.find(item => item.id === taskId);
|
|
33
|
+
if (!task)
|
|
34
|
+
throw new Error(`Task ${taskId} not found`);
|
|
35
|
+
if (task.state === state)
|
|
36
|
+
return job;
|
|
37
|
+
const now = new Date().toISOString();
|
|
38
|
+
const from = task.state;
|
|
39
|
+
applyTaskWorkflowDecision(task, {
|
|
40
|
+
accepted: true,
|
|
41
|
+
target: state,
|
|
42
|
+
guard: null,
|
|
43
|
+
actions: [],
|
|
44
|
+
mutations: [],
|
|
45
|
+
effects: [],
|
|
46
|
+
reason: 'accepted',
|
|
47
|
+
}, now);
|
|
48
|
+
task.manualStateOverrides = [
|
|
49
|
+
...(task.manualStateOverrides ?? []),
|
|
50
|
+
{ from, to: state, at: now, actor: 'dashboard-user' },
|
|
51
|
+
].slice(-20);
|
|
52
|
+
task.leaseOwner = null;
|
|
53
|
+
task.leaseExpiresAt = null;
|
|
54
|
+
task.executionId = null;
|
|
55
|
+
await store.saveTask(task);
|
|
56
|
+
return await store.load(job.id);
|
|
57
|
+
};
|
|
58
|
+
}
|
package/dist/dashboard-html.js
CHANGED
|
@@ -298,6 +298,8 @@ function counts(tasks){const out={};for(const task of Array.isArray(tasks)?tasks
|
|
|
298
298
|
function compactCounts(input){const order=['RUNNING','BLOCKED','FAILED','QUEUED','DRAFT','SUCCEEDED','CANCELLED'];return order.filter(state=>input[state]).map(state=>input[state]+' '+state).join(', ')||'No tasks';}
|
|
299
299
|
function relativeTime(timestamp){if(!timestamp)return '—';const parsed=Date.parse(timestamp);if(!Number.isFinite(parsed))return value(timestamp);const seconds=Math.max(0,Math.round((Date.now()-parsed)/1000));if(seconds<60)return 'just now';if(seconds<3600)return Math.floor(seconds/60)+'m ago';if(seconds<86400)return Math.floor(seconds/3600)+'h ago';return Math.floor(seconds/86400)+'d ago';}
|
|
300
300
|
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;}
|
|
301
|
+
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;}
|
|
302
|
+
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;}
|
|
301
303
|
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,job.prNumber);if(url){const link=document.createElement('a');link.href=url;link.target='_blank';link.rel='noopener noreferrer';link.textContent='PR #'+job.prNumber;line.append(link);}else line.append(text('unavailable'));box.append(line);return box;}
|
|
302
304
|
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;}
|
|
303
305
|
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;}
|
|
@@ -305,7 +307,8 @@ function parseAgentGraphSource(source){if(typeof source!=='string'||!source.trim
|
|
|
305
307
|
function agentGraphField(task){if(typeof task.agentGraph!=='string'||!task.agentGraph.trim())return null;const box=element('div','detail-group');box.style.gridColumn='1 / -1';box.append(element('p','detail-label',text('Task agent graph')));const graph=element('div','agent-graph');const parsed=parseAgentGraphSource(task.agentGraph);if(parsed&&parsed.waves.length){parsed.waves.forEach((wave,index)=>{const row=element('div','agent-wave');if(index>0)row.append(element('span','agent-arrow',text('↓')));for(const id of wave)row.append(element('span','agent-node',text(id+' · '+(parsed.agents.get(id)||id))));graph.append(row);});}const source=document.createElement('pre');source.className='agent-graph-source';source.textContent=task.agentGraph;graph.append(source);box.append(graph);return box;}
|
|
306
308
|
function clearEvents(){displayedEventIds.clear();eventFeed.replaceChildren(element('li','empty',text('Waiting for events.')));}
|
|
307
309
|
async function loadEventHistory(){if(!selectedJob){clearEvents();return;}const params=new URLSearchParams({job:selectedJob,limit:String(MAX_EVENTS)});if(eventTaskId)params.set('task',eventTaskId);try{const history=await loadJSON('/api/events?'+params.toString());clearEvents();for(const event of Array.isArray(history)?history.slice().reverse():[])addEvent(event);}catch{clearEvents();}}
|
|
308
|
-
function
|
|
310
|
+
function copySelectedJobIdentity(id){const job=jobs.find(candidate=>candidate.id===id);if(!job||!navigator.clipboard?.writeText)return;void navigator.clipboard.writeText(repoName(job.repository)+'/pr#'+job.prNumber).catch(()=>{});}
|
|
311
|
+
function selectJob(id){selectedJob=id;selectedTaskId=null;eventTaskId=null;rememberSelection();clearEvents();location.hash=id?'#/jobs/'+encodeURIComponent(id):'#/jobs';copySelectedJobIdentity(id);renderAll();void loadEventHistory();}
|
|
309
312
|
function matchesQuickFilter(job){if(quickMode==='autonomous')return job.executionMode==='autonomous';if(quickMode==='draft')return job.draft||job.state==='DRAFT';if(quickMode==='status')return !!selectedRuntimeStatus&&(job.runtimeStatus||'IDLE')===selectedRuntimeStatus;return true;}
|
|
310
313
|
function matchesSearch(job){const query=searchQuery.trim().toLowerCase();if(!query)return true;const pr=String(job.prNumber||'');const normalized=query.replace(/^pr\s*#?/,'').replace(/^#/,'');return String(job.title||job.id||'').toLowerCase().includes(query)||pr.includes(normalized);}
|
|
311
314
|
function filteredJobs(){return jobs.filter(job=>(!selectedRepo||job.repository===selectedRepo)&&matchesQuickFilter(job)&&matchesSearch(job));}
|
|
@@ -332,16 +335,17 @@ function actionButton(job,action,label,icon){const button=document.createElement
|
|
|
332
335
|
async function runJobAction(job,action,button){button.disabled=true;try{const response=await fetch('/api/job-action?id='+encodeURIComponent(job.id)+'&action='+encodeURIComponent(action),{method:'POST'});if(!response.ok)throw new Error(response.status+' '+response.statusText);await refresh(true);}catch(error){button.title=labelForAction(action)+': '+error.message;}finally{renderAll();}}
|
|
333
336
|
function labelForAction(action){return action==='start'?'Start':action==='reconcile'?'Reconcile':'Cancel';}
|
|
334
337
|
function repoName(repository){if(typeof repository!=='string'||!repository)return 'unknown-repo';const parts=repository.split('/').filter(Boolean);return parts.at(-1)||repository;}
|
|
335
|
-
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(job.id===selectedJob));row.addEventListener('click',()=>selectJob(job.id));const content=element('div','job-content');const select=document.createElement('button');select.type='button';select.className='job-button';select.setAttribute('aria-current',String(job.id===selectedJob));select.title=job.title||job.id;select.addEventListener('click',event=>{event.stopPropagation();selectJob(job.id);});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.executionMode==='autonomous')states.append(element('span','badge flag-autonomous',text('AUTONOMOUS')));if(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 actions=element('span','job-actions');actions.append(actionButton(job,'start','Start','▶'),actionButton(job,'reconcile','Reconcile','↻'),actionButton(job,'cancel','Cancel','×'));const primary=element('div','job-primary');primary.append(states,actions);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));}}
|
|
336
|
-
function taskNode(task,children,depth){const li=element('li');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',()=>{selectedTaskId=task.id;eventTaskId=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)));li.append(row);const nested=children.get(task.id)||[];if(nested.length){const ul=element('ul','tree');for(const child of nested)ul.append(taskNode(child,children,depth+1));li.append(ul);}return li;}
|
|
338
|
+
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(job.id===selectedJob));row.addEventListener('click',()=>selectJob(job.id));const content=element('div','job-content');const select=document.createElement('button');select.type='button';select.className='job-button';select.setAttribute('aria-current',String(job.id===selectedJob));select.title=job.title||job.id;select.addEventListener('click',event=>{event.stopPropagation();selectJob(job.id);});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.executionMode==='autonomous')states.append(element('span','badge flag-autonomous',text('AUTONOMOUS')));if(job.reviewEnabled!==false&&job.githubState!=='MERGED')states.append(element('span','badge flag-review',text('REVIEW REQUIRED')));if(job.githubState==='MERGED'&&job.publication?.version)states.append(element('span','badge SUCCEEDED',text('PUBLISHED v'+job.publication.version)));if(isCompleteUnmerged(job))states.append(element('span','badge MERGE_PENDING',text('MERGE PENDING')));const actions=element('span','job-actions');actions.append(actionButton(job,'start','Start','▶'),actionButton(job,'reconcile','Reconcile','↻'),actionButton(job,'cancel','Cancel','×'));const primary=element('div','job-primary');primary.append(states,actions);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));}}
|
|
339
|
+
function taskNode(task,children,depth){const li=element('li');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)));li.append(row);const nested=children.get(task.id)||[];if(nested.length){const ul=element('ul','tree');for(const child of nested)ul.append(taskNode(child,children,depth+1));li.append(ul);}return li;}
|
|
337
340
|
function buildTree(tasks){taskTree.replaceChildren();if(!Array.isArray(tasks)||!tasks.length){taskTree.append(element('li','empty',text('No tasks in durable state.')));return;}const known=new Set(tasks.map(task=>task.id));const children=new Map();const roots=[];for(const task of tasks){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));}
|
|
338
341
|
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;}
|
|
339
|
-
|
|
340
|
-
function
|
|
341
|
-
|
|
342
|
-
function
|
|
342
|
+
const TASK_STATES=['DRAFT','QUEUED','RUNNING','BLOCKED','SUCCEEDED','FAILED','CANCELLED'];
|
|
343
|
+
async function setTaskState(task,state,select){const job=selected();if(!job||state===task.state)return;select.disabled=true;try{const response=await fetch('/api/task-state?id='+encodeURIComponent(job.id)+'&task='+encodeURIComponent(task.id)+'&state='+encodeURIComponent(state),{method:'POST'});if(!response.ok)throw new Error(response.status+' '+response.statusText);const updated=await response.json();const index=jobs.findIndex(candidate=>candidate.id===job.id);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;}}
|
|
344
|
+
function taskStateField(task){const box=element('div','detail-group');box.append(element('p','detail-label',text('State')));const select=document.createElement('select');select.className='job-filter task-state-select';select.setAttribute('aria-label','Task state');for(const state of TASK_STATES)select.append(new Option(state,state));select.value=task.state;select.addEventListener('change',()=>void setTaskState(task,select.value,select));box.append(select);return box;}
|
|
345
|
+
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):'—');}
|
|
346
|
+
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);}
|
|
343
347
|
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();}
|
|
344
|
-
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(job.id!==selectedJob){selectedJob=job.id;selectedTaskId=null;eventTaskId=null;clearEvents();rememberSelection();}const summary=element('div','job-summary');summary.append(field('PR state',job.githubState||'UNKNOWN'),field('Lifecycle state',job.state),field('Runtime status',job.runtimeStatus||'IDLE'),field('Execution mode',job.executionMode||'fixed'),field('Review',reviewStatus(job)),
|
|
348
|
+
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(job.id!==selectedJob){selectedJob=job.id;selectedTaskId=null;eventTaskId=null;clearEvents();rememberSelection();}const summary=element('div','job-summary');summary.append(field('PR state',job.githubState||'UNKNOWN'),field('Lifecycle state',job.state),field('Runtime status',job.runtimeStatus||'IDLE'),field('Execution mode',job.executionMode||'fixed'),field('Review',reviewStatus(job)),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)));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();}
|
|
345
349
|
function renderAll(){jobs.sort(compareJobs);syncFilters();renderJobList();renderSelectedJob();syncMobileJobsView();}
|
|
346
350
|
function isCompleteUnmerged(job){return job.runtimeStatus==='MERGE_PENDING';}
|
|
347
351
|
function isTerminalJob(job){return ['COMPLETED','CANCELLED'].includes(job.state)||['MERGED','CLOSED'].includes(job.githubState);}
|
package/dist/dashboard.js
CHANGED
|
@@ -68,7 +68,7 @@ export function serveDashboard(store, port = 8787, bus, refreshMs = 300000, jobI
|
|
|
68
68
|
const discoveredJobs = async (forceRefresh = false) => {
|
|
69
69
|
try {
|
|
70
70
|
const jobs = overviewProvider
|
|
71
|
-
? (await overviewProvider(forceRefresh)).map(item => ({ ...item.job, githubState: item.githubState, runtimeStatus: deriveJobRuntimeStatus(item.job, item.githubState), mergedAt: item.mergedAt, closedAt: item.closedAt, draft: item.draft }))
|
|
71
|
+
? (await overviewProvider(forceRefresh)).map(item => ({ ...item.job, githubState: item.githubState, runtimeStatus: deriveJobRuntimeStatus(item.job, item.githubState), mergedAt: item.mergedAt, closedAt: item.closedAt, draft: item.draft, publication: item.publication }))
|
|
72
72
|
: jobId ? [await store.load(jobId)] : [];
|
|
73
73
|
lastKnownJobs = jobs;
|
|
74
74
|
return jobs;
|
|
@@ -261,6 +261,32 @@ export function serveDashboard(store, port = 8787, bus, refreshMs = 300000, jobI
|
|
|
261
261
|
}
|
|
262
262
|
return;
|
|
263
263
|
}
|
|
264
|
+
if (new URL(req.url ?? '/', 'http://127.0.0.1').pathname === '/api/task-state') {
|
|
265
|
+
const url = new URL(req.url ?? '/', 'http://127.0.0.1');
|
|
266
|
+
const requestedJobId = url.searchParams.get('id') ?? '';
|
|
267
|
+
const requestedTaskId = url.searchParams.get('task') ?? '';
|
|
268
|
+
const requestedState = url.searchParams.get('state') ?? '';
|
|
269
|
+
const validState = ['DRAFT', 'QUEUED', 'RUNNING', 'BLOCKED', 'SUCCEEDED', 'FAILED', 'CANCELLED'].includes(requestedState);
|
|
270
|
+
if (req.method !== 'POST' || !/^[A-Za-z0-9_-]+$/.test(requestedJobId) || !/^[A-Za-z0-9_-]+$/.test(requestedTaskId) || !validState || !resolvedActions?.taskSetState) {
|
|
271
|
+
res.writeHead(404);
|
|
272
|
+
res.end('Task state override unavailable');
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
try {
|
|
276
|
+
const result = await resolvedActions.taskSetState(requestedJobId, requestedTaskId, requestedState);
|
|
277
|
+
if (!res.headersSent)
|
|
278
|
+
res.writeHead(200, { 'content-type': 'application/json' });
|
|
279
|
+
if (!res.writableEnded)
|
|
280
|
+
res.end(JSON.stringify(result));
|
|
281
|
+
}
|
|
282
|
+
catch (error) {
|
|
283
|
+
if (!res.headersSent)
|
|
284
|
+
res.writeHead(409);
|
|
285
|
+
if (!res.writableEnded)
|
|
286
|
+
res.end(error instanceof Error ? error.message : String(error));
|
|
287
|
+
}
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
264
290
|
if (new URL(req.url ?? '/', 'http://127.0.0.1').pathname === '/api/job') {
|
|
265
291
|
try {
|
|
266
292
|
const requestedJobId = new URL(req.url ?? '/', 'http://127.0.0.1').searchParams.get('id') ?? undefined;
|
package/dist/github-webhook.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createHmac, timingSafeEqual } from 'node:crypto';
|
|
2
2
|
export const PACKAGE_PUBLISHED_MARKER = 'agents-relay:package-published:v1';
|
|
3
3
|
const WAKE_EVENTS = new Set(['pull_request', 'issue_comment', 'pull_request_review', 'pull_request_review_comment', 'check_run', 'check_suite', 'workflow_run']);
|
|
4
|
-
function
|
|
4
|
+
export function parsePackagePublicationMarker(body) {
|
|
5
5
|
if (typeof body !== 'string')
|
|
6
6
|
return null;
|
|
7
7
|
const prefix = '<!-- ' + PACKAGE_PUBLISHED_MARKER + ' ';
|
|
@@ -51,7 +51,7 @@ export function parseGitHubWebhook(headers, body) {
|
|
|
51
51
|
const issue = payload.issue;
|
|
52
52
|
const number = Number(pullRequest?.number ?? (issue?.pull_request ? issue.number : 0));
|
|
53
53
|
const comment = payload.comment;
|
|
54
|
-
const publication = event === 'issue_comment' ?
|
|
54
|
+
const publication = event === 'issue_comment' ? parsePackagePublicationMarker(comment?.body) : null;
|
|
55
55
|
return {
|
|
56
56
|
deliveryId,
|
|
57
57
|
event,
|
package/dist/relayd.js
CHANGED
|
@@ -11,7 +11,8 @@ import { RepositoryWorkerPool, aggregateManagedGitHubJobs } from './pool.js';
|
|
|
11
11
|
import { Reconciler } from './reconciler.js';
|
|
12
12
|
import { dashboardManagedJobs, GitHubStore, InMemoryStore, loadManagedGitHubPullRequestGraphql } from './store.js';
|
|
13
13
|
import { githubAuthContext } from './github-auth.js';
|
|
14
|
-
import { dashboardJobAction,
|
|
14
|
+
import { dashboardJobAction, isEntrypoint, runtimePlanner, runtimeReviewer, startService, SERVICE_DEFAULTS } from './cli.js';
|
|
15
|
+
import { dashboardTaskAction, dashboardTaskSetState } from './dashboard-actions.js';
|
|
15
16
|
import { codexAndZaiUsageRegistry } from './usage.js';
|
|
16
17
|
import { defaultWorkspaceRoot, discoverWorkspaceRepositories, resolveManagedProjectPath, resolveRepositoryProjectPath } from './workspace.js';
|
|
17
18
|
import { deriveJobRuntimeStatus } from './runtime-status.js';
|
|
@@ -188,6 +189,15 @@ export async function runDaemon(argv) {
|
|
|
188
189
|
const store = new GitHubStore(client, overview.job.repository, overview.job.prNumber, trusted);
|
|
189
190
|
return await dashboardTaskAction(store, makeReconciler(store, concurrency), action)(jobId, taskId);
|
|
190
191
|
};
|
|
192
|
+
const workspaceTaskSetState = async (jobId, taskId, state) => {
|
|
193
|
+
const overview = cachedOverviews.find(item => item.job.id === jobId);
|
|
194
|
+
if (!overview)
|
|
195
|
+
throw new Error(`Managed job ${jobId} not found`);
|
|
196
|
+
const store = new GitHubStore(client, overview.job.repository, overview.job.prNumber, trusted);
|
|
197
|
+
const result = await dashboardTaskSetState(store)(jobId, taskId, state);
|
|
198
|
+
await refreshPullRequest(overview.job.repository, overview.job.prNumber);
|
|
199
|
+
return result;
|
|
200
|
+
};
|
|
191
201
|
const secretFile = value(argv, '--github-webhook-secret-file');
|
|
192
202
|
const secret = value(argv, '--github-webhook-secret') || process.env.AGENTS_RELAY_GITHUB_WEBHOOK_SECRET || (secretFile ? (await readFile(secretFile, 'utf8')).trim() : '');
|
|
193
203
|
const webhook = secret ? {
|
|
@@ -216,14 +226,14 @@ export async function runDaemon(argv) {
|
|
|
216
226
|
const cached = cachedOverviews.find(item => item.job.id === jobId);
|
|
217
227
|
if (!cached)
|
|
218
228
|
return null;
|
|
219
|
-
const cachedJob = () => ({ ...cached.job, githubState: cached.githubState, runtimeStatus: deriveJobRuntimeStatus(cached.job, cached.githubState), mergedAt: cached.mergedAt, closedAt: cached.closedAt, draft: cached.draft });
|
|
229
|
+
const cachedJob = () => ({ ...cached.job, githubState: cached.githubState, runtimeStatus: deriveJobRuntimeStatus(cached.job, cached.githubState), mergedAt: cached.mergedAt, closedAt: cached.closedAt, draft: cached.draft, publication: cached.publication });
|
|
220
230
|
if (client.rateLimitStatus().limited)
|
|
221
231
|
return cachedJob();
|
|
222
232
|
try {
|
|
223
233
|
const refreshed = (await refreshPullRequest(cached.job.repository, cached.job.prNumber)).find(item => item.job.id === jobId);
|
|
224
234
|
if (!refreshed)
|
|
225
235
|
return null;
|
|
226
|
-
return { ...refreshed.job, githubState: refreshed.githubState, runtimeStatus: deriveJobRuntimeStatus(refreshed.job, refreshed.githubState), mergedAt: refreshed.mergedAt, closedAt: refreshed.closedAt, draft: refreshed.draft };
|
|
236
|
+
return { ...refreshed.job, githubState: refreshed.githubState, runtimeStatus: deriveJobRuntimeStatus(refreshed.job, refreshed.githubState), mergedAt: refreshed.mergedAt, closedAt: refreshed.closedAt, draft: refreshed.draft, publication: refreshed.publication };
|
|
227
237
|
}
|
|
228
238
|
catch (error) {
|
|
229
239
|
process.stderr.write(`warning: selected job refresh unavailable: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
@@ -236,6 +246,7 @@ export async function runDaemon(argv) {
|
|
|
236
246
|
cancel: workspaceJobAction('cancel'),
|
|
237
247
|
taskRetry: workspaceTaskAction('retry'),
|
|
238
248
|
taskCancel: workspaceTaskAction('cancel'),
|
|
249
|
+
taskSetState: workspaceTaskSetState,
|
|
239
250
|
});
|
|
240
251
|
await once(server, 'listening');
|
|
241
252
|
const reconcileSafely = async () => {
|
package/dist/store.js
CHANGED
|
@@ -2,6 +2,15 @@ import { spawn } from 'node:child_process';
|
|
|
2
2
|
import { createSign } from 'node:crypto';
|
|
3
3
|
import { parseAgent, parseJob, parseTask, renderAgentComment, renderJobComment, renderTaskComment, AGENT_MARKER, JOB_MARKER, TASK_MARKER } from './markers.js';
|
|
4
4
|
import { isTaskForJob, sameTaskDefinition } from './store-task-definition.js';
|
|
5
|
+
import { parsePackagePublicationMarker } from './github-webhook.js';
|
|
6
|
+
function publicationFromComments(rows, trustedAuthors) {
|
|
7
|
+
const allowed = rows.filter(item => {
|
|
8
|
+
const author = item.author;
|
|
9
|
+
const login = typeof author?.login === 'string' ? author.login : '';
|
|
10
|
+
return trustedAuthors.has(login) || login === 'github-actions[bot]';
|
|
11
|
+
});
|
|
12
|
+
return allowed.map(item => parsePackagePublicationMarker(String(item.body ?? ''))).filter((item) => item !== null).at(-1) ?? null;
|
|
13
|
+
}
|
|
5
14
|
export async function loadManagedGitHubPullRequestGraphql(client, repository, prNumber, trustedAuthors) {
|
|
6
15
|
const [owner, name] = repository.split('/', 2);
|
|
7
16
|
if (!owner || !name || !Number.isInteger(prNumber) || prNumber <= 0 || !client.graphql)
|
|
@@ -21,6 +30,7 @@ export async function loadManagedGitHubPullRequestGraphql(client, repository, pr
|
|
|
21
30
|
const bodyJob = parseJob(typeof pr.body === 'string' ? pr.body : '');
|
|
22
31
|
const commentJobs = trusted.map(item => parseJob(String(item.body ?? ''))).filter((item) => item !== null);
|
|
23
32
|
const jobs = bodyJob ? [bodyJob] : commentJobs;
|
|
33
|
+
const publication = publicationFromComments((Array.isArray(comments) ? comments : []), trustedAuthors);
|
|
24
34
|
const tasks = trusted.map(item => {
|
|
25
35
|
const row = item;
|
|
26
36
|
const task = parseTask(String(row.body ?? ''));
|
|
@@ -35,7 +45,7 @@ export async function loadManagedGitHubPullRequestGraphql(client, repository, pr
|
|
|
35
45
|
job.tasks = tasks.filter(task => task.jobId === job.id || (!task.jobId && jobs.length === 1));
|
|
36
46
|
if (!job.description && typeof pr.body === 'string' && pr.body)
|
|
37
47
|
job.description = pr.body;
|
|
38
|
-
return { job, githubState, draft: pr.isDraft === true, mergedAt: typeof pr.mergedAt === 'string' ? pr.mergedAt : null, closedAt: typeof pr.closedAt === 'string' ? pr.closedAt : null };
|
|
48
|
+
return { job, githubState, draft: pr.isDraft === true, mergedAt: typeof pr.mergedAt === 'string' ? pr.mergedAt : null, closedAt: typeof pr.closedAt === 'string' ? pr.closedAt : null, publication };
|
|
39
49
|
});
|
|
40
50
|
}
|
|
41
51
|
export async function loadManagedGitHubJobGraphql(client, repository, prNumber, jobId, trustedAuthors) {
|
|
@@ -456,6 +466,7 @@ export async function listManagedGitHubJobs(client, repository, trustedAuthors)
|
|
|
456
466
|
const bodyJob = parseJob(typeof pr.body === 'string' ? pr.body : '');
|
|
457
467
|
const commentJobs = trusted.map(item => parseJob(String(item.body ?? ''))).filter((item) => item !== null);
|
|
458
468
|
const jobs = bodyJob ? [bodyJob] : commentJobs;
|
|
469
|
+
const publication = publicationFromComments(rows, trustedAuthors);
|
|
459
470
|
const tasks = trusted.map(item => parseTask(String(item.body ?? ''))).filter((item) => item !== null);
|
|
460
471
|
const state = String(pr.state ?? '').toUpperCase();
|
|
461
472
|
const githubState = pr.merged === true || state === 'MERGED' ? 'MERGED' : state === 'CLOSED' ? 'CLOSED' : 'OPEN';
|
|
@@ -465,7 +476,7 @@ export async function listManagedGitHubJobs(client, repository, trustedAuthors)
|
|
|
465
476
|
job.prNumber = number;
|
|
466
477
|
job.repository = repository;
|
|
467
478
|
job.tasks = tasks.filter(task => task.jobId === job.id || (!task.jobId && jobs.length === 1));
|
|
468
|
-
output.push({ job, githubState, draft: pr.isDraft === true, mergedAt: typeof pr.mergedAt === 'string' ? pr.mergedAt : null, closedAt: typeof pr.closedAt === 'string' ? pr.closedAt : null });
|
|
479
|
+
output.push({ job, githubState, draft: pr.isDraft === true, mergedAt: typeof pr.mergedAt === 'string' ? pr.mergedAt : null, closedAt: typeof pr.closedAt === 'string' ? pr.closedAt : null, publication });
|
|
469
480
|
}
|
|
470
481
|
}
|
|
471
482
|
};
|
package/package.json
CHANGED
|
@@ -21,14 +21,15 @@ Normal worker tasks use an agent runtime:
|
|
|
21
21
|
|
|
22
22
|
Shell commands, tests, Markad Vision, ffmpeg, build tools, and similar CLIs are tools used inside an agent or directly by the orchestrator. They are not Agents Relay worker tasks.
|
|
23
23
|
|
|
24
|
-
Every model-backed task
|
|
24
|
+
Every model-backed task producer normally declares:
|
|
25
25
|
- its descriptive `input`;
|
|
26
|
-
-
|
|
27
|
-
- routing metadata;
|
|
26
|
+
- semantic requirements such as capabilities/runtime only when known;
|
|
28
27
|
- exactly one durable output contract:
|
|
29
28
|
- `task_pr`: the managed task/PR is the authoritative result; or
|
|
30
29
|
- `file`: an exact file path is the authoritative result.
|
|
31
30
|
|
|
31
|
+
Relay then runs worker-router followed by model-router and persists the selected worker/adapter and model routing before launch. Explicit adapter/provider/model values are compatibility/debug constraints, not normal producer requirements.
|
|
32
|
+
|
|
32
33
|
## Lifecycle contract
|
|
33
34
|
|
|
34
35
|
Events are authoritative for model-backed task state.
|
|
@@ -62,7 +63,7 @@ Example:
|
|
|
62
63
|
~~~sh
|
|
63
64
|
npx agents-relay job create --repo OWNER/REPO --head feat/example --base main --id JOB_ID --title "Objective"
|
|
64
65
|
|
|
65
|
-
npx agents-relay task create --repo OWNER/REPO --pr 12 --id JOB_ID --task-id implement --
|
|
66
|
+
npx agents-relay task create --repo OWNER/REPO --pr 12 --id JOB_ID --task-id implement --output task-pr --input "Implement the requested change, validate it, and update the managed task/PR."
|
|
66
67
|
|
|
67
68
|
npx agents-relayd --repo OWNER/REPO --events nats
|
|
68
69
|
~~~
|