agents-relay 2.0.115 → 2.0.117
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/dist/dashboard-html.js +2 -2
- package/dist/reconciler-review.js +11 -0
- package/dist/workflow/contracts.js +2 -0
- package/dist/workflow/review-v2.js +2 -1
- package/package.json +1 -1
- package/skills/agents-relay/SKILL.md +5 -1
- package/skills/agents-relay/contracts/event-contract.md +2 -0
- package/workflow/review-lifecycle.mmd +4 -0
package/dist/dashboard-html.js
CHANGED
|
@@ -388,10 +388,10 @@ function jobChatRef(job){const pr=Number(job?.prNumber||0);return repoName(job?.
|
|
|
388
388
|
function chatShortcutUrl(prompt){return 'https://chatgpt.com/?prompt='+encodeURIComponent(prompt);}
|
|
389
389
|
const CHAT_SHORTCUT_TARGET='agents-relay-shortcut';
|
|
390
390
|
function chatShortcutField(job){const box=element('div','detail-group');box.append(element('p','detail-label',text('ChatGPT shortcut')));const select=document.createElement('select');select.className='job-filter job-chat-shortcut-select';select.setAttribute('aria-label','ChatGPT shortcut');select.append(new Option('',''));for(const item of parseChatShortcuts(chatShortcutText()))select.append(new Option(item.name,item.prompt));select.value='';select.addEventListener('change',()=>{if(!select.value)return;const prompt=select.value.replaceAll('%1',jobChatRef(job));window.open(chatShortcutUrl(prompt),CHAT_SHORTCUT_TARGET,'noopener');select.value='';});box.append(select);return box;}
|
|
391
|
-
function renderJobList(){jobList.replaceChildren();const visible=filteredJobs();if(!jobs.length){jobList.append(element('li','empty',text('No durable jobs discovered.')));return;}if(!visible.length){jobList.append(element('li','empty',text('No jobs match the selected repo.')));return;}for(const job of visible){const row=element('div','job-row'+(isCompleteUnmerged(job)?' complete-unmerged':''));row.setAttribute('aria-current',String(jobSelectionKey(job)===selectedJob));row.addEventListener('click',()=>selectJob(job));const content=element('div','job-content');const select=document.createElement('button');select.type='button';select.className='job-button';select.setAttribute('aria-current',String(jobSelectionKey(job)===selectedJob));select.title=job.title||job.id;select.addEventListener('click',event=>{event.stopPropagation();selectJob(job);});select.append(element('span','job-title',text(repoName(job.repository)+'/'+(Number(job.prNumber||0)>0?'PR#'+value(job.prNumber):'
|
|
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)+'/'+value(job.id)+(Number(job.prNumber||0)>0?'/PR#'+value(job.prNumber):''))));const states=element('span','job-status-line');states.append(element('span','badge '+value(job.state),text(value(job.state))),element('span','badge '+value(job.runtimeStatus||'IDLE'),text(value(job.runtimeStatus||'IDLE'))));if(job.publicationFailure)states.append(element('span','badge FAILED',text('PUBLISH FAILED: '+value(job.publicationFailure.stage))));else if(job.publication?.version)states.append(element('span','badge SUCCEEDED',text('v'+job.publication.version)));else if(Number(job.prNumber||0)>0&&job.githubState==='MERGED')states.append(element('span','badge MERGED',text('PR Merged')));else if(job.state==='COMPLETED'&&job.githubState==='CLOSED')states.append(element('span','badge SUCCEEDED',text('PR Archived')));if(job.executionMode==='autonomous')states.append(element('span','badge flag-autonomous',text('AUTONOMOUS')));if(job.state==='ACTIVE'&&job.reviewEnabled!==false)states.append(element('span','badge flag-review',text('REVIEW REQUIRED')));if(isCompleteUnmerged(job))states.append(element('span','badge MERGE_PENDING',text('MERGE PENDING')));const primary=element('div','job-primary');primary.append(states);const lastLine=element('div','job-last-line');lastLine.append(element('span','subtle',text(compactCounts(counts(job.tasks)))),element('span','subtle',text(relativeTime(job.updatedAt))));content.append(select,primary,lastLine);row.append(content);jobList.append(element('li','',row));}}
|
|
392
392
|
function taskTagDisplay(job,task){const display=task.tag&&job?.tag?.values?.[task.tag];return typeof display==='string'&&display?display:null;}
|
|
393
393
|
function taskSubtreeComplete(task,children){return ['SUCCEEDED','CANCELLED'].includes(task.state)&&(children.get(task.id)||[]).every(child=>taskSubtreeComplete(child,children));}
|
|
394
|
-
function taskNode(task,children,depth){const li=element('li');const nested=children.get(task.id)||[];const line=element('div','task-line');if(nested.length){const key=(selectedJob||'')+':'+task.id;const collapsed=collapsedTasks.has(key)||(!expandedTasks.has(key)&&taskSubtreeComplete(task,children));const fold=document.createElement('button');fold.type='button';fold.className='task-fold';fold.setAttribute('aria-expanded',String(!collapsed));fold.setAttribute('aria-label',(collapsed?'Expand ':'Collapse ')+task.id);fold.textContent=collapsed?'▸':'▾';line.append(fold);const ul=element('ul','tree');ul.hidden=collapsed;for(const child of nested)ul.append(taskNode(child,children,depth+1));fold.addEventListener('click',()=>{const next=!ul.hidden;ul.hidden=next;fold.setAttribute('aria-expanded',String(!next));fold.setAttribute('aria-label',(next?'Expand ':'Collapse ')+task.id);fold.textContent=next?'▸':'▾';if(next){collapsedTasks.add(key);expandedTasks.delete(key);}else{collapsedTasks.delete(key);expandedTasks.add(key);}});li.append(line,ul);}else li.append(line);const row=document.createElement('button');row.type='button';row.className='task-row';row.setAttribute('aria-current',String(task.id===selectedTaskId));row.title=task.id;row.addEventListener('click',()=>{const deselect=selectedTaskId===task.id;selectedTaskId=deselect?null:task.id;eventTaskId=deselect?null:task.id;rememberSelection();clearEvents();renderAll();void loadEventHistory();});const label=element('span','task-id',text(task.title||task.input?.split(/\\r?\\n/).find(line=>line.trim())?.trim()||shortManagedId(task.id,selected()?.repository,'t')));label.style.paddingLeft=Math.min(depth,4)*6+'px';row.append(label,element('span','badge '+value(task.state),text(task.state)));const tag=taskTagDisplay(selected(),task);if(tag){const badge=element('span','task-tag',text(tag));badge.title=(selected()?.tag?.description||'Task tag')+': '+task.tag;row.append(badge);}line.append(row);return li;}
|
|
394
|
+
function taskNode(task,children,depth){const li=element('li');const nested=children.get(task.id)||[];const line=element('div','task-line');if(nested.length){const key=(selectedJob||'')+':'+task.id;const collapsed=collapsedTasks.has(key)||(!expandedTasks.has(key)&&taskSubtreeComplete(task,children));const fold=document.createElement('button');fold.type='button';fold.className='task-fold';fold.setAttribute('aria-expanded',String(!collapsed));fold.setAttribute('aria-label',(collapsed?'Expand ':'Collapse ')+task.id);fold.textContent=collapsed?'▸':'▾';line.append(fold);const ul=element('ul','tree');ul.hidden=collapsed;for(const child of nested)ul.append(taskNode(child,children,depth+1));fold.addEventListener('click',()=>{const next=!ul.hidden;ul.hidden=next;fold.setAttribute('aria-expanded',String(!next));fold.setAttribute('aria-label',(next?'Expand ':'Collapse ')+task.id);fold.textContent=next?'▸':'▾';if(next){collapsedTasks.add(key);expandedTasks.delete(key);}else{collapsedTasks.delete(key);expandedTasks.add(key);}});li.append(line,ul);}else li.append(line);const row=document.createElement('button');row.type='button';row.className='task-row';row.setAttribute('aria-current',String(task.id===selectedTaskId));row.title=task.id;row.addEventListener('click',()=>{const deselect=selectedTaskId===task.id;selectedTaskId=deselect?null:task.id;eventTaskId=deselect?null:task.id;rememberSelection();clearEvents();renderAll();void loadEventHistory();});const label=element('span','task-id',text(value(task.id)+': '+value(task.title||task.input?.split(/\\r?\\n/).find(line=>line.trim())?.trim()||shortManagedId(task.id,selected()?.repository,'t'))));label.style.paddingLeft=Math.min(depth,4)*6+'px';row.append(label,element('span','badge '+value(task.state),text(task.state)));const tag=taskTagDisplay(selected(),task);if(tag){const badge=element('span','task-tag',text(tag));badge.title=(selected()?.tag?.description||'Task tag')+': '+task.tag;row.append(badge);}line.append(row);return li;}
|
|
395
395
|
function buildTree(tasks){taskTree.replaceChildren();if(!Array.isArray(tasks)||!tasks.length){taskTree.append(element('li','empty',text('No tasks in durable state.')));return;}const work=tasks.filter(task=>task.kind!=='planner');if(!work.length){taskTree.append(element('li','empty',text('No work tasks in durable state.')));return;}const known=new Set(work.map(task=>task.id));const children=new Map();const roots=[];for(const task of work){if(!task.parentTaskId||!known.has(task.parentTaskId))roots.push(task);else{const siblings=children.get(task.parentTaskId)||[];siblings.push(task);children.set(task.parentTaskId,siblings);}}for(const task of roots)taskTree.append(taskNode(task,children,0));}
|
|
396
396
|
function attemptBudget(task){const attempt=Number(task?.attempt)||0;const max=Number(task?.maxAttempts)||0;if(max>=3&&max%3===0){const effectiveMax=Math.max(max,Math.ceil(attempt/3)*3);return attempt+' / 3×'+(effectiveMax/3);}return attempt+' / '+max;}
|
|
397
397
|
const JOB_STATES=['DRAFT','ACTIVE','COMPLETED','CANCELLED'];
|
|
@@ -176,6 +176,17 @@ export class ReconcilerReviewBase extends ReconcilerRuntimeBase {
|
|
|
176
176
|
async ensureFinalReview(job, headSha) {
|
|
177
177
|
if (headSha && !(await this.projectedReviewHeadReady(job, headSha)))
|
|
178
178
|
return;
|
|
179
|
+
// Restore the review gate only after an authoritative unblock of the original reviewer.
|
|
180
|
+
const priorReviewTask = job.tasks.find(task => task.id === job.review?.taskId);
|
|
181
|
+
if (job.review?.outcome === 'blocked' && job.review.headSha === headSha
|
|
182
|
+
&& priorReviewTask?.agentName === 'reviewer' && priorReviewTask.unblock?.event === 'task.unblocked'
|
|
183
|
+
&& ['QUEUED', 'RUNNING', 'FAILED'].includes(priorReviewTask.state)) {
|
|
184
|
+
const recovered = await dispatchReviewLifecycle('blocked', 'review.retry', { reviewUnblocked: true });
|
|
185
|
+
if (recovered.accepted && recovered.target === 'PENDING') {
|
|
186
|
+
job.review = { ...job.review, outcome: 'pending', updatedAt: new Date().toISOString() };
|
|
187
|
+
await this.store.saveJob(job);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
179
190
|
let reviewTaskId = null;
|
|
180
191
|
const decision = await dispatchReviewLifecycle(job.review?.outcome, 'reconcile', this.reviewFacts(job, headSha), async (actionId) => {
|
|
181
192
|
if (actionId !== 'review.create')
|
|
@@ -30,6 +30,7 @@ export const WORKFLOW_EVENT_NAMES = [
|
|
|
30
30
|
'review.approved',
|
|
31
31
|
'review.changes_required',
|
|
32
32
|
'review.blocked',
|
|
33
|
+
'review.retry',
|
|
33
34
|
'task.blocked',
|
|
34
35
|
'task.cancelled',
|
|
35
36
|
'task.completed',
|
|
@@ -49,6 +50,7 @@ export const WORKFLOW_FACT_NAMES = [
|
|
|
49
50
|
'autoMerge',
|
|
50
51
|
'recoverableExecution',
|
|
51
52
|
'retryAvailable',
|
|
53
|
+
'reviewUnblocked',
|
|
52
54
|
'initialPlan',
|
|
53
55
|
'scheduledRun',
|
|
54
56
|
'hasUnblock',
|
|
@@ -17,9 +17,10 @@ export const reviewLifecycleV2 = new GraphRuntime(source, {
|
|
|
17
17
|
currentReviewCancelled: boolGuard('currentReviewCancelled'),
|
|
18
18
|
noApprovedCurrentReview: boolGuard('noApprovedCurrentReview'),
|
|
19
19
|
autoMerge: boolGuard('autoMerge'),
|
|
20
|
+
reviewUnblocked: boolGuard('reviewUnblocked'),
|
|
20
21
|
},
|
|
21
22
|
actions: Object.fromEntries(['review.create', 'review.cancelStale', 'review.createFix', 'review.requestMerge'].map(id => [id, { id }])),
|
|
22
|
-
events: Object.fromEntries(['reconcile', 'review.started', 'review.approved', 'review.changes_required', 'review.blocked', 'head.changed'].map(id => [id, { id }])),
|
|
23
|
+
events: Object.fromEntries(['reconcile', 'review.started', 'review.approved', 'review.changes_required', 'review.blocked', 'review.retry', 'head.changed'].map(id => [id, { id }])),
|
|
23
24
|
});
|
|
24
25
|
export function reviewGraphState(outcome) {
|
|
25
26
|
if (!outcome)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: agents-relay
|
|
3
|
-
description:
|
|
3
|
+
description: Use when work needs durable, multi-agent orchestration across turns or processes: plan and execute Jobs/Tasks, coordinate dependencies and reviews, recover failures, and track authoritative lifecycle events. Also use for content-generation workflows (research, story, production, human review, publication) that need explicit agent roles and handoff boundaries. Not for simple one-shot tasks with no durable coordination.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Agents Relay
|
|
@@ -13,6 +13,10 @@ For unified agent terminal events and role-specific results, see `contracts/even
|
|
|
13
13
|
|
|
14
14
|
Before creating, planning, reviewing, or managing a job/task, read and follow `contracts/task-job-contract.md`. This is the canonical producer/runtime protocol for story size, lifecycle events, review, and auto-merge.
|
|
15
15
|
|
|
16
|
+
## Content-generation workflow
|
|
17
|
+
|
|
18
|
+
For content-generation objectives, read `workflows/content-generation.md` before choosing agents, review boundaries, or durable Tasks. This workflow is a planning guide; the shared task/job contract remains authoritative.
|
|
19
|
+
|
|
16
20
|
## Core task contract
|
|
17
21
|
|
|
18
22
|
A managed child task is always a descriptive unit of work. It says what an agent must accomplish; it is never a shell command.
|
|
@@ -32,3 +32,5 @@ See [task-recovery-flows.md](task-recovery-flows.md) for unified architecture, o
|
|
|
32
32
|
Reviewer unavailable evidence is an execution failure (`task.failed`), not a blocked Task. Legacy Reviewer verdict `blocked` is normalized to failure and must dispatch Troubleshooter. Only Relay blocks a Troubleshooter during human escalation.
|
|
33
33
|
|
|
34
34
|
A host-mediated interactive invocation is a separate Relay-controlled wait. It requires an already-bound `interactiveInvocation` with status `awaiting_decision`. It is not permission for a Worker or Reviewer to choose a blocked terminal outcome.
|
|
35
|
+
|
|
36
|
+
A legacy Job-level `review: blocked` gate is reset to `pending` only after authoritative `task.unblocked` metadata exists on its bound Reviewer Task and the immutable review head still matches. The same Reviewer Task then resumes without creating a duplicate.
|
|
@@ -37,6 +37,7 @@ cancelStarted["action: review.cancelStale"]
|
|
|
37
37
|
cancelApproved["action: review.cancelStale"]
|
|
38
38
|
cancelChanges["action: review.cancelStale"]
|
|
39
39
|
cancelBlocked["action: review.cancelStale"]
|
|
40
|
+
reviewUnblocked["guard: reviewUnblocked"]
|
|
40
41
|
fixPending["action: review.createFix"]
|
|
41
42
|
fixStarted["action: review.createFix"]
|
|
42
43
|
fixChanges["action: review.createFix"]
|
|
@@ -117,5 +118,8 @@ approved -->|on: head.changed| cancelApproved
|
|
|
117
118
|
cancelApproved --> stale
|
|
118
119
|
changes -->|on: head.changed| cancelChanges
|
|
119
120
|
cancelChanges --> stale
|
|
121
|
+
blocked -->|on: review.retry| reviewUnblocked
|
|
122
|
+
reviewUnblocked -->|true| pending
|
|
123
|
+
reviewUnblocked -->|false| blocked
|
|
120
124
|
blocked -->|on: head.changed| cancelBlocked
|
|
121
125
|
cancelBlocked --> stale
|