agents-relay 1.0.140 → 1.0.142
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/planner-child.js +3 -0
- package/dist/planner.js +24 -2
- package/dist/reconciler-runtime.js +11 -0
- package/dist/reconciler.js +7 -5
- package/dist/review-chair.js +134 -0
- package/dist/store-task-definition.js +1 -1
- package/package.json +1 -1
- package/skills/agents-relay/agents/planner.agent.md +1 -0
- package/skills/agents-relay/agents/review-chair.agent.md +49 -0
- package/skills/agents-relay/contracts/task-job-contract.md +13 -0
|
@@ -0,0 +1,3 @@
|
|
|
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 });
|
|
3
|
+
}
|
package/dist/planner.js
CHANGED
|
@@ -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, 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, 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))
|
|
@@ -88,6 +88,27 @@ function workerRequirements(value, field) {
|
|
|
88
88
|
}
|
|
89
89
|
return { capabilities, runtime: optional('runtime'), taskKind: optional('taskKind'), trustDomain: optional('trustDomain'), region: optional('region'), labels };
|
|
90
90
|
}
|
|
91
|
+
function reviewMetadata(value, field) {
|
|
92
|
+
if (value === undefined || value === null)
|
|
93
|
+
return undefined;
|
|
94
|
+
const root = record(value, field);
|
|
95
|
+
if (root.role !== 'chair' && root.role !== 'specialist')
|
|
96
|
+
throw new Error(`Planner ${field}.role must be chair or specialist`);
|
|
97
|
+
const targetValue = record(root.target, `${field}.target`);
|
|
98
|
+
const artifact = optionalString(targetValue.artifact, `${field}.target.artifact`);
|
|
99
|
+
const version = optionalString(targetValue.version, `${field}.target.version`);
|
|
100
|
+
if (!artifact || !version)
|
|
101
|
+
throw new Error(`Planner ${field}.target requires artifact and version`);
|
|
102
|
+
const dimension = optionalString(root.dimension, `${field}.dimension`);
|
|
103
|
+
const selectionReason = optionalString(root.selectionReason, `${field}.selectionReason`);
|
|
104
|
+
if (root.role === 'specialist' && (!dimension || !selectionReason)) {
|
|
105
|
+
throw new Error(`Planner ${field} specialist tasks require dimension and selectionReason`);
|
|
106
|
+
}
|
|
107
|
+
const round = root.round === undefined ? undefined : root.round;
|
|
108
|
+
if (round !== undefined && (!Number.isInteger(round) || Number(round) < 1))
|
|
109
|
+
throw new Error(`Planner ${field}.round must be a positive integer`);
|
|
110
|
+
return { role: root.role, target: { artifact, version }, dimension, selectionReason, round: round === undefined ? undefined : Number(round) };
|
|
111
|
+
}
|
|
91
112
|
function rawValue(value) {
|
|
92
113
|
if (typeof value === 'string')
|
|
93
114
|
return JSON.stringify(value);
|
|
@@ -185,6 +206,7 @@ export function parsePlannerResult(value) {
|
|
|
185
206
|
const route = routing(task.routing, 'next_tasks[' + index + '].routing');
|
|
186
207
|
const requirements = workerRequirements(task.workerRequirements ?? task.requirements, 'next_tasks[' + index + '].workerRequirements');
|
|
187
208
|
const agentGraph = optionalString(task.agentGraph, `next_tasks[${index}].agentGraph`);
|
|
209
|
+
const review = reviewMetadata(task.review, `next_tasks[${index}].review`);
|
|
188
210
|
if (agentGraph) {
|
|
189
211
|
try {
|
|
190
212
|
parseAgentGraph(agentGraph);
|
|
@@ -200,7 +222,7 @@ export function parsePlannerResult(value) {
|
|
|
200
222
|
throw new Error(`Planner next_tasks[${index}].timeoutMs is invalid`);
|
|
201
223
|
return {
|
|
202
224
|
id, input: task.input, priority, storySize,
|
|
203
|
-
projectName: optionalString(task.projectName, 'projectName'), agentName: optionalString(task.agentName, 'agentName'), agentGraph,
|
|
225
|
+
projectName: optionalString(task.projectName, 'projectName'), agentName: optionalString(task.agentName, 'agentName'), agentGraph, review,
|
|
204
226
|
dependencies: stringArray(task.dependencies, 'dependencies'), capabilities: stringArray(task.capabilities, 'capabilities'), workerRequirements: requirements,
|
|
205
227
|
adapter: adapter, output: output, routing: route,
|
|
206
228
|
continuation: task.continuation, maxAttempts, timeoutMs
|
|
@@ -153,6 +153,15 @@ export class ReconcilerRuntimeBase {
|
|
|
153
153
|
registerExecutionOwner(jobId, taskId, executionId) {
|
|
154
154
|
activeExecutionOwners.set(executionEventSource(jobId, taskId, executionId), this);
|
|
155
155
|
}
|
|
156
|
+
clearTerminalTimer(taskId, executionId) {
|
|
157
|
+
const live = this.live.get(taskId);
|
|
158
|
+
if (!live || live.executionId !== executionId)
|
|
159
|
+
return;
|
|
160
|
+
const timer = this.terminalTimers.get(taskId);
|
|
161
|
+
if (timer)
|
|
162
|
+
clearTimeout(timer);
|
|
163
|
+
this.terminalTimers.delete(taskId);
|
|
164
|
+
}
|
|
156
165
|
clearExecution(taskId, executionId) {
|
|
157
166
|
const live = this.live.get(taskId);
|
|
158
167
|
if (!executionId || live?.executionId === executionId) {
|
|
@@ -243,6 +252,8 @@ export class ReconcilerRuntimeBase {
|
|
|
243
252
|
const task = job.tasks.find(item => item.id === taskId);
|
|
244
253
|
if (!task || task.executionId !== executionId)
|
|
245
254
|
return;
|
|
255
|
+
if (task.adapter !== 'chatgpt')
|
|
256
|
+
this.clearTerminalTimer(taskId, executionId);
|
|
246
257
|
await this.emit(eventFor(jobId, taskId, task.parentTaskId, task.adapter === 'chatgpt' ? 'task.process.async_exited' : 'task.process.exited', error ? 'failed' : 'succeeded', error ?? result?.summary ?? 'Worker runtime exited', 'orchestrator', { authoritative: false, waitingForTerminalEvent: true, processError: error }, { agent: 'relay', component: 'agents-relay' }));
|
|
247
258
|
}
|
|
248
259
|
async timeoutTask(jobId, taskId, executionId) {
|
package/dist/reconciler.js
CHANGED
|
@@ -15,6 +15,8 @@ import { CallbackWorkflowEffectHandler } from './workflow/effects.js';
|
|
|
15
15
|
import { applyHistoricalJobCompatibilityRepair } from './workflow/runtime.js';
|
|
16
16
|
import { pendingTroubleshooterTasks } from './troubleshooter.js';
|
|
17
17
|
import { prepareManagedExecutionWorktree } from './workspace.js';
|
|
18
|
+
import { createReviewChairExecutionPlan } from './review-chair.js';
|
|
19
|
+
import { samePlannerChildDefinition } from './planner-child.js';
|
|
18
20
|
const PRIORITY_RANK = { P0: 0, P1: 1, P2: 2, P3: 3 };
|
|
19
21
|
export function compareTaskPriority(a, b, jobPriority = 'P2') { const ar = PRIORITY_RANK[a.priority ?? jobPriority]; const br = PRIORITY_RANK[b.priority ?? jobPriority]; return ar - br || a.createdAt.localeCompare(b.createdAt) || a.id.localeCompare(b.id); }
|
|
20
22
|
export async function managedWorkerInput(job, task, context) {
|
|
@@ -22,6 +24,9 @@ export async function managedWorkerInput(job, task, context) {
|
|
|
22
24
|
return task.input;
|
|
23
25
|
const selectedAgent = task.agentName && context ? await agentContractPrompt(context, task.agentName) : '';
|
|
24
26
|
const selectedGraph = task.agentGraph && context ? await agentGraphPrompt(context, task.agentGraph) : '';
|
|
27
|
+
const chairPlan = task.review?.role === 'chair' && context
|
|
28
|
+
? createReviewChairExecutionPlan(context.agents, task.review.target)
|
|
29
|
+
: null;
|
|
25
30
|
if (!task.output)
|
|
26
31
|
throw new Error(`Task ${task.id} requires an explicit output contract`);
|
|
27
32
|
if (!task.executionId)
|
|
@@ -41,7 +46,7 @@ Project: ${task.projectName ?? context?.project.name ?? 'unspecified'}
|
|
|
41
46
|
|
|
42
47
|
Retrieve the durable job and task from GitHub before doing the work. Use whatever GitHub access this environment provides: the URLs, a GitHub integration/tool, or GitHub CLI/API. GitHub is the source of truth for task intent and declared output.
|
|
43
48
|
|
|
44
|
-
${context ? executionContextPrompt(context) + '\n\n' : ''}${selectedAgent ? selectedAgent + '\n\n' : ''}${selectedGraph ? selectedGraph + '\n\n' : ''}Follow the resolved project instructions and Agents Relay/events-bus worker contracts.
|
|
49
|
+
${context ? executionContextPrompt(context) + '\n\n' : ''}${selectedAgent ? selectedAgent + '\n\n' : ''}${selectedGraph ? selectedGraph + '\n\n' : ''}${chairPlan ? `[Review Chair execution plan]\n${JSON.stringify(chairPlan, null, 2)}\n\nUse this runtime-discovered panel to create sibling durable specialist Tasks. Preserve each specialistTasks entry as review metadata, keep the target immutable, and synthesize structured results with synthesizeReview semantics before returning the unified revision plan.\n\n` : ''}Follow the resolved project instructions and Agents Relay/events-bus worker contracts.
|
|
45
50
|
|
|
46
51
|
[Output contract]
|
|
47
52
|
${output}
|
|
@@ -542,9 +547,6 @@ export class Reconciler extends ReconcilerReviewBase {
|
|
|
542
547
|
plannerChild(job, planner, spec, executionContext) {
|
|
543
548
|
validateAgentSelection(executionContext, spec.agentName, spec.agentGraph);
|
|
544
549
|
const now = new Date().toISOString();
|
|
545
|
-
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, parentTaskId: planner.id, 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
|
+
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: planner.id, 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 };
|
|
546
551
|
}
|
|
547
552
|
}
|
|
548
|
-
function samePlannerChildDefinition(left, right) {
|
|
549
|
-
return JSON.stringify({ id: left.id, input: left.input, output: left.output, 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, 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 });
|
|
550
|
-
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
function text(value, field) {
|
|
2
|
+
if (typeof value !== 'string' || value.trim().length === 0)
|
|
3
|
+
throw new Error(`Review ${field} must be a non-empty string`);
|
|
4
|
+
return value.trim();
|
|
5
|
+
}
|
|
6
|
+
function target(value) {
|
|
7
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
8
|
+
throw new Error('Review target must be an object');
|
|
9
|
+
const record = value;
|
|
10
|
+
return { artifact: text(record.artifact, 'target.artifact'), version: text(record.version, 'target.version') };
|
|
11
|
+
}
|
|
12
|
+
function severity(value) {
|
|
13
|
+
if (value === undefined || value === null)
|
|
14
|
+
return undefined;
|
|
15
|
+
if (typeof value !== 'string' || !/^P[0-3]$/.test(value))
|
|
16
|
+
throw new Error('Review finding severity must be P0, P1, P2, or P3');
|
|
17
|
+
return value;
|
|
18
|
+
}
|
|
19
|
+
export function parseReviewFinding(value, index = 0) {
|
|
20
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
21
|
+
throw new Error(`Review finding ${index} must be an object`);
|
|
22
|
+
const record = value;
|
|
23
|
+
const verdict = record.verdict;
|
|
24
|
+
if (verdict !== 'pass' && verdict !== 'concern' && verdict !== 'fail' && verdict !== 'blocked')
|
|
25
|
+
throw new Error(`Review finding ${index}.verdict is invalid`);
|
|
26
|
+
return {
|
|
27
|
+
id: record.id === undefined ? undefined : text(record.id, `finding ${index}.id`),
|
|
28
|
+
reviewer: text(record.reviewer, `finding ${index}.reviewer`),
|
|
29
|
+
dimension: text(record.dimension, `finding ${index}.dimension`),
|
|
30
|
+
verdict,
|
|
31
|
+
summary: text(record.summary, `finding ${index}.summary`),
|
|
32
|
+
severity: severity(record.severity),
|
|
33
|
+
location: record.location === undefined ? undefined : text(record.location, `finding ${index}.location`),
|
|
34
|
+
recommendation: record.recommendation === undefined ? undefined : text(record.recommendation, `finding ${index}.recommendation`),
|
|
35
|
+
evidence: record.evidence === undefined ? undefined : text(record.evidence, `finding ${index}.evidence`),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
export function parseSpecialistReview(value) {
|
|
39
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
40
|
+
throw new Error('Specialist review must be an object');
|
|
41
|
+
const record = value;
|
|
42
|
+
const verdict = record.verdict;
|
|
43
|
+
if (verdict !== 'pass' && verdict !== 'concern' && verdict !== 'fail' && verdict !== 'blocked')
|
|
44
|
+
throw new Error('Specialist review verdict is invalid');
|
|
45
|
+
if (!Array.isArray(record.findings))
|
|
46
|
+
throw new Error('Specialist review findings must be an array');
|
|
47
|
+
const parsedTarget = target(record.target);
|
|
48
|
+
const findings = record.findings.map((item, index) => parseReviewFinding(item, index));
|
|
49
|
+
for (const finding of findings) {
|
|
50
|
+
if (finding.reviewer !== text(record.reviewer, 'reviewer') || finding.dimension !== text(record.dimension, 'dimension')) {
|
|
51
|
+
throw new Error('Specialist review findings must preserve reviewer and dimension attribution');
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return { reviewer: text(record.reviewer, 'reviewer'), dimension: text(record.dimension, 'dimension'), target: parsedTarget, verdict, findings };
|
|
55
|
+
}
|
|
56
|
+
function candidateFromAgent(agent) {
|
|
57
|
+
return { agent: agent.name, dimensions: [...agent.capabilities], description: agent.description };
|
|
58
|
+
}
|
|
59
|
+
/** Selects roles from the resolved visible catalog; it never selects workers or models. */
|
|
60
|
+
export function selectReviewPanel(candidates, dimensions, max = 5) {
|
|
61
|
+
if (!Number.isInteger(max) || max < 1)
|
|
62
|
+
throw new Error('Review panel max must be a positive integer');
|
|
63
|
+
const normalized = candidates.map(candidate => 'name' in candidate ? candidateFromAgent(candidate) : candidate);
|
|
64
|
+
const selected = [];
|
|
65
|
+
const usedAgents = new Set();
|
|
66
|
+
for (const dimension of [...dimensions].map((item, index) => ({ item, index })).sort((a, b) => (a.item.priority ?? 99) - (b.item.priority ?? 99) || a.index - b.index).map(item => item.item)) {
|
|
67
|
+
const match = normalized
|
|
68
|
+
.filter(candidate => !usedAgents.has(candidate.agent))
|
|
69
|
+
.map(candidate => ({ candidate, score: candidate.dimensions.some(item => item.toLowerCase() === dimension.name.toLowerCase()) ? 0 : 1 }))
|
|
70
|
+
.sort((a, b) => a.score - b.score)[0];
|
|
71
|
+
if (!match || match.score > 0)
|
|
72
|
+
continue;
|
|
73
|
+
selected.push({ agent: match.candidate.agent, dimension: dimension.name, reason: `Visible agent capability matches the ${dimension.name} review dimension${match.candidate.description ? `: ${match.candidate.description}` : ''}.` });
|
|
74
|
+
usedAgents.add(match.candidate.agent);
|
|
75
|
+
if (selected.length >= max)
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
return selected;
|
|
79
|
+
}
|
|
80
|
+
export function specialistReviewTaskMetadata(targetValue, member, round = 1) {
|
|
81
|
+
return { role: 'specialist', target: { ...targetValue }, dimension: member.dimension, selectionReason: member.reason, round };
|
|
82
|
+
}
|
|
83
|
+
export function createSpecialistReviewTasks(tasks, targetValue, panel, round = 1) {
|
|
84
|
+
const existing = new Set(tasks.filter(task => task.review?.role === 'specialist').map(task => `${task.review?.target.artifact}@${task.review?.target.version}:${task.review?.dimension}:${task.agentName ?? ''}`));
|
|
85
|
+
return panel
|
|
86
|
+
.filter(member => !existing.has(`${targetValue.artifact}@${targetValue.version}:${member.dimension}:${member.agent}`))
|
|
87
|
+
.map(member => specialistReviewTaskMetadata(targetValue, member, round));
|
|
88
|
+
}
|
|
89
|
+
/** Builds the durable-task inputs used by the live Review Chair worker path. */
|
|
90
|
+
export function createReviewChairExecutionPlan(agents, targetValue, max = 5) {
|
|
91
|
+
const dimensions = [...new Set(agents.flatMap(agent => agent.capabilities))]
|
|
92
|
+
.map(name => ({ name }))
|
|
93
|
+
.filter(dimension => dimension.name !== 'model');
|
|
94
|
+
const panel = selectReviewPanel(agents, dimensions, max);
|
|
95
|
+
return { target: { ...targetValue }, panel, specialistTasks: createSpecialistReviewTasks([], targetValue, panel) };
|
|
96
|
+
}
|
|
97
|
+
function findingKey(finding) { return `${finding.location ?? ''}|${finding.summary.toLowerCase().replace(/\s+/g, ' ').trim()}`; }
|
|
98
|
+
function priority(finding) { return finding.severity ?? (finding.verdict === 'blocked' ? 'P0' : finding.verdict === 'fail' ? 'P1' : 'P2'); }
|
|
99
|
+
/** Deterministically merges independent specialist output into one executor-facing plan. */
|
|
100
|
+
export function synthesizeReview(targetValue, panel, reviews) {
|
|
101
|
+
const mismatched = reviews.find(review => review.target.artifact !== targetValue.artifact || review.target.version !== targetValue.version);
|
|
102
|
+
if (mismatched)
|
|
103
|
+
throw new Error(`Specialist review ${mismatched.reviewer} targets ${mismatched.target.artifact}@${mismatched.target.version}, expected ${targetValue.artifact}@${targetValue.version}`);
|
|
104
|
+
const byKey = new Map();
|
|
105
|
+
for (const review of reviews)
|
|
106
|
+
for (const finding of review.findings) {
|
|
107
|
+
const key = findingKey(finding);
|
|
108
|
+
const group = byKey.get(key) ?? [];
|
|
109
|
+
group.push(finding);
|
|
110
|
+
byKey.set(key, group);
|
|
111
|
+
}
|
|
112
|
+
const usedIds = new Set();
|
|
113
|
+
const mergedGroups = [...byKey.values()].map((group, index) => {
|
|
114
|
+
const preferredId = group[0].id ?? `finding-${index + 1}`;
|
|
115
|
+
let id = preferredId;
|
|
116
|
+
let suffix = 2;
|
|
117
|
+
while (usedIds.has(id))
|
|
118
|
+
id = `${preferredId}-${suffix++}`;
|
|
119
|
+
usedIds.add(id);
|
|
120
|
+
return { group, id };
|
|
121
|
+
});
|
|
122
|
+
const findings = mergedGroups.map(({ group, id }) => ({
|
|
123
|
+
...group[0],
|
|
124
|
+
id,
|
|
125
|
+
attributions: group.map(item => ({ reviewer: item.reviewer, dimension: item.dimension, ...(item.id ? { findingId: item.id } : {}) })),
|
|
126
|
+
}));
|
|
127
|
+
const conflicts = mergedGroups.filter(({ group }) => new Set(group.map(item => item.verdict)).size > 1).map(({ group, id }) => ({ findingIds: [id], resolution: `Prioritize the more severe disposition and preserve all specialist attribution: ${group.map(item => `${item.reviewer}=${item.verdict}`).join(', ')}.` }));
|
|
128
|
+
const actions = findings.filter(finding => finding.verdict !== 'pass').map(finding => ({ priority: priority(finding), action: finding.recommendation ?? `Address: ${finding.summary}`, findingIds: [finding.id ?? findingKey(finding)], rationale: finding.evidence ?? `Raised by ${finding.reviewer} for ${finding.dimension}.` })).sort((a, b) => a.priority.localeCompare(b.priority) || a.action.localeCompare(b.action));
|
|
129
|
+
const covered = new Set(reviews.map(review => review.dimension));
|
|
130
|
+
const missingDimensions = panel.map(member => member.dimension).filter(dimension => !covered.has(dimension));
|
|
131
|
+
const unresolved = findings.filter(finding => finding.verdict === 'blocked');
|
|
132
|
+
return { target: { ...targetValue }, panel: panel.map(member => ({ ...member })), findings, conflicts, actions, unresolved, needsAdditionalSpecialist: missingDimensions.length > 0, missingDimensions };
|
|
133
|
+
}
|
|
134
|
+
function findingsIndex(group) { return group.map(item => item.reviewer).sort().join('-').toLowerCase().replace(/[^a-z0-9-]+/g, '-') || 'unknown'; }
|
|
@@ -6,7 +6,7 @@ function immutableTaskDefinition(task) {
|
|
|
6
6
|
return {
|
|
7
7
|
jobId: task.jobId, id: task.id, priority: task.priority, storySize: task.storySize ?? 'M', projectName: task.projectName, agentName: task.agentName, agentGraph: task.agentGraph,
|
|
8
8
|
kind: task.kind ?? 'work', planningDecisionId: task.planningDecisionId, planningSource: task.planningSource, planningRound: task.planningRound, plannerGuidance: task.plannerGuidance, manualPlanning: task.manualPlanning, parentTaskId: task.parentTaskId, dependencies: task.dependencies, capabilities: task.capabilities, workerRequirements: task.workerRequirements, workerSelection: task.workerSelection, adapter: task.adapter,
|
|
9
|
-
input: task.input, attachments: task.attachments, output: task.output, routing, chatgptTabClosePolicy: task.chatgptTabClosePolicy ?? 'after-start', continuation: task.continuation ?? null, maxAttempts: task.maxAttempts, timeoutMs: task.timeoutMs
|
|
9
|
+
input: task.input, attachments: task.attachments, output: task.output, review: task.review, routing, chatgptTabClosePolicy: task.chatgptTabClosePolicy ?? 'after-start', continuation: task.continuation ?? null, maxAttempts: task.maxAttempts, timeoutMs: task.timeoutMs
|
|
10
10
|
};
|
|
11
11
|
}
|
|
12
12
|
export function sameTaskDefinition(left, right) {
|
package/package.json
CHANGED
|
@@ -66,6 +66,7 @@ Do not create separate Tasks merely for "analyze", "format", "call tool", "check
|
|
|
66
66
|
- Otherwise return only the next useful executable batch from the graph-derived Task boundaries.
|
|
67
67
|
- Do not repeat completed work or recreate an existing Task unless replacement work is explicitly needed.
|
|
68
68
|
- Prefer independently verifiable Tasks with stable IDs, but do not split work solely to make Tasks smaller.
|
|
69
|
+
- When a Job needs multi-perspective review, use the visible `review-chair` agent or an Agent Graph to select specialist roles dynamically. Keep specialist Tasks as independent siblings against the same immutable target/version, and leave worker/model/provider routing to Relay.
|
|
69
70
|
- A Task describes what an agent must accomplish; never emit a shell command as a worker Task.
|
|
70
71
|
- Use dependencies only for Task IDs inside the current Job, and only when execution order actually matters. Never encode cross-Job or cross-repository dependencies in `dependencies`.
|
|
71
72
|
- When the current Job is a prerequisite for another durable Job/Task, emit that dependent in top-level `unblocks`. The current blocker Job owns this authoritative reverse relation. Dependent-side `blockedBy` is optional observability metadata and is not the source of truth. Use `resolutionMode: "merge"` when blocker PR merge is sufficient; use `"verified"` when publication, deployment, runtime health, human approval, or another post-merge fact must be verified.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: review-chair
|
|
3
|
+
description: Select a non-redundant specialist review panel for one immutable target and synthesize one attributed revision plan.
|
|
4
|
+
capabilities: review-orchestration, structured-synthesis
|
|
5
|
+
delegation: relay-child-task
|
|
6
|
+
restrictions: no-artifact-edits, no-worker-model-provider-selection, preserve-final-review
|
|
7
|
+
runtime: codex
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Review Chair Agent
|
|
11
|
+
|
|
12
|
+
You own the review panel for exactly one declared immutable target/version. You
|
|
13
|
+
are a review orchestrator, not an artifact editor and not the final approval
|
|
14
|
+
gate.
|
|
15
|
+
|
|
16
|
+
## Contract
|
|
17
|
+
|
|
18
|
+
1. Read the Job objective, acceptance criteria, audience/platform, context, and
|
|
19
|
+
the exact target `{ artifact, version }`. Reject or report any specialist
|
|
20
|
+
result that targets a different version.
|
|
21
|
+
2. Derive only the review dimensions that matter for this target. Discover
|
|
22
|
+
candidates from the resolved visible-agent catalog at execution time; never
|
|
23
|
+
use a hard-coded reviewer list. Select a small non-redundant panel (normally
|
|
24
|
+
2-5 agents) and record each agent, dimension, and evidence-based reason.
|
|
25
|
+
3. Create normal durable specialist Tasks with `parentTaskId` lineage. Every
|
|
26
|
+
specialist Task must carry the same immutable target/version. Independent
|
|
27
|
+
specialists may be siblings and may run in parallel when dependencies allow.
|
|
28
|
+
Select agent identities/roles only; worker, adapter, provider, and model
|
|
29
|
+
routing remains Relay's responsibility.
|
|
30
|
+
4. Require structured specialist results with reviewer, dimension, verdict,
|
|
31
|
+
findings, severity/priority, target location when useful, recommendation,
|
|
32
|
+
and evidence/rationale. Preserve attribution.
|
|
33
|
+
5. Deduplicate overlapping findings, make disagreements explicit, resolve them
|
|
34
|
+
against the Job objective, and produce one prioritized P0/P1/P2/P3 revision
|
|
35
|
+
plan. The executor receives this unified plan, never competing edit lists.
|
|
36
|
+
6. If a missing expertise dimension or unresolved blocker remains, record it and
|
|
37
|
+
request an additional specialist Task before declaring synthesis complete.
|
|
38
|
+
Preserve prior trade-off rationale across revision rounds when available.
|
|
39
|
+
|
|
40
|
+
## Output
|
|
41
|
+
|
|
42
|
+
Return a structured synthesis containing: target; selected panel and reasons;
|
|
43
|
+
merged findings; conflicts and resolutions; prioritized actions with attribution;
|
|
44
|
+
unresolved/blocking issues; and whether another specialist round is needed.
|
|
45
|
+
|
|
46
|
+
The Review Chair must not modify the reviewed artifact, select a worker/model/
|
|
47
|
+
provider, merge or approve the Job, or replace the existing SHA-bound final
|
|
48
|
+
reviewer. A revised artifact is a new target/version and starts a new review
|
|
49
|
+
round.
|
|
@@ -150,4 +150,17 @@ A zero-task autonomous Job stays idle under ordinary reconciliation, but an expl
|
|
|
150
150
|
|
|
151
151
|
Planner-produced Tasks obey this same contract, including explicit `storySize`.
|
|
152
152
|
|
|
153
|
+
### Optional Review Chair Tasks
|
|
154
|
+
|
|
155
|
+
Projects may use the shared `review-chair` agent as a normal Task or Agent
|
|
156
|
+
Graph node. The chair discovers specialist agent roles from the resolved
|
|
157
|
+
visible-agent catalog, records selection reasons, and creates sibling durable
|
|
158
|
+
specialist Tasks carrying the same immutable review target/version. Their
|
|
159
|
+
structured findings are synthesized into one attributed revision plan. A chair
|
|
160
|
+
may add a specialist when synthesis exposes a missing dimension. It never
|
|
161
|
+
chooses worker/model/provider, edits the artifact, or replaces the existing
|
|
162
|
+
SHA-bound final reviewer. For example, `video-director + market + style ->
|
|
163
|
+
review-chair -> unified revision plan` is a project pattern, not a hard-coded
|
|
164
|
+
Relay reviewer list.
|
|
165
|
+
|
|
153
166
|
A scheduled autonomous job adds cron/timezone/next-run and missed-run/concurrency policy to the trusted job marker, plus the `agents-relay:autonomous` label and one bounded time-of-day `agents-relay:next-run:*` index label. The GitHub heartbeat discovers only indexed due candidates, validates the marker's exact `nextRunAt`, claims the slot idempotently, advances the durable next run, and pulses `agents-relay:schedule-wake`. GitHub webhook delivery then wakes local Relay for planner/worker execution. While the schedule is active, the durable Job remains `ACTIVE`; between runs its derived runtime status is `WAITING`. Scheduled recurring Jobs do not enter one-shot final-review/auto-merge after each satisfied run; older-cycle failures are historical evidence and do not block a later slot.
|