agents-relay 1.0.154 → 1.0.156
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/cli-help.js +2 -2
- package/dist/cli-support.js +24 -0
- package/dist/cli.js +5 -1
- package/dist/dashboard-html.js +5 -3
- package/dist/planner-child.js +1 -1
- package/dist/planner.js +31 -4
- package/dist/reconciler.js +4 -3
- package/package.json +1 -1
- package/skills/agents-relay/agents/planner.agent.md +3 -2
- package/skills/agents-relay/contracts/task-job-contract.md +2 -2
package/dist/cli-help.js
CHANGED
|
@@ -46,7 +46,7 @@ Example:
|
|
|
46
46
|
|
|
47
47
|
Required: --repo OWNER/REPO, --id JOB_ID, and either --head BRANCH or --pr NUMBER.
|
|
48
48
|
With --head, reuse/create the PR for that head. With --pr, bind an existing unmanaged PR.
|
|
49
|
-
Common: --base BRANCH (default: main), --title TEXT, --objective-file PATH|- (preferred), --objective MARKDOWN (short/simple only), --project-path RELATIVE_PATH, --priority P0|P1|P2|P3, --mode fixed|autonomous, --no-auto-merge, --no-review. --objective and --objective-file are mutually exclusive. For a new --head Job, one is required and must contain enough standalone context to complete the Job without the originating chat/thread. Objective Markdown is also the default PR body; --body is an explicit override.
|
|
49
|
+
Common: --base BRANCH (default: main), --title TEXT, --objective-file PATH|- (preferred), --objective MARKDOWN (short/simple only), --project-path RELATIVE_PATH, --priority P0|P1|P2|P3, --mode fixed|autonomous, --no-auto-merge, --no-review, --task-status-markers JSON. --objective and --objective-file are mutually exclusive. For a new --head Job, one is required and must contain enough standalone context to complete the Job without the originating chat/thread. Objective Markdown is also the default PR body; --body is an explicit override.
|
|
50
50
|
Job creation never creates executable tasks. The orchestrator decides the initial task batch separately. Re-running create against an already healthy Job is idempotent and does not mutate its metadata.
|
|
51
51
|
|
|
52
52
|
Example:
|
|
@@ -54,7 +54,7 @@ Example:
|
|
|
54
54
|
'job update': `Partially update mutable metadata on a healthy existing Job.
|
|
55
55
|
|
|
56
56
|
Required: --repo OWNER/REPO, --pr NUMBER, and --id JOB_ID.
|
|
57
|
-
Mutable: --title, --objective-file PATH|- (preferred) or --objective MARKDOWN, --project-path RELATIVE_PATH, --priority, --mode fixed|autonomous, --schedule, --auto-merge|--no-auto-merge, --review|--no-review, and continuation options. Updating the objective also updates the PR body.
|
|
57
|
+
Mutable: --title, --objective-file PATH|- (preferred) or --objective MARKDOWN, --project-path RELATIVE_PATH, --priority, --mode fixed|autonomous, --schedule, --task-status-markers JSON, --auto-merge|--no-auto-merge, --review|--no-review, and continuation options. Updating the objective also updates the PR body.
|
|
58
58
|
Unspecified fields are preserved. Marker/binding corruption is never repaired implicitly; update fails with a precise diagnostic instead.
|
|
59
59
|
|
|
60
60
|
Example:
|
package/dist/cli-support.js
CHANGED
|
@@ -97,6 +97,30 @@ export function chatGptTabClosePolicy(args) {
|
|
|
97
97
|
return value;
|
|
98
98
|
}
|
|
99
99
|
export function requestedExecutionMode(args) { const value = arg(args, '--mode', arg(args, '--execution-mode')); return value ? executionMode(args) : undefined; }
|
|
100
|
+
export function taskStatusMarkers(args) {
|
|
101
|
+
const raw = arg(args, '--task-status-markers');
|
|
102
|
+
if (!raw)
|
|
103
|
+
return undefined;
|
|
104
|
+
let parsed;
|
|
105
|
+
try {
|
|
106
|
+
parsed = JSON.parse(raw);
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
throw new Error('--task-status-markers must be a JSON object');
|
|
110
|
+
}
|
|
111
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
112
|
+
throw new Error('--task-status-markers must be a JSON object');
|
|
113
|
+
const allowed = new Set(['DRAFT', 'QUEUED', 'RUNNING', 'BLOCKED', 'SUCCEEDED', 'FAILED', 'CANCELLED']);
|
|
114
|
+
const out = {};
|
|
115
|
+
for (const [state, marker] of Object.entries(parsed)) {
|
|
116
|
+
if (!allowed.has(state))
|
|
117
|
+
throw new Error('--task-status-markers contains invalid Task state ' + state);
|
|
118
|
+
if (typeof marker !== 'string' || marker.length === 0 || marker.length > 32)
|
|
119
|
+
throw new Error('--task-status-markers value for ' + state + ' must be a non-empty string up to 32 characters');
|
|
120
|
+
out[state] = marker;
|
|
121
|
+
}
|
|
122
|
+
return { taskStatusMarkers: out };
|
|
123
|
+
}
|
|
100
124
|
export function continuation(args) { const kind = arg(args, '--continue-kind'); const target = arg(args, '--continue-target'); return kind && target ? { kind: kind, target, input: arg(args, '--continue-input') || undefined } : null; }
|
|
101
125
|
export function scheduleFromArgs(args) {
|
|
102
126
|
const cron = arg(args, '--schedule');
|
package/dist/cli.js
CHANGED
|
@@ -25,7 +25,7 @@ import { runBlockerCommand } from './cli-blocker.js';
|
|
|
25
25
|
import { helpText, usage } from './cli-help.js';
|
|
26
26
|
import { parseAgentGraph } from './agent-graph.js';
|
|
27
27
|
export { helpText } from './cli-help.js';
|
|
28
|
-
import { SERVICE_DEFAULTS, ServiceEventBus, arg, hasArg, textOption, integerArg, repository, storeFor, saveLocal, githubStore, eventBus, publishWake, priority, storySize, executionMode, chatGptTabClosePolicy, requestedExecutionMode, continuation, scheduleFromArgs, requestManagedReconciliation, } from './cli-support.js';
|
|
28
|
+
import { SERVICE_DEFAULTS, ServiceEventBus, arg, hasArg, textOption, integerArg, repository, storeFor, saveLocal, githubStore, eventBus, publishWake, priority, storySize, executionMode, chatGptTabClosePolicy, requestedExecutionMode, continuation, scheduleFromArgs, taskStatusMarkers, requestManagedReconciliation, } from './cli-support.js';
|
|
29
29
|
export { SERVICE_DEFAULTS, eventBus, executionMode, chatGptTabClosePolicy } from './cli-support.js';
|
|
30
30
|
export async function ensureManagedGitHubJob(client, repositoryName, trustedAuthors, options) {
|
|
31
31
|
if (!options.id)
|
|
@@ -51,6 +51,7 @@ export async function ensureManagedGitHubJob(client, repositoryName, trustedAuth
|
|
|
51
51
|
description: pr.body || undefined,
|
|
52
52
|
priority: options.priority ?? 'P2',
|
|
53
53
|
executionMode: options.executionMode ?? 'fixed',
|
|
54
|
+
display: options.display,
|
|
54
55
|
autoMerge: options.autoMerge ?? true,
|
|
55
56
|
reviewEnabled: options.reviewEnabled ?? true,
|
|
56
57
|
prNumber: pr.number,
|
|
@@ -342,6 +343,7 @@ async function runJobCommand(action, args) {
|
|
|
342
343
|
priority: priority(args),
|
|
343
344
|
executionMode: requestedExecutionMode(args),
|
|
344
345
|
schedule: scheduleFromArgs(args),
|
|
346
|
+
display: taskStatusMarkers(args),
|
|
345
347
|
autoMerge: !hasArg(args, '--no-auto-merge'),
|
|
346
348
|
reviewEnabled: !hasArg(args, '--no-review'),
|
|
347
349
|
continuation: continuation(args),
|
|
@@ -406,6 +408,8 @@ async function runJobCommand(action, args) {
|
|
|
406
408
|
job.executionMode = executionMode(args, job.executionMode ?? 'fixed');
|
|
407
409
|
if (hasArg(args, '--schedule'))
|
|
408
410
|
job.schedule = scheduleFromArgs(args);
|
|
411
|
+
if (hasArg(args, '--task-status-markers'))
|
|
412
|
+
job.display = taskStatusMarkers(args);
|
|
409
413
|
if (hasArg(args, '--auto-merge') && hasArg(args, '--no-auto-merge'))
|
|
410
414
|
throw new Error('choose only one of --auto-merge or --no-auto-merge');
|
|
411
415
|
if (hasArg(args, '--auto-merge'))
|
package/dist/dashboard-html.js
CHANGED
|
@@ -284,6 +284,7 @@ function selected(){return jobs.find(job=>job.id===selectedJob)||jobs[0]||null;}
|
|
|
284
284
|
function counts(tasks){const out={};for(const task of Array.isArray(tasks)?tasks:[])out[task.state]=(out[task.state]||0)+1;return out;}
|
|
285
285
|
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';}
|
|
286
286
|
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';}
|
|
287
|
+
function cadence(job){const schedule=job?.schedule;if(!schedule?.cron)return '—';const cron=String(schedule.cron).trim();const timezone=value(schedule.timezone||'UTC');const fields=cron.split(/\\s+/);let label=cron;if(fields.length===5){const [minute,hour,day,month,weekday]=fields;if(day==='*'&&month==='*'&&weekday==='*'){const everyMinute=minute.match(/^\\*\\/(\\d+)$/);if(everyMinute&&hour==='*')label='Every '+everyMinute[1]+' min';else if(minute==='0'&&hour==='*')label='Hourly';else if(/^\\d+$/.test(minute)&&/^\\d+$/.test(hour)){const hh=String(Number(hour)).padStart(2,'0');const mm=String(Number(minute)).padStart(2,'0');label='Daily '+hh+':'+mm;}}}return label+' · '+cron+' · '+timezone;}
|
|
287
288
|
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;}
|
|
288
289
|
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;}
|
|
289
290
|
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;}
|
|
@@ -319,8 +320,9 @@ function syncFilters(){
|
|
|
319
320
|
}
|
|
320
321
|
function repoName(repository){if(typeof repository!=='string'||!repository)return 'unknown-repo';const parts=repository.split('/').filter(Boolean);return parts.at(-1)||repository;}
|
|
321
322
|
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('v'+job.publication.version)));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));}}
|
|
322
|
-
function
|
|
323
|
-
function
|
|
323
|
+
function taskStatusMarker(job,task){const marker=job?.display?.taskStatusMarkers?.[task.state];return typeof marker==='string'&&marker?marker:null;}
|
|
324
|
+
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);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);else collapsedTasks.delete(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 marker=taskStatusMarker(selected(),task);if(marker)row.append(element('span','task-status-marker',text(marker)));line.append(row);return li;}
|
|
325
|
+
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));}
|
|
324
326
|
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;}
|
|
325
327
|
const TASK_STATES=['DRAFT','QUEUED','RUNNING','BLOCKED','SUCCEEDED','FAILED','CANCELLED'];
|
|
326
328
|
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;}}
|
|
@@ -328,7 +330,7 @@ function taskStateField(task){const box=element('div','detail-group');box.append
|
|
|
328
330
|
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):'—');}
|
|
329
331
|
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);}
|
|
330
332
|
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();}
|
|
331
|
-
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();}
|
|
333
|
+
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('Cadence',cadence(job)),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();}
|
|
332
334
|
function renderAll(){jobs.sort(compareJobs);syncFilters();renderJobList();renderSelectedJob();syncMobileJobsView();}
|
|
333
335
|
function isCompleteUnmerged(job){return job.runtimeStatus==='MERGE_PENDING';}
|
|
334
336
|
function isTerminalJob(job){return ['COMPLETED','CANCELLED'].includes(job.state)||['MERGED','CLOSED'].includes(job.githubState);}
|
package/dist/planner-child.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
export function samePlannerChildDefinition(left, right) {
|
|
2
|
-
return JSON.stringify({ id: left.id, input: left.input, output: left.output, review: left.review ?? null, priority: left.priority, storySize: left.storySize ?? 'M', projectName: left.projectName, agentName: left.agentName, agentGraph: left.agentGraph, dependencies: left.dependencies, capabilities: left.capabilities, adapter: left.adapter, routing: left.routing, continuation: left.continuation ?? null, maxAttempts: left.maxAttempts, timeoutMs: left.timeoutMs }) === JSON.stringify({ id: right.id, input: right.input, output: right.output, review: right.review ?? null, priority: right.priority, storySize: right.storySize ?? 'M', projectName: right.projectName, agentName: right.agentName, agentGraph: right.agentGraph, dependencies: right.dependencies, capabilities: right.capabilities, adapter: right.adapter, routing: right.routing, continuation: right.continuation ?? null, maxAttempts: right.maxAttempts, timeoutMs: right.timeoutMs });
|
|
2
|
+
return JSON.stringify({ id: left.id, parentTaskId: left.parentTaskId, input: left.input, output: left.output, review: left.review ?? null, priority: left.priority, storySize: left.storySize ?? 'M', projectName: left.projectName, agentName: left.agentName, agentGraph: left.agentGraph, dependencies: left.dependencies, capabilities: left.capabilities, adapter: left.adapter, routing: left.routing, continuation: left.continuation ?? null, maxAttempts: left.maxAttempts, timeoutMs: left.timeoutMs }) === JSON.stringify({ id: right.id, parentTaskId: right.parentTaskId, input: right.input, output: right.output, review: right.review ?? null, priority: right.priority, storySize: right.storySize ?? 'M', projectName: right.projectName, agentName: right.agentName, agentGraph: right.agentGraph, dependencies: right.dependencies, capabilities: right.capabilities, adapter: right.adapter, routing: right.routing, continuation: right.continuation ?? null, maxAttempts: right.maxAttempts, timeoutMs: right.timeoutMs });
|
|
3
3
|
}
|
package/dist/planner.js
CHANGED
|
@@ -34,11 +34,11 @@ export function createPlannerRoundTask(job, options = {}) {
|
|
|
34
34
|
};
|
|
35
35
|
return {
|
|
36
36
|
jobId: job.id, id, kind: 'planner', storySize: 'M', agentName: 'planner',
|
|
37
|
-
parentTaskId:
|
|
37
|
+
parentTaskId: null, dependencies: [], capabilities: ['planner'], adapter: 'orchestrator',
|
|
38
38
|
input: plannerInput(context), continuation: null, continuationDeliveredAt: null, state: 'QUEUED', attempt: 0,
|
|
39
39
|
maxAttempts: options.manual ? 1 : 3, leaseOwner: null, leaseExpiresAt: null, executionId: null, threadId: null,
|
|
40
40
|
result: null, plannerResult: null, error: null, timeoutMs: options.timeoutMs ?? storySizeTimeoutMs('M'),
|
|
41
|
-
createdAt: now, updatedAt: now, planningDecisionId: id, planningSource: 'model-planner', planningRound: round,
|
|
41
|
+
createdAt: now, updatedAt: now, planningDecisionId: id, planningSource: 'model-planner', planningRound: round, previousPlannerTaskId: previousPlanner?.id ?? null,
|
|
42
42
|
plannerGuidance: options.guidance, manualPlanning: options.manual === true,
|
|
43
43
|
};
|
|
44
44
|
}
|
|
@@ -47,7 +47,7 @@ export function planningSpec(task) {
|
|
|
47
47
|
throw new Error(`Initial task ${task.id} requires an output contract`);
|
|
48
48
|
if (!['codex', 'chatgpt', 'orchestrator'].includes(task.adapter))
|
|
49
49
|
throw new Error(`Initial task ${task.id} requires a supported adapter`);
|
|
50
|
-
return { id: task.id, input: task.input, priority: task.priority, storySize: task.storySize, projectName: task.projectName, agentName: task.agentName, agentGraph: task.agentGraph, review: task.review, dependencies: task.dependencies, capabilities: task.capabilities, workerRequirements: task.workerRequirements, adapter: task.adapter, output: task.output ?? { kind: 'task_pr' }, routing: task.routing, continuation: task.continuation, maxAttempts: task.maxAttempts, timeoutMs: task.timeoutMs };
|
|
50
|
+
return { id: task.id, parentTaskId: task.parentTaskId, input: task.input, priority: task.priority, storySize: task.storySize, projectName: task.projectName, agentName: task.agentName, agentGraph: task.agentGraph, review: task.review, dependencies: task.dependencies, capabilities: task.capabilities, workerRequirements: task.workerRequirements, adapter: task.adapter, output: task.output ?? { kind: 'task_pr' }, routing: task.routing, continuation: task.continuation, maxAttempts: task.maxAttempts, timeoutMs: task.timeoutMs };
|
|
51
51
|
}
|
|
52
52
|
function record(value, field = 'result') {
|
|
53
53
|
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
@@ -189,6 +189,7 @@ export function parsePlannerResult(value) {
|
|
|
189
189
|
ids.add(id);
|
|
190
190
|
if (typeof task.input !== 'string' || task.input.length === 0)
|
|
191
191
|
throw new Error(`Planner next_tasks[${index}].input is required`);
|
|
192
|
+
const parentTaskId = task.parentTaskId === undefined || task.parentTaskId === null ? task.parentTaskId : optionalString(task.parentTaskId, `next_tasks[${index}].parentTaskId`);
|
|
192
193
|
const adapter = task.adapter;
|
|
193
194
|
if (adapter !== undefined && !['codex', 'chatgpt'].includes(String(adapter)))
|
|
194
195
|
throw new Error(`Planner next_tasks[${index}].adapter must be codex or chatgpt when provided`);
|
|
@@ -221,7 +222,7 @@ export function parsePlannerResult(value) {
|
|
|
221
222
|
if (typeof timeoutMs !== 'number' || !Number.isFinite(timeoutMs) || timeoutMs < 0)
|
|
222
223
|
throw new Error(`Planner next_tasks[${index}].timeoutMs is invalid`);
|
|
223
224
|
return {
|
|
224
|
-
id, input: task.input, priority, storySize,
|
|
225
|
+
id, parentTaskId, input: task.input, priority, storySize,
|
|
225
226
|
projectName: optionalString(task.projectName, 'projectName'), agentName: optionalString(task.agentName, 'agentName'), agentGraph, review,
|
|
226
227
|
dependencies: stringArray(task.dependencies, 'dependencies'), capabilities: stringArray(task.capabilities, 'capabilities'), workerRequirements: requirements,
|
|
227
228
|
adapter: adapter, output: output, routing: route,
|
|
@@ -230,6 +231,32 @@ export function parsePlannerResult(value) {
|
|
|
230
231
|
});
|
|
231
232
|
return { objective_status: status, assessment: root.assessment, next_tasks, unblocks };
|
|
232
233
|
}
|
|
234
|
+
export function validatePlannerHierarchy(existing, specs) {
|
|
235
|
+
const existingById = new Map(existing.map(task => [task.id, task]));
|
|
236
|
+
const specsById = new Map(specs.map(spec => [spec.id, spec]));
|
|
237
|
+
for (const spec of specs) {
|
|
238
|
+
const parent = spec.parentTaskId;
|
|
239
|
+
if (!parent)
|
|
240
|
+
continue;
|
|
241
|
+
if (parent === spec.id)
|
|
242
|
+
throw new Error(`Planner next task ${spec.id} cannot parent itself`);
|
|
243
|
+
const existingParent = existingById.get(parent);
|
|
244
|
+
if (existingParent?.kind === 'planner')
|
|
245
|
+
throw new Error(`Planner next task ${spec.id} cannot use planner round ${parent} as parentTaskId`);
|
|
246
|
+
if (!existingParent && !specsById.has(parent))
|
|
247
|
+
throw new Error(`Planner next task ${spec.id} references unknown parentTaskId ${parent}`);
|
|
248
|
+
}
|
|
249
|
+
for (const spec of specs) {
|
|
250
|
+
const seen = new Set([spec.id]);
|
|
251
|
+
let parent = spec.parentTaskId ?? null;
|
|
252
|
+
while (parent && specsById.has(parent)) {
|
|
253
|
+
if (seen.has(parent))
|
|
254
|
+
throw new Error(`Planner next_tasks parentTaskId cycle includes ${parent}`);
|
|
255
|
+
seen.add(parent);
|
|
256
|
+
parent = specsById.get(parent)?.parentTaskId ?? null;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
233
260
|
export function parsePlannerOutput(output) {
|
|
234
261
|
const trimmed = output.trim();
|
|
235
262
|
const candidate = trimmed.replace(/^```(?:json)?\s*/i, '').replace(/\s*```$/, '').trim();
|
package/dist/reconciler.js
CHANGED
|
@@ -4,7 +4,7 @@ import { dependencyReady, schedule, transitionJobEvent, transitionTaskEvent } fr
|
|
|
4
4
|
import { implementationComplete } from './runtime-status.js';
|
|
5
5
|
import { LaunchFailure, StartAckTimeout } from './adapters.js';
|
|
6
6
|
import { eventFor } from './events.js';
|
|
7
|
-
import { createPlannerRoundTask, parsePlannerResult, plannerInput } from './planner.js';
|
|
7
|
+
import { createPlannerRoundTask, parsePlannerResult, plannerInput, validatePlannerHierarchy } from './planner.js';
|
|
8
8
|
import { agentContractPrompt, executionContextPrompt, resolveExecutionContext } from './execution-context.js';
|
|
9
9
|
import { agentGraphPrompt, validateAgentSelection } from './agent-graph.js';
|
|
10
10
|
import { ReconcilerReviewBase } from './reconciler-review.js';
|
|
@@ -455,7 +455,7 @@ export class Reconciler extends ReconcilerReviewBase {
|
|
|
455
455
|
let promise;
|
|
456
456
|
try {
|
|
457
457
|
const executionContext = await this.plannerContext(job, task);
|
|
458
|
-
const context = { job, tasks: job.tasks, objective: job.objective ?? job.description ?? job.title, previousPlannerTaskId: task.
|
|
458
|
+
const context = { job, tasks: job.tasks, objective: job.objective ?? job.description ?? job.title, previousPlannerTaskId: task.previousPlannerTaskId ?? null, executionContext, retryFeedback: task.plannerRetryFeedback, guidance: task.plannerGuidance };
|
|
459
459
|
task.input = plannerInput(context);
|
|
460
460
|
await this.store.saveTask(task);
|
|
461
461
|
promise = planner.plan(context, controller.signal);
|
|
@@ -495,6 +495,7 @@ export class Reconciler extends ReconcilerReviewBase {
|
|
|
495
495
|
try {
|
|
496
496
|
const result = parsePlannerResult(raw);
|
|
497
497
|
const executionContext = await this.plannerContext(job, task);
|
|
498
|
+
validatePlannerHierarchy(job.tasks, result.next_tasks);
|
|
498
499
|
if (result.unblocks.length > 0 && !this.store.registerBlockerDependent)
|
|
499
500
|
throw new Error('Durable store does not support Planner cross-Job unblock registration');
|
|
500
501
|
for (const unblock of result.unblocks)
|
|
@@ -546,6 +547,6 @@ export class Reconciler extends ReconcilerReviewBase {
|
|
|
546
547
|
plannerChild(job, planner, spec, executionContext) {
|
|
547
548
|
validateAgentSelection(executionContext, spec.agentName, spec.agentGraph);
|
|
548
549
|
const now = new Date().toISOString();
|
|
549
|
-
return { jobId: job.id, id: spec.id, kind: 'work', priority: spec.priority ?? job.priority, storySize: spec.storySize ?? 'M', projectName: spec.projectName, agentName: spec.agentName, agentGraph: spec.agentGraph, review: spec.review, parentTaskId:
|
|
550
|
+
return { jobId: job.id, id: spec.id, kind: 'work', priority: spec.priority ?? job.priority, storySize: spec.storySize ?? 'M', projectName: spec.projectName, agentName: spec.agentName, agentGraph: spec.agentGraph, review: spec.review, parentTaskId: spec.parentTaskId ?? null, dependencies: spec.dependencies ?? [], capabilities: spec.capabilities ?? [], workerRequirements: spec.workerRequirements, adapter: 'codex', input: spec.input, output: spec.output, continuation: spec.continuation ?? null, continuationDeliveredAt: null, state: (spec.dependencies?.length ?? 0) > 0 ? 'DRAFT' : 'QUEUED', attempt: 0, maxAttempts: spec.maxAttempts ?? 3, leaseOwner: null, leaseExpiresAt: null, executionId: null, threadId: null, result: null, plannerResult: null, error: null, timeoutMs: spec.timeoutMs ?? taskOverallTimeoutMs(spec.storySize ?? 'M'), createdAt: now, updatedAt: now, planningDecisionId: planner.id, planningSource: planner.planningSource ?? 'model-planner', planningRound: planner.planningRound ?? 1 };
|
|
550
551
|
}
|
|
551
552
|
}
|
package/package.json
CHANGED
|
@@ -6,7 +6,7 @@ Before producing tasks, read and follow `../contracts/task-job-contract.md`.
|
|
|
6
6
|
|
|
7
7
|
You are a pure planning agent. Relay invokes you with the durable Job, existing Tasks, previous planner rounds, and resolved project context. You return one structured planning result. You do not call the Agents Relay CLI, persist state, create Jobs, or mutate Tasks yourself.
|
|
8
8
|
|
|
9
|
-
Relay persists each successful planner invocation as one planner round
|
|
9
|
+
Relay persists each successful planner invocation as one planner-history round. Returned work Tasks are not automatically children of that round; use `parentTaskId` only when the business Task graph needs hierarchy.
|
|
10
10
|
|
|
11
11
|
## Planning contract
|
|
12
12
|
|
|
@@ -88,7 +88,7 @@ For `objective_status: "in_progress"`, `assessment` must be Markdown containing:
|
|
|
88
88
|
|
|
89
89
|
For `objective_status: "satisfied"`, `assessment` may be a concise evidence-based completion assessment without a graph.
|
|
90
90
|
|
|
91
|
-
Each item in next_tasks contains id, input, output, and storySize. `unblocks`, when present, is an array of `{repository, prNumber, jobId, taskId, resolutionMode?}` records identifying Tasks that must remain waiting until the current Job resolves their blocker. It may also contain priority, projectName, agentName, dependencies, capabilities, workerRequirements, maxAttempts, and timeoutMs. Adapter/routing fields are selected by Relay after planning; do not invent them.
|
|
91
|
+
Each item in next_tasks contains id, input, output, and storySize. `unblocks`, when present, is an array of `{repository, prNumber, jobId, taskId, resolutionMode?}` records identifying Tasks that must remain waiting until the current Job resolves their blocker. It may also contain parentTaskId, priority, projectName, agentName, dependencies, capabilities, workerRequirements, maxAttempts, and timeoutMs. Adapter/routing fields are selected by Relay after planning; do not invent them.
|
|
92
92
|
|
|
93
93
|
### Exact `next_tasks` serialization
|
|
94
94
|
|
|
@@ -100,6 +100,7 @@ Use these exact serialized forms:
|
|
|
100
100
|
- `agentGraph`, when present, is one executable Mermaid Agent Graph **string** using `flowchart TD|TB|LR|RL`, for example `"flowchart LR\n build-log-editor --> build-log-producer"`. Do not emit `graph LR`, and do not emit an object with `nodes` or `edges`.
|
|
101
101
|
- `priority`, when present, is exactly `"P0"`, `"P1"`, `"P2"`, or `"P3"`; otherwise omit it.
|
|
102
102
|
- `storySize` is exactly `"S"`, `"M"`, or `"L"`.
|
|
103
|
+
- `parentTaskId`, when present, is the durable parent work Task ID in the current Job. Never point it at a `plan-N` planner round.
|
|
103
104
|
|
|
104
105
|
A valid non-satisfied response:
|
|
105
106
|
|
|
@@ -144,11 +144,11 @@ The public CLI resource surface is:
|
|
|
144
144
|
|
|
145
145
|
`job create` persists only durable Job state. It never creates executable work. `task create` creates a real executable Task directly under the Job; direct orchestrator-created Tasks are not wrapped in synthetic planner Tasks and have no planner-round provenance. `plan` is the explicit public operation for invoking the Planner against an existing Job; it is not generic CRUD for planner records.
|
|
146
146
|
|
|
147
|
-
Relay invokes the planner agent, validates its structured result, persists that invocation as one real `plan-N` round, then materializes `next_tasks
|
|
147
|
+
Relay invokes the planner agent, validates its structured result, persists that invocation as one real `plan-N` round, then materializes `next_tasks`. Planner rounds are planning-history records, not business-tree parents. Their history is linked by planner provenance (`previousPlannerTaskId` / planning-round metadata); business Task hierarchy is expressed only through work Task `parentTaskId`. `--prompt` / `--prompt-file` add caller guidance to the durable planning input without replacing the Job objective. The planner agent itself does not call Relay CLI or persist Tasks. Only an actual planner invocation creates a planning round. Historical `planner-N` rounds remain readable and participate in round numbering.
|
|
148
148
|
|
|
149
149
|
A zero-task autonomous Job stays idle under ordinary reconciliation, but an explicit `plan` command can start it immediately. An autonomous Job may also begin with one or more direct orchestrator-created Tasks and no Plan. After that direct batch completes, Relay may create the next real planning round through the same round-construction/execution path used by manual `plan`. Later rounds continue until the objective is satisfied. Fixed Jobs never create autonomous planner rounds automatically.
|
|
150
150
|
|
|
151
|
-
Planner-produced Tasks obey this same contract, including explicit `storySize`.
|
|
151
|
+
Planner-produced Tasks obey this same contract, including explicit `storySize`. A planner may set `parentTaskId` to another Task ID in the current Job to create durable business hierarchy. Omit it for root Tasks. Never use a `plan-N` Task as a business parent.
|
|
152
152
|
|
|
153
153
|
### Optional Review Chair Tasks
|
|
154
154
|
|