agents-relay 2.0.86 → 2.0.88
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/adapters.js +1 -1
- package/dist/dashboard-html.js +2 -2
- package/dist/planner-task-title.js +10 -0
- package/dist/planner.js +2 -1
- package/dist/reconciler.js +10 -3
- package/dist/troubleshooter.js +37 -7
- package/dist/workflow/contracts.js +1 -0
- package/dist/workflow/definitions-v2.js +1 -0
- package/package.json +1 -1
- package/skills/agents-relay/agents/planner.agent.md +4 -1
- package/workflow/task-lifecycle.mmd +14 -0
package/dist/adapters.js
CHANGED
|
@@ -274,7 +274,7 @@ async function findExistingReviewTurn(cli, sessionId, delivery, signal) {
|
|
|
274
274
|
verified_by: 'existing-user-turn', user_message_id: typeof observation.user_message_id === 'string' ? observation.user_message_id : null };
|
|
275
275
|
}
|
|
276
276
|
async function runBrowserWorker(cli, configFile, signal, action = 'temporary-submit', delivery) {
|
|
277
|
-
const session = parseWorkspaceJson(await browserWorkspaceCommand(cli, ['session', 'start'], undefined, signal));
|
|
277
|
+
const session = parseWorkspaceJson(await browserWorkspaceCommand(cli, ['session', 'start', '--workspace', CHATGPT_BROWSER_WORKSPACE], undefined, signal));
|
|
278
278
|
const sessionId = session.session_id;
|
|
279
279
|
if (typeof sessionId !== 'string' || !sessionId)
|
|
280
280
|
throw new Error('browser-workspace session start returned no session_id');
|
package/dist/dashboard-html.js
CHANGED
|
@@ -332,7 +332,7 @@ function element(name,className,child){const node=document.createElement(name);i
|
|
|
332
332
|
function yamlScalar(input){if(input===null)return 'null';if(typeof input==='boolean'||typeof input==='number')return String(input);return JSON.stringify(String(input));}
|
|
333
333
|
function yamlDump(input,indent=0){const pad=' '.repeat(indent);if(Array.isArray(input)){if(!input.length)return pad+'[]';return input.map(item=>{if(item&&typeof item==='object'){return pad+'-\\n'+yamlDump(item,indent+2);}return pad+'- '+yamlScalar(item);}).join('\\n');}if(input&&typeof input==='object'){const entries=Object.entries(input);if(!entries.length)return pad+'{}';const lines=[];for(const [key,item] of entries){if(typeof item==='string'&&item.includes('\\n')){lines.push(pad+key+': |');for(const line of item.replace(/\\n$/,'').split('\\n'))lines.push(' '.repeat(indent+2)+line);continue;}if(item&&typeof item==='object'){lines.push(pad+key+':');lines.push(yamlDump(item,indent+2));}else{lines.push(pad+key+': '+yamlScalar(item));}}return lines.join('\\n');}return pad+yamlScalar(input);}
|
|
334
334
|
function field(label,input){const box=element('div','detail-group');box.append(element('p','detail-label',text(label)),element('p','detail-value',text(input)));return box;}
|
|
335
|
-
function markdownField(label,input){const box=element('div','detail-group');box.append(element('p','detail-label',text(label)),markdownValue(input));return box;}
|
|
335
|
+
function threadField(label,threadId){const box=element('div','detail-group');box.append(element('p','detail-label',text(label)));const id=String(threadId||'').trim();if(!/^[0-9a-f]{8}-[0-9a-f-]{27,}$/i.test(id)){box.append(element('p','detail-value',text(id||'—')));return box;}const link=document.createElement('a');link.className='detail-value';link.href='https://chatgpt.com/c/'+encodeURIComponent(id);link.target='implementation';link.rel='noopener noreferrer';link.textContent=id.slice(0,8);link.title=id;box.append(link);return box;}function markdownField(label,input){const box=element('div','detail-group');box.append(element('p','detail-label',text(label)),markdownValue(input));return box;}
|
|
336
336
|
function rememberSelection(){if(selectedJob)sessionStorage.setItem('agents-relay:selected-job',selectedJob);else sessionStorage.removeItem('agents-relay:selected-job');if(selectedTaskId)sessionStorage.setItem('agents-relay:selected-task',selectedTaskId);else sessionStorage.removeItem('agents-relay:selected-task');}
|
|
337
337
|
function syncMobileJobsView(){const detail=mobileJobs.matches&&Boolean(selectedJob)&&location.hash.startsWith('#/jobs/');document.body.dataset.mobileJobsView=detail?'detail':'list';}
|
|
338
338
|
function shortManagedId(id,repository,kind){const parts=String(repository||'').split('/');if(parts.length!==2)return String(id||'');const prefix=kind+'-'+parts[0].length+'-'+parts[0]+'-'+parts[1]+'-';const value=String(id||'');const suffix=value.startsWith(prefix)?value.slice(prefix.length):'';return /^[0-9]+$/.test(suffix)?'#'+suffix:value;}function jobSelectionKey(job){return job?String(job.repository||'')+'|'+String(job.prNumber||'')+'|'+String(job.id||''):'';}
|
|
@@ -403,7 +403,7 @@ function taskStateField(task){const box=element('div','detail-group');box.append
|
|
|
403
403
|
function manualOverrideField(task){const rows=Array.isArray(task.manualStateOverrides)?task.manualStateOverrides:[];const last=rows.at(-1);return field('Manual override',last?last.from+' → '+last.to+' · '+relativeTime(last.at):'—');}
|
|
404
404
|
function renderTaskDetail(task){taskDetailRoot.replaceChildren();if(!task){taskDetailRoot.append(element('p','empty',text('Select a task to inspect execution details.')));return;}const routing=task.routing||{};const groups=element('div','detail-groups');groups.append(field('Title',task.title||'—'),field('Task ID',task.id),field('Kind',task.kind||'work'),taskStateField(task),manualOverrideField(task),field('Planning decision',task.planningDecisionId||'—'),field('Planning source',task.planningSource||'—'),field('Planning round',task.planningRound===undefined?'—':String(task.planningRound)),field('Project / agent',value(task.projectName)+' / '+value(task.agentName)),field('Parent task',task.parentTaskId||'root'),field('Adapter',task.adapter),field('Provider / model',value(routing.provider)+' / '+value(routing.model)),field('Reasoning / profile',value(routing.reasoning||routing.profile)),field('Attempt',attemptBudget(task)),field('Dependencies',Array.isArray(task.dependencies)&&task.dependencies.length?task.dependencies.join(', '):'none'),taskCommentField(task),chatField(task));const job=selected();if(task.reviewHeadSha&&job)groups.append(commitField('HEAD commit',job,task.reviewHeadSha));const graph=agentGraphField(task);if(graph)groups.append(graph);if(task.plannerResult)groups.append(field('Objective status',task.plannerResult.objective_status),markdownField('Assessment',task.plannerResult.assessment));if(task.error)groups.append(field('Error',task.error));taskDetailRoot.append(groups);}
|
|
405
405
|
function reviewStatus(job){const review=job?.review;if(!review)return 'Not started';if(review.outcome==='approved'&&review.headSha===job.headSha)return 'MERGE READY · approved';const stale=Array.isArray(job?.tasks)&&job.tasks.some(task=>task.agentName==='reviewer'&&task.reviewHeadSha&&job.headSha&&task.reviewHeadSha!==job.headSha);if(stale)return 'STALE · awaiting review';return String(review.outcome||'pending').replace('_',' ').toUpperCase();}
|
|
406
|
-
function renderSelectedJob(){jobDetail.replaceChildren();const job=selected();if(!job){selectedJobTitle.textContent='';jobDetail.append(element('div','empty',text('No durable jobs discovered.')));buildTree([]);renderTaskDetail(null);return;}selectedJobTitle.textContent=value(job.title||job.id);if(jobSelectionKey(job)!==selectedJob){selectedJob=jobSelectionKey(job);selectedTaskId=null;eventTaskId=null;clearEvents();rememberSelection();}const summary=element('div','job-summary');summary.append(field('PR state',job.githubState||'UNKNOWN'),jobStateField(job),field('Runtime status',job.runtimeStatus||'IDLE'),field('Execution mode',job.executionMode||'fixed'),field('Cadence',cadence(job)),field('Review',reviewStatus(job)),field('Creator thread',job.creatorThreadId||'—'),
|
|
406
|
+
function renderSelectedJob(){jobDetail.replaceChildren();const job=selected();if(!job){selectedJobTitle.textContent='';jobDetail.append(element('div','empty',text('No durable jobs discovered.')));buildTree([]);renderTaskDetail(null);return;}selectedJobTitle.textContent=value(job.title||job.id);if(jobSelectionKey(job)!==selectedJob){selectedJob=jobSelectionKey(job);selectedTaskId=null;eventTaskId=null;clearEvents();rememberSelection();}const summary=element('div','job-summary');summary.append(field('PR state',job.githubState||'UNKNOWN'),jobStateField(job),field('Runtime status',job.runtimeStatus||'IDLE'),field('Execution mode',job.executionMode||'fixed'),field('Cadence',cadence(job)),field('Review',reviewStatus(job)),field('Creator thread',job.creatorThreadId||'—'),threadField('Implementation thread',job.implementationThreadId),threadField('Reviewer thread',job.reviewerThreadId),commitField('Review head SHA',job,job.review?.headSha||job.headSha||''),prField(job),field('Repository',job.repository||'—'),field('Last activity',relativeTime(job.updatedAt)),field('Task states',compactCounts(job.tasks)),chatShortcutField(job));jobDetail.append(summary);const selectedTask=(job.tasks||[]).find(task=>task.id===selectedTaskId)||null;if(selectedTaskId&&!selectedTask){selectedTaskId=null;eventTaskId=null;clearEvents();rememberSelection();}buildTree(job.tasks||[]);renderTaskDetail(selectedTask);renderEventScope();}
|
|
407
407
|
function renderAll(){jobs.sort(compareJobs);syncFilters();renderJobList();renderSelectedJob();syncMobileJobsView();}
|
|
408
408
|
function isCompleteUnmerged(job){return job.runtimeStatus==='MERGE_PENDING';}
|
|
409
409
|
function isTerminalJob(job){return ['COMPLETED','CANCELLED'].includes(job.state)||['MERGED','CLOSED'].includes(job.githubState);}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Validate freshly proposed planner titles; legacy persisted Plans are not re-parsed. */
|
|
2
|
+
export function requiredPlannerTaskTitle(value, index) {
|
|
3
|
+
if (typeof value !== 'string' || !value.trim())
|
|
4
|
+
throw new Error(`Planner next_tasks[${index}].title is required`);
|
|
5
|
+
const title = value.trim();
|
|
6
|
+
const words = title.split(/\s+/);
|
|
7
|
+
if (words.length < 5 || words.length > 8)
|
|
8
|
+
throw new Error(`Planner next_tasks[${index}].title must contain 5–8 words (got ${words.length})`);
|
|
9
|
+
return title;
|
|
10
|
+
}
|
package/dist/planner.js
CHANGED
|
@@ -11,6 +11,7 @@ import { dispatchPlannerTrigger } from './workflow/planner-trigger-v2.js';
|
|
|
11
11
|
import { createTaskEntity } from './workflow/entity-creation.js';
|
|
12
12
|
import { prepareReviewerThreadDelivery, chatgptThreadRequirements } from './reviewer-thread.js';
|
|
13
13
|
import { spawnOwnedProcess } from './owned-process.js';
|
|
14
|
+
import { requiredPlannerTaskTitle } from './planner-task-title.js';
|
|
14
15
|
export function semanticTaskTree(tasks) {
|
|
15
16
|
const work = tasks.filter(task => task.kind !== 'planner');
|
|
16
17
|
const ids = new Set(work.map(task => task.id));
|
|
@@ -309,7 +310,7 @@ export function parsePlannerResult(value) {
|
|
|
309
310
|
if (typeof timeoutMs !== 'number' || !Number.isFinite(timeoutMs) || timeoutMs < 0)
|
|
310
311
|
throw new Error(`Planner next_tasks[${index}].timeoutMs is invalid`);
|
|
311
312
|
return {
|
|
312
|
-
id, title:
|
|
313
|
+
id, title: requiredPlannerTaskTitle(task.title, index), parentTaskId, input: task.input, priority, storySize,
|
|
313
314
|
projectName: optionalString(task.projectName, 'projectName'), agentName: optionalString(task.agentName, 'agentName'), agentGraph, review,
|
|
314
315
|
dependencies: stringArray(task.dependencies, 'dependencies'), capabilities: stringArray(task.capabilities, 'capabilities'), workerRequirements: requirements,
|
|
315
316
|
testInjection: parseTestInjection(task.testInjection ?? task['--test-injection']) ?? optionalString(task.testInjection ?? task['--test-injection'], `next_tasks[${index}].--test-injection`),
|
package/dist/reconciler.js
CHANGED
|
@@ -8,7 +8,7 @@ import { dependencyReady, schedule, transitionTaskEvent } from './scheduler.js';
|
|
|
8
8
|
import { applyTaskRetryAction } from './task-actions.js';
|
|
9
9
|
import { LaunchFailure, StartAckTimeout } from './adapters.js';
|
|
10
10
|
import { eventFor } from './events.js';
|
|
11
|
-
import { createPlanRound, materializePlannerChild, pendingPlan
|
|
11
|
+
import { createPlanRound, materializePlannerChild, pendingPlan } from './planner.js';
|
|
12
12
|
import { retryPendingPlannerUnblocks } from './planner-runtime.js';
|
|
13
13
|
import { dispatchPlannerOrchestration, plannerOrchestrationFacts } from './workflow/planner-orchestration-v2.js';
|
|
14
14
|
import { resolveExecutionContext } from './execution-context.js';
|
|
@@ -18,7 +18,7 @@ import { executeManualPlan } from './manual-plan.js';
|
|
|
18
18
|
import { executionEventSource, withExecutionBoundary } from './reconciler-runtime.js';
|
|
19
19
|
export { executionEventSource } from './reconciler-runtime.js';
|
|
20
20
|
import { CallbackWorkflowEffectHandler } from './workflow/effects.js';
|
|
21
|
-
import { exhaustedTroubleshooterRecoveries, pendingTroubleshooterTasks } from './troubleshooter.js';
|
|
21
|
+
import { exhaustedTroubleshooterRecoveries, pendingTroubleshooterTasks, pendingTroubleshooterReopens, refreshTroubleshooterInput } from './troubleshooter.js';
|
|
22
22
|
import { taskUsesCanonicalProjectRun } from './workspace.js';
|
|
23
23
|
import { cleanupMergedJobWorktree } from './job-worktree-cleanup.js';
|
|
24
24
|
import { jobHasActiveExecution } from './job-execution-slot.js';
|
|
@@ -106,6 +106,12 @@ export class Reconciler extends ReconcilerReviewBase {
|
|
|
106
106
|
detach: (promise, currentJobId, planId, context) => this.detach(promise, currentJobId, planId, context),
|
|
107
107
|
reconcile: currentJobId => this.reconcile(currentJobId),
|
|
108
108
|
}, jobId);
|
|
109
|
+
for (const { original, recovery } of pendingTroubleshooterReopens(await this.store.load(jobId))) {
|
|
110
|
+
await transitionTaskEvent(recovery, 'recovery.reopen', { troubleshooterRecovery: recovery.agentName === 'troubleshooter' });
|
|
111
|
+
refreshTroubleshooterInput(job, original, recovery);
|
|
112
|
+
await this.store.saveTask(recovery);
|
|
113
|
+
await this.emit(eventFor(job.id, recovery.id, original.id, 'troubleshooter.reopened', 'queued', `Reusing Troubleshooter ${recovery.id} for Task ${original.id} attempt ${original.attempt}`, 'orchestrator', { originalTaskId: original.id, attempt: original.attempt }));
|
|
114
|
+
}
|
|
109
115
|
for (const recovery of pendingTroubleshooterTasks(await this.store.load(jobId))) {
|
|
110
116
|
await this.store.appendTask(recovery);
|
|
111
117
|
await this.emit(eventFor(job.id, recovery.id, null, "troubleshooter.created", "queued", "Troubleshooter task " + recovery.id + " created for failed/blocked work", "orchestrator", { agentName: "troubleshooter" }));
|
|
@@ -269,7 +275,8 @@ export class Reconciler extends ReconcilerReviewBase {
|
|
|
269
275
|
async recoverPlannerChildren(job) {
|
|
270
276
|
for (const plan of (job.plans ?? []).filter(item => item.status === 'SUCCEEDED' && item.plannerResult)) {
|
|
271
277
|
const executionContext = await this.plannerContext(job, plan);
|
|
272
|
-
|
|
278
|
+
// Historical plans were validated on commit. Rechecking against later Task failures
|
|
279
|
+
// blocks recovery before the Troubleshooter graph can run.
|
|
273
280
|
const children = plan.plannerResult.next_tasks.map(spec => materializePlannerChild(job, plan, spec, executionContext));
|
|
274
281
|
for (const child of children) {
|
|
275
282
|
const existing = job.tasks.find(item => item.id === child.id);
|
package/dist/troubleshooter.js
CHANGED
|
@@ -5,9 +5,9 @@ function slug(value) {
|
|
|
5
5
|
return value.replace(/[^A-Za-z0-9_-]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 72) || 'task';
|
|
6
6
|
}
|
|
7
7
|
export function troubleshooterTaskId(task) {
|
|
8
|
-
return `troubleshoot-${slug(task.id)}
|
|
8
|
+
return `troubleshoot-${slug(task.id)}`;
|
|
9
9
|
}
|
|
10
|
-
function failureInput(job, task) {
|
|
10
|
+
function failureInput(job, task, recoveryId = troubleshooterTaskId(task)) {
|
|
11
11
|
return `Recover the failed/blocked task below and make it move again if technically safe.
|
|
12
12
|
|
|
13
13
|
Original repository: ${job.repository}
|
|
@@ -26,23 +26,51 @@ Retrieve this task's lifecycle event history before classifying the root cause o
|
|
|
26
26
|
GET http://127.0.0.1:8787/api/events?job=${encodeURIComponent(job.id)}&task=${encodeURIComponent(task.id)}&limit=200
|
|
27
27
|
Inspect event order, type/status, source, data/phase, duplicate events, missing expected lifecycle events, late events, and execution identifiers. If the event stream points to delivery/runtime failure, inspect the corresponding Relay/worker/tool logs too. Do not treat the final task.error string as the complete root cause.
|
|
28
28
|
|
|
29
|
-
Follow the selected Troubleshooter agent contract. Diagnose root cause from durable task state + event history + relevant runtime logs before retry. The final task.error string alone is never sufficient. Classify the evidence as model/provider/runtime, worker/tool/infrastructure, semantic task failure, or timeout/unknown. Select exactly one graph-enabled resolution path: exact_original_retry, same_job_dependency, blocker_job_dependency (with merge or verified mode), escalation_gate, or cancel_supersede. Only model/provider/runtime evidence may call the narrow model re-decision action; that action gathers fresh usage, reloads current policy, updates this exact Task's routing, and does not retry it. Troubleshooter must then reactivate/retry this exact original task and verify progress. Use the dedicated agents-relay task recover path with --task-id ${task.id} and --recovery-task-id ${
|
|
29
|
+
Follow the selected Troubleshooter agent contract. Diagnose root cause from durable task state + event history + relevant runtime logs before retry. The final task.error string alone is never sufficient. Classify the evidence as model/provider/runtime, worker/tool/infrastructure, semantic task failure, or timeout/unknown. Select exactly one graph-enabled resolution path: exact_original_retry, same_job_dependency, blocker_job_dependency (with merge or verified mode), escalation_gate, or cancel_supersede. Only model/provider/runtime evidence may call the narrow model re-decision action; that action gathers fresh usage, reloads current policy, updates this exact Task's routing, and does not retry it. Troubleshooter must then reactivate/retry this exact original task and verify progress. Use the dedicated agents-relay task recover path with --task-id ${task.id} and --recovery-task-id ${recoveryId}; never use ordinary task retry from a Troubleshooter because manual retry intentionally supersedes active recovery. Do not manually choose a provider/model or create a replacement task; no replacement_task action exists. If the blocker is a shared worker/runtime/tool defect, create and directly complete a P0/self-job in the owning repository, including required release/deploy verification. If diagnosis suggests a genuine human/external blocker, do not publish task.blocked directly and do not implement the result branch procedurally. Call agents-relay escalation with the original intent, blocker, and relevant context. Pass any caller-defined notification plan through --on-needs-user/--on-needs-user-file; do not hard-code a channel. The canonical escalation graph invokes Neo escalation and owns the resolved/failed/needs_user branch; follow its selected outcome. Retry this exact original task only after the blocker is removed, reconcile the original job, and verify durable progress.`;
|
|
30
30
|
}
|
|
31
31
|
function belongsToCurrentRun(job, task) {
|
|
32
32
|
const slot = job.schedule?.currentRunSlot;
|
|
33
33
|
return !slot || task.createdAt >= slot;
|
|
34
34
|
}
|
|
35
|
-
const ACTIVE_RECOVERY_STATES = new Set(['DRAFT', 'QUEUED', 'RUNNING', 'BLOCKED']);
|
|
35
|
+
const ACTIVE_RECOVERY_STATES = new Set(['DRAFT', 'QUEUED', 'RUNNING', 'BLOCKED', 'UNBLOCKED']);
|
|
36
36
|
function recoveryTargetsTask(recovery, original) {
|
|
37
37
|
if (recovery.agentName !== 'troubleshooter')
|
|
38
38
|
return false;
|
|
39
39
|
if (recovery.parentTaskId)
|
|
40
40
|
return recovery.parentTaskId === original.id;
|
|
41
|
-
return recovery.id.startsWith(`troubleshoot-${slug(original.id)}-a`);
|
|
41
|
+
return recovery.id === troubleshooterTaskId(original) || recovery.id.startsWith(`troubleshoot-${slug(original.id)}-a`);
|
|
42
42
|
}
|
|
43
43
|
function hasRecoveryBarrier(job, original) {
|
|
44
|
-
|
|
45
|
-
|
|
44
|
+
// Exactly one durable recovery identity per original Task, across all failures.
|
|
45
|
+
return job.tasks.some(task => recoveryTargetsTask(task, original));
|
|
46
|
+
}
|
|
47
|
+
function recoveryEpisode(recovery) {
|
|
48
|
+
return recovery.recoveryForAttempt ?? Number(/-a(\d+)$/.exec(recovery.id)?.[1] ?? -1);
|
|
49
|
+
}
|
|
50
|
+
/** Reuse the existing recovery rather than creating one child per worker retry. */
|
|
51
|
+
export function pendingTroubleshooterReopens(job) {
|
|
52
|
+
const pending = [];
|
|
53
|
+
for (const original of job.tasks) {
|
|
54
|
+
if (original.kind === 'planner' || original.agentName === 'troubleshooter' || original.state !== 'FAILED')
|
|
55
|
+
continue;
|
|
56
|
+
if (!belongsToCurrentRun(job, original))
|
|
57
|
+
continue;
|
|
58
|
+
const recoveries = job.tasks.filter(candidate => recoveryTargetsTask(candidate, original));
|
|
59
|
+
if (!recoveries.length || recoveries.some(candidate => ACTIVE_RECOVERY_STATES.has(candidate.state)))
|
|
60
|
+
continue;
|
|
61
|
+
const recovery = recoveries.sort((a, b) => recoveryEpisode(b) - recoveryEpisode(a))[0];
|
|
62
|
+
if (!['SUCCEEDED', 'CANCELLED'].includes(recovery.state) || recoveryEpisode(recovery) >= original.attempt)
|
|
63
|
+
continue;
|
|
64
|
+
pending.push({ original, recovery });
|
|
65
|
+
}
|
|
66
|
+
return pending;
|
|
67
|
+
}
|
|
68
|
+
export function refreshTroubleshooterInput(job, original, recovery) {
|
|
69
|
+
recovery.input = failureInput(job, original, recovery.id);
|
|
70
|
+
recovery.recoveryForAttempt = original.attempt;
|
|
71
|
+
recovery.maxAttempts = Math.max(recovery.maxAttempts, recovery.attempt + 3);
|
|
72
|
+
recovery.routing = undefined;
|
|
73
|
+
recovery.workerSelection = undefined;
|
|
46
74
|
}
|
|
47
75
|
export function pendingTroubleshooterTasks(job) {
|
|
48
76
|
const existing = new Set(job.tasks.filter(task => task.agentName === 'troubleshooter').map(task => task.id));
|
|
@@ -64,6 +92,8 @@ export function pendingTroubleshooterTasks(job) {
|
|
|
64
92
|
recoveries.push(createTaskEntity(job, {
|
|
65
93
|
jobId: job.id,
|
|
66
94
|
id,
|
|
95
|
+
title: `Diagnose and recover failed task ${task.id}`,
|
|
96
|
+
recoveryForAttempt: task.attempt,
|
|
67
97
|
kind: 'work',
|
|
68
98
|
priority: 'P0',
|
|
69
99
|
storySize: 'M',
|
|
@@ -25,6 +25,7 @@ export const taskLifecycleV2 = load('task-lifecycle', TASK_STATES, {
|
|
|
25
25
|
runnable: boolGuard('runnable'),
|
|
26
26
|
dispatchable: boolGuard('dispatchable'),
|
|
27
27
|
recoverableExecution: boolGuard('recoverableExecution'),
|
|
28
|
+
troubleshooterRecovery: boolGuard('troubleshooterRecovery'),
|
|
28
29
|
retryAvailable: boolGuard('retryAvailable'),
|
|
29
30
|
});
|
|
30
31
|
export const jobLifecycleV2 = load('job-lifecycle', JOB_STATES, {
|
package/package.json
CHANGED
|
@@ -114,7 +114,9 @@ For `objective_status: "in_progress"`, `assessment` must be Markdown containing:
|
|
|
114
114
|
|
|
115
115
|
For `objective_status: "satisfied"`, `assessment` may be a concise evidence-based completion assessment without a graph.
|
|
116
116
|
|
|
117
|
-
Each item in next_tasks
|
|
117
|
+
Each item in `next_tasks` MUST include a concise, meaningful `title` of **5–8 words** (separate from the detailed `input`). Do not use the input text as a title.
|
|
118
|
+
|
|
119
|
+
Each item in `next_tasks` contains **required `title`**, id, input, output, and storySize. The planner parser rejects missing titles and titles outside 5–8 whitespace-separated words. `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.
|
|
118
120
|
|
|
119
121
|
### Exact `next_tasks` serialization
|
|
120
122
|
|
|
@@ -138,6 +140,7 @@ A valid non-satisfied response:
|
|
|
138
140
|
"next_tasks": [
|
|
139
141
|
{
|
|
140
142
|
"id": "episode-2026-09-26",
|
|
143
|
+
"title": "Produce and verify daily content package",
|
|
141
144
|
"input": "Produce and verify the dated content package.",
|
|
142
145
|
"output": {"kind": "file", "path": "runs/2026-09-26/package.md"},
|
|
143
146
|
"storySize": "L",
|
|
@@ -18,6 +18,11 @@ recoverComplete["guard: recoverableExecution"]
|
|
|
18
18
|
recoverBlocked["guard: recoverableExecution"]
|
|
19
19
|
recoverFailed["guard: recoverableExecution"]
|
|
20
20
|
recoverCancelled["guard: recoverableExecution"]
|
|
21
|
+
troubleshooterRecovery["guard: troubleshooterRecovery"]
|
|
22
|
+
recoveryReopenClearError["action: mutation.clearError"]
|
|
23
|
+
recoveryReopenClearReason["action: mutation.clearFailureReason"]
|
|
24
|
+
recoveryReopenClearLease["action: mutation.clearLease"]
|
|
25
|
+
recoveryReopenClearExecution["action: mutation.clearExecution"]
|
|
21
26
|
|
|
22
27
|
retryAvailableRunning["guard: retryAvailable"]
|
|
23
28
|
plannerRetryAvailable["guard: retryAvailable"]
|
|
@@ -200,3 +205,12 @@ recoverFailed -->|true| failed
|
|
|
200
205
|
failed -->|on: task.cancelled| recoverCancelled
|
|
201
206
|
recoverCancelled -->|true| cancelled
|
|
202
207
|
failed -->|on: cancel| cancelled
|
|
208
|
+
|
|
209
|
+
%% Only a persistent Troubleshooter may reopen a terminal recovery for a new failure episode.
|
|
210
|
+
succeeded -->|on: recovery.reopen| troubleshooterRecovery
|
|
211
|
+
cancelled -->|on: recovery.reopen| troubleshooterRecovery
|
|
212
|
+
troubleshooterRecovery -->|true| recoveryReopenClearError
|
|
213
|
+
recoveryReopenClearError --> recoveryReopenClearReason
|
|
214
|
+
recoveryReopenClearReason --> recoveryReopenClearLease
|
|
215
|
+
recoveryReopenClearLease --> recoveryReopenClearExecution
|
|
216
|
+
recoveryReopenClearExecution --> queued
|