agents-relay 1.0.129 → 1.0.131
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/blockers.js +11 -7
- package/dist/planner.js +28 -1
- package/dist/reconciler.js +5 -1
- package/dist/store-github.js +11 -1
- package/dist/usage.js +68 -3
- package/dist/worker-routing.js +13 -2
- package/package.json +1 -1
- package/skills/agents-relay/agents/planner.agent.md +5 -3
- package/skills/agents-relay/contracts/task-job-contract.md +2 -2
package/dist/blockers.js
CHANGED
|
@@ -11,8 +11,8 @@ export async function registerBlockerDependency(options) {
|
|
|
11
11
|
const task = dependentJob.tasks.find(item => item.id === options.dependentTaskId);
|
|
12
12
|
if (!task)
|
|
13
13
|
throw new Error(`Dependent task ${options.dependentTaskId} not found`);
|
|
14
|
-
if (!['FAILED', 'BLOCKED'].includes(task.state))
|
|
15
|
-
throw new Error(`Dependent task ${task.id} is not blocked/failed (${task.state})`);
|
|
14
|
+
if (!['DRAFT', 'FAILED', 'BLOCKED'].includes(task.state))
|
|
15
|
+
throw new Error(`Dependent task ${task.id} is not waiting/blocked/failed (${task.state})`);
|
|
16
16
|
const now = options.now ?? new Date().toISOString();
|
|
17
17
|
const relation = {
|
|
18
18
|
repository: dependentJob.repository,
|
|
@@ -48,7 +48,7 @@ export async function registerBlockerDependency(options) {
|
|
|
48
48
|
function obsoleteReason(task, relation) {
|
|
49
49
|
if (!task)
|
|
50
50
|
return 'dependent task no longer exists';
|
|
51
|
-
if (!['FAILED', 'BLOCKED'].includes(task.state))
|
|
51
|
+
if (!['DRAFT', 'FAILED', 'BLOCKED'].includes(task.state))
|
|
52
52
|
return `dependent task advanced to ${task.state}`;
|
|
53
53
|
if (task.attempt !== relation.blockedAttempt)
|
|
54
54
|
return `dependent task advanced from attempt ${relation.blockedAttempt} to ${task.attempt}`;
|
|
@@ -81,8 +81,7 @@ export async function resolveBlockerDependencies(options) {
|
|
|
81
81
|
continue;
|
|
82
82
|
}
|
|
83
83
|
const task = targetJob.tasks.find(item => item.id === relation.taskId);
|
|
84
|
-
const
|
|
85
|
-
const matches = !!task && ['FAILED', 'BLOCKED'].includes(task.state) && task.attempt === relation.blockedAttempt && sameBlocker(task.blockedBy, blockerRef);
|
|
84
|
+
const matches = !!task && ['DRAFT', 'FAILED', 'BLOCKED'].includes(task.state) && task.attempt === relation.blockedAttempt;
|
|
86
85
|
if (!matches) {
|
|
87
86
|
relation.state = 'obsolete';
|
|
88
87
|
relation.resolvedAt = now;
|
|
@@ -91,8 +90,13 @@ export async function resolveBlockerDependencies(options) {
|
|
|
91
90
|
obsolete += 1;
|
|
92
91
|
continue;
|
|
93
92
|
}
|
|
94
|
-
task.
|
|
95
|
-
|
|
93
|
+
if (task.state === 'DRAFT') {
|
|
94
|
+
await transitionTaskEvent(task, 'release');
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
task.maxAttempts = extendRetryBudget(task);
|
|
98
|
+
await transitionTaskEvent(task, 'retry');
|
|
99
|
+
}
|
|
96
100
|
task.error = null;
|
|
97
101
|
task.leaseOwner = null;
|
|
98
102
|
task.leaseExpiresAt = null;
|
package/dist/planner.js
CHANGED
|
@@ -129,6 +129,33 @@ export function parsePlannerResult(value) {
|
|
|
129
129
|
throw new Error('Planner assessment must be a string');
|
|
130
130
|
if (!Array.isArray(root.next_tasks))
|
|
131
131
|
throw new Error('Planner next_tasks must be an array');
|
|
132
|
+
const unblockKeys = new Set();
|
|
133
|
+
const unblocksRaw = root.unblocks === undefined ? [] : root.unblocks;
|
|
134
|
+
if (!Array.isArray(unblocksRaw))
|
|
135
|
+
throw new Error('Planner unblocks must be an array when provided');
|
|
136
|
+
const unblocks = unblocksRaw.map((item, index) => {
|
|
137
|
+
const unblock = record(item, `unblocks[${index}]`);
|
|
138
|
+
const repository = optionalString(unblock.repository, `unblocks[${index}].repository`);
|
|
139
|
+
const jobId = optionalString(unblock.jobId, `unblocks[${index}].jobId`);
|
|
140
|
+
const taskId = optionalString(unblock.taskId, `unblocks[${index}].taskId`);
|
|
141
|
+
if (!repository || !/^[^/\s]+\/[^/\s]+$/.test(repository))
|
|
142
|
+
throw new Error(`Planner unblocks[${index}].repository must be OWNER/REPO`);
|
|
143
|
+
if (!jobId)
|
|
144
|
+
throw new Error(`Planner unblocks[${index}].jobId is required`);
|
|
145
|
+
if (!taskId)
|
|
146
|
+
throw new Error(`Planner unblocks[${index}].taskId is required`);
|
|
147
|
+
const prNumber = unblock.prNumber;
|
|
148
|
+
if (!Number.isInteger(prNumber) || Number(prNumber) <= 0)
|
|
149
|
+
throw new Error(`Planner unblocks[${index}].prNumber must be a positive integer`);
|
|
150
|
+
const resolutionMode = unblock.resolutionMode === undefined ? undefined : String(unblock.resolutionMode);
|
|
151
|
+
if (resolutionMode !== undefined && resolutionMode !== 'merge' && resolutionMode !== 'verified')
|
|
152
|
+
throw new Error(`Planner unblocks[${index}].resolutionMode must be merge or verified`);
|
|
153
|
+
const key = `${repository}#${prNumber}:${jobId}:${taskId}`;
|
|
154
|
+
if (unblockKeys.has(key))
|
|
155
|
+
throw new Error(`Planner unblocks contains duplicate dependent ${key}`);
|
|
156
|
+
unblockKeys.add(key);
|
|
157
|
+
return { repository, prNumber: Number(prNumber), jobId, taskId, resolutionMode: resolutionMode };
|
|
158
|
+
});
|
|
132
159
|
const ids = new Set();
|
|
133
160
|
const next_tasks = root.next_tasks.map((item, index) => {
|
|
134
161
|
const task = record(item, `next_tasks[${index}]`);
|
|
@@ -169,7 +196,7 @@ export function parsePlannerResult(value) {
|
|
|
169
196
|
continuation: task.continuation, maxAttempts, timeoutMs
|
|
170
197
|
};
|
|
171
198
|
});
|
|
172
|
-
return { objective_status: status, assessment: root.assessment, next_tasks };
|
|
199
|
+
return { objective_status: status, assessment: root.assessment, next_tasks, unblocks };
|
|
173
200
|
}
|
|
174
201
|
export function parsePlannerOutput(output) {
|
|
175
202
|
const trimmed = output.trim();
|
package/dist/reconciler.js
CHANGED
|
@@ -491,6 +491,10 @@ export class Reconciler extends ReconcilerReviewBase {
|
|
|
491
491
|
try {
|
|
492
492
|
const result = parsePlannerResult(raw);
|
|
493
493
|
const executionContext = await this.plannerContext(job, task);
|
|
494
|
+
if (result.unblocks.length > 0 && !this.store.registerBlockerDependent)
|
|
495
|
+
throw new Error('Durable store does not support Planner cross-Job unblock registration');
|
|
496
|
+
for (const unblock of result.unblocks)
|
|
497
|
+
await this.store.registerBlockerDependent?.(job.id, unblock);
|
|
494
498
|
const children = result.next_tasks.map(spec => this.plannerChild(job, task, spec, executionContext));
|
|
495
499
|
for (const child of children) {
|
|
496
500
|
const existing = job.tasks.find(item => item.id === child.id);
|
|
@@ -499,7 +503,7 @@ export class Reconciler extends ReconcilerReviewBase {
|
|
|
499
503
|
}
|
|
500
504
|
// Persist the completed decision before exposing executable children.
|
|
501
505
|
task.plannerResult = result;
|
|
502
|
-
task.result = { summary: result.assessment, data: { objective_status: result.objective_status, next_tasks: result.next_tasks } };
|
|
506
|
+
task.result = { summary: result.assessment, data: { objective_status: result.objective_status, next_tasks: result.next_tasks, unblocks: result.unblocks } };
|
|
503
507
|
task.error = null;
|
|
504
508
|
task.plannerRetryFeedback = undefined;
|
|
505
509
|
task.leaseOwner = null;
|
package/dist/store-github.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { parseAgent, parseJob, parseTask, renderAgentComment, renderJobBody, renderTaskComment, JOB_MARKER } from './markers.js';
|
|
2
2
|
import { isTaskForJob, sameTaskDefinition } from './store-task-definition.js';
|
|
3
|
-
import { resolveBlockerDependencies } from './blockers.js';
|
|
3
|
+
import { registerBlockerDependency, resolveBlockerDependencies } from './blockers.js';
|
|
4
4
|
const TASK_WRITE_OVERLAY = new Map();
|
|
5
5
|
export class GitHubStore {
|
|
6
6
|
client;
|
|
@@ -118,6 +118,16 @@ export class GitHubStore {
|
|
|
118
118
|
return ref && sha ? { ref, sha } : null;
|
|
119
119
|
}
|
|
120
120
|
async headSha() { return (await this.pullRequestHead())?.sha ?? null; }
|
|
121
|
+
async registerBlockerDependent(jobId, dependent) {
|
|
122
|
+
await registerBlockerDependency({
|
|
123
|
+
blockerStore: this,
|
|
124
|
+
blockerJobId: jobId,
|
|
125
|
+
dependentStore: new GitHubStore(this.client, dependent.repository, dependent.prNumber, this.trustedAuthors),
|
|
126
|
+
dependentJobId: dependent.jobId,
|
|
127
|
+
dependentTaskId: dependent.taskId,
|
|
128
|
+
resolutionMode: dependent.resolutionMode,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
121
131
|
async resolveBlockerDependents(jobId, resolution) {
|
|
122
132
|
return await resolveBlockerDependencies({ blockerStore: this, blockerJobId: jobId, resolution, dependentStore: (repository, prNumber) => new GitHubStore(this.client, repository, prNumber, this.trustedAuthors) });
|
|
123
133
|
}
|
package/dist/usage.js
CHANGED
|
@@ -4,6 +4,67 @@ import { homedir } from 'node:os';
|
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
import { createInterface } from 'node:readline';
|
|
6
6
|
import { spawn } from 'node:child_process';
|
|
7
|
+
export class BrowserWorkspaceUsageReader {
|
|
8
|
+
command;
|
|
9
|
+
timeoutMs;
|
|
10
|
+
now;
|
|
11
|
+
staleAfterMs;
|
|
12
|
+
constructor(command = 'browser-harness', timeoutMs = 10_000, now = () => new Date(), staleAfterMs = 15 * 60_000) {
|
|
13
|
+
this.command = command;
|
|
14
|
+
this.timeoutMs = timeoutMs;
|
|
15
|
+
this.now = now;
|
|
16
|
+
this.staleAfterMs = staleAfterMs;
|
|
17
|
+
}
|
|
18
|
+
async read() {
|
|
19
|
+
const observedAt = this.now().toISOString();
|
|
20
|
+
return await new Promise(resolve => {
|
|
21
|
+
const child = spawn(this.command, [], { stdio: ['pipe', 'pipe', 'ignore'] });
|
|
22
|
+
let output = '';
|
|
23
|
+
let settled = false;
|
|
24
|
+
const finish = (capacity) => {
|
|
25
|
+
if (settled)
|
|
26
|
+
return;
|
|
27
|
+
settled = true;
|
|
28
|
+
clearTimeout(timer);
|
|
29
|
+
child.kill();
|
|
30
|
+
resolve(capacity);
|
|
31
|
+
};
|
|
32
|
+
const unavailable = (reason) => ({ name: 'Harness', poolSize: null, idle: null, leased: null, available: null, observedAt, reason });
|
|
33
|
+
const timer = setTimeout(() => finish(unavailable('Browser Workspace capacity probe timed out.')), this.timeoutMs);
|
|
34
|
+
child.stdout.setEncoding('utf8');
|
|
35
|
+
child.stdout.on('data', chunk => { output += chunk; });
|
|
36
|
+
child.once('error', () => finish(unavailable('Browser Workspace capacity probe could not be started.')));
|
|
37
|
+
child.once('close', code => {
|
|
38
|
+
if (settled)
|
|
39
|
+
return;
|
|
40
|
+
const lines = output.trim().split('\n').reverse();
|
|
41
|
+
for (const line of lines) {
|
|
42
|
+
try {
|
|
43
|
+
const raw = JSON.parse(line);
|
|
44
|
+
const name = typeof raw.name === 'string' ? raw.name : 'Harness';
|
|
45
|
+
const rawObservedAt = typeof raw.observedAt === 'string' && !Number.isNaN(Date.parse(raw.observedAt)) ? raw.observedAt : observedAt;
|
|
46
|
+
if (Date.parse(observedAt) - Date.parse(rawObservedAt) > this.staleAfterMs) {
|
|
47
|
+
finish(unavailable('Browser Workspace capacity probe is stale.'));
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const numberOrNull = (value) => typeof value === 'number' && Number.isFinite(value) && value >= 0 ? value : null;
|
|
51
|
+
const idle = numberOrNull(raw.idle);
|
|
52
|
+
const leased = numberOrNull(raw.leased);
|
|
53
|
+
const poolSize = numberOrNull(raw.poolSize);
|
|
54
|
+
const available = typeof raw.available === 'boolean' ? raw.available : idle === null ? null : idle > 0;
|
|
55
|
+
if (idle !== null || leased !== null || poolSize !== null || available !== null) {
|
|
56
|
+
finish({ name, poolSize, idle, leased, available, observedAt: rawObservedAt, reason: null });
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
catch { /* browser-harness may print startup diagnostics before JSON */ }
|
|
61
|
+
}
|
|
62
|
+
finish(unavailable(code === 0 ? 'Browser Workspace capacity probe returned no usable capacity.' : `Browser Workspace capacity probe exited with code ${code ?? 'unknown'}.`));
|
|
63
|
+
});
|
|
64
|
+
child.stdin.end('import json\nprint(json.dumps(workspace_capacity()))\n');
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
}
|
|
7
68
|
function finiteNumber(value) {
|
|
8
69
|
if (typeof value === 'number' && Number.isFinite(value))
|
|
9
70
|
return value;
|
|
@@ -320,18 +381,21 @@ export class UsageRegistry {
|
|
|
320
381
|
zaiReader;
|
|
321
382
|
deepSeekReader;
|
|
322
383
|
openRouterReader;
|
|
323
|
-
|
|
384
|
+
browserWorkspaceReader;
|
|
385
|
+
constructor(openaiReader, zaiReader = new ZaiUsageReader(), deepSeekReader = null, openRouterReader = null, browserWorkspaceReader = null) {
|
|
324
386
|
this.openaiReader = openaiReader;
|
|
325
387
|
this.zaiReader = zaiReader;
|
|
326
388
|
this.deepSeekReader = deepSeekReader;
|
|
327
389
|
this.openRouterReader = openRouterReader;
|
|
390
|
+
this.browserWorkspaceReader = browserWorkspaceReader;
|
|
328
391
|
}
|
|
329
392
|
async snapshot() {
|
|
330
|
-
const [openai, zai, deepseek, openrouter] = await Promise.all([
|
|
393
|
+
const [openai, zai, deepseek, openrouter, browserWorkspace] = await Promise.all([
|
|
331
394
|
this.openaiReader.read(),
|
|
332
395
|
this.zaiReader.read(),
|
|
333
396
|
this.deepSeekReader?.read() ?? Promise.resolve(null),
|
|
334
397
|
this.openRouterReader?.read() ?? Promise.resolve(null),
|
|
398
|
+
this.browserWorkspaceReader?.read() ?? Promise.resolve(null),
|
|
335
399
|
]);
|
|
336
400
|
const credits = [];
|
|
337
401
|
if (deepseek)
|
|
@@ -345,6 +409,7 @@ export class UsageRegistry {
|
|
|
345
409
|
{ id: 'zai', label: 'Z.ai', usage: zai },
|
|
346
410
|
],
|
|
347
411
|
credits,
|
|
412
|
+
...(browserWorkspace ? { browserWorkspace } : {}),
|
|
348
413
|
};
|
|
349
414
|
}
|
|
350
415
|
}
|
|
@@ -370,5 +435,5 @@ export class UnavailableUsageReader {
|
|
|
370
435
|
}
|
|
371
436
|
}
|
|
372
437
|
export function codexAndZaiUsageRegistry(codexHome, _legacyWindowDays) {
|
|
373
|
-
return new UsageRegistry(new CodexApiUsageReader(), new ZaiUsageReader(), process.env.DEEPSEEK_API_KEY ? new DeepSeekCreditReader() : null, process.env.OPENROUTER_API_KEY ? new OpenRouterCreditReader() : null);
|
|
438
|
+
return new UsageRegistry(new CodexApiUsageReader(), new ZaiUsageReader(), process.env.DEEPSEEK_API_KEY ? new DeepSeekCreditReader() : null, process.env.OPENROUTER_API_KEY ? new OpenRouterCreditReader() : null, new BrowserWorkspaceUsageReader());
|
|
374
439
|
}
|
package/dist/worker-routing.js
CHANGED
|
@@ -68,6 +68,7 @@ export function workerCandidates(adapters, registered, snapshot) {
|
|
|
68
68
|
resourceResetAt: resource.resetAt,
|
|
69
69
|
resourceResetInMinutes: resource.resetInMinutes,
|
|
70
70
|
resourceWindows: resource.windows,
|
|
71
|
+
browserWorkspaceCapacity: adapter.name === 'chatgpt' ? snapshot?.browserWorkspace : undefined,
|
|
71
72
|
};
|
|
72
73
|
});
|
|
73
74
|
});
|
|
@@ -82,6 +83,8 @@ export function satisfiesWorker(candidate, requirements) {
|
|
|
82
83
|
return false;
|
|
83
84
|
if (requirements.runtime && candidate.runtime !== requirements.runtime)
|
|
84
85
|
return false;
|
|
86
|
+
if (candidate.adapter === 'chatgpt' && candidate.browserWorkspaceCapacity?.idle === 0)
|
|
87
|
+
return false;
|
|
85
88
|
return true;
|
|
86
89
|
}
|
|
87
90
|
export function eligibleWorkerCandidates(candidates, requirements) {
|
|
@@ -104,6 +107,7 @@ function selection(selected, selectedBy, decisionEvidence) {
|
|
|
104
107
|
resourceResetAt: selected.resourceResetAt,
|
|
105
108
|
resourceResetInMinutes: selected.resourceResetInMinutes,
|
|
106
109
|
resourceWindows: selected.resourceWindows,
|
|
110
|
+
browserWorkspaceCapacity: selected.browserWorkspaceCapacity,
|
|
107
111
|
decisionEvidence,
|
|
108
112
|
};
|
|
109
113
|
}
|
|
@@ -111,15 +115,21 @@ function selection(selected, selectedBy, decisionEvidence) {
|
|
|
111
115
|
export function selectWorker(candidates, requirements) {
|
|
112
116
|
const eligible = eligibleWorkerCandidates(candidates, requirements);
|
|
113
117
|
if (eligible.length === 0)
|
|
114
|
-
throw new Error(
|
|
118
|
+
throw new Error(noEligibleWorkerReason(candidates));
|
|
115
119
|
const selected = deterministicWorker(eligible);
|
|
116
120
|
return { candidate: selected, selection: selection(selected, 'worker-router:fallback') };
|
|
117
121
|
}
|
|
122
|
+
function noEligibleWorkerReason(candidates) {
|
|
123
|
+
if (candidates.some(candidate => candidate.adapter === 'chatgpt' && candidate.browserWorkspaceCapacity?.idle === 0)) {
|
|
124
|
+
return 'No eligible worker: ChatGPT Browser Workspace has no idle capacity.';
|
|
125
|
+
}
|
|
126
|
+
return 'No eligible worker satisfies the authoritative worker constraints';
|
|
127
|
+
}
|
|
118
128
|
/** Gather factual worker/task/resource context, then let decide() choose among legal workers. */
|
|
119
129
|
export async function routeWorker(candidates, requirements, options = {}) {
|
|
120
130
|
const eligible = eligibleWorkerCandidates(candidates, requirements);
|
|
121
131
|
if (eligible.length === 0)
|
|
122
|
-
throw new Error(
|
|
132
|
+
throw new Error(noEligibleWorkerReason(candidates));
|
|
123
133
|
const preferredChatgptConcurrency = options.preferredChatgptConcurrency ?? DEFAULT_CHATGPT_PREFERRED_CONCURRENCY;
|
|
124
134
|
const chatgptRunning = options.chatgptRunning ?? 0;
|
|
125
135
|
const deterministic = deterministicWorker(eligible);
|
|
@@ -152,6 +162,7 @@ export async function routeWorker(candidates, requirements, options = {}) {
|
|
|
152
162
|
resourceResetAt: item.resourceResetAt ?? null,
|
|
153
163
|
resourceResetInMinutes: item.resourceResetInMinutes ?? null,
|
|
154
164
|
resourceWindows: item.resourceWindows ?? null,
|
|
165
|
+
browserWorkspaceCapacity: item.browserWorkspaceCapacity ?? null,
|
|
155
166
|
})),
|
|
156
167
|
}));
|
|
157
168
|
const choice = await chooseWithDecision(options.decisionEngine, options.decisionMode ?? 'off', { facts }, {
|
package/package.json
CHANGED
|
@@ -67,7 +67,8 @@ Do not create separate Tasks merely for "analyze", "format", "call tool", "check
|
|
|
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
69
|
- A Task describes what an agent must accomplish; never emit a shell command as a worker Task.
|
|
70
|
-
- Use dependencies only when execution order actually matters.
|
|
70
|
+
- 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
|
+
- 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.
|
|
71
72
|
- Describe each child Task precisely enough for worker-router to decide from the Task itself. Do not invent Codex/ChatGPT capability labels. workerRequirements is optional and should normally be omitted; use an explicit runtime only when the objective already requires that runtime.
|
|
72
73
|
- Do not choose codex/chatgpt, provider/model, or usage probes.
|
|
73
74
|
- Every child Task must declare exactly one output: `task_pr` or `file`.
|
|
@@ -77,7 +78,7 @@ Do not create separate Tasks merely for "analyze", "format", "call tool", "check
|
|
|
77
78
|
|
|
78
79
|
## Output
|
|
79
80
|
|
|
80
|
-
Return exactly one JSON object with `objective_status`, `assessment`, and `
|
|
81
|
+
Return exactly one JSON object with `objective_status`, `assessment`, `next_tasks`, and optional top-level `unblocks`.
|
|
81
82
|
|
|
82
83
|
For `objective_status: "in_progress"`, `assessment` must be Markdown containing:
|
|
83
84
|
|
|
@@ -86,7 +87,7 @@ For `objective_status: "in_progress"`, `assessment` must be Markdown containing:
|
|
|
86
87
|
|
|
87
88
|
For `objective_status: "satisfied"`, `assessment` may be a concise evidence-based completion assessment without a graph.
|
|
88
89
|
|
|
89
|
-
Each item in next_tasks contains id, input, output, and storySize. 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.
|
|
90
|
+
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.
|
|
90
91
|
|
|
91
92
|
### Exact `next_tasks` serialization
|
|
92
93
|
|
|
@@ -105,6 +106,7 @@ A valid non-satisfied response:
|
|
|
105
106
|
{
|
|
106
107
|
"objective_status": "in_progress",
|
|
107
108
|
"assessment": "```mermaid\nflowchart LR\n A[Evidence] --> B[Content package]\n```\nOne daily package is the durable Task boundary.",
|
|
109
|
+
"unblocks": [{"repository":"org/app","prNumber":42,"jobId":"dependent-job","taskId":"consume-output","resolutionMode":"merge"}],
|
|
108
110
|
"next_tasks": [
|
|
109
111
|
{
|
|
110
112
|
"id": "episode-2026-09-26",
|
|
@@ -101,9 +101,9 @@ A Task with `adapter: orchestrator` is synchronous self-work owned by the exact
|
|
|
101
101
|
|
|
102
102
|
## Cross-Job blocker dependencies
|
|
103
103
|
|
|
104
|
-
Task `dependencies` remain local to one Job and must contain only Task IDs from that Job. Shared runtime/tool/platform blockers use a separate durable cross-Job relation. The blocker Job owns reverse `unblocks` records identifying dependent repository, PR, Job, Task, and blocked attempt. The dependent Task may carry a lightweight `blockedBy` reference for observability and deduplication.
|
|
104
|
+
Task `dependencies` remain local to one Job and must contain only Task IDs from that Job. Shared runtime/tool/platform blockers use a separate durable cross-Job relation. The blocker Job owns reverse `unblocks` records identifying dependent repository, PR, Job, Task, and blocked attempt. The dependent Task may carry a lightweight `blockedBy` reference for observability and deduplication. Planner output follows the same ownership rule: when the current Job is the prerequisite, Planner declares dependents through top-level `unblocks`; Relay persists those declarations on the blocker Job via the blocker subsystem. Planner must never encode cross-Job dependencies as local `dependencies`. A dependent may be registered while its Task is still `DRAFT`, and blocker resolution releases that Task when the blocker resolves.
|
|
105
105
|
|
|
106
|
-
Blocker resolution is generation-safe and idempotent: Relay
|
|
106
|
+
Blocker resolution is generation-safe and idempotent: Relay releases a dependent only when it is still DRAFT/FAILED/BLOCKED on the recorded attempt. The blocker-side `unblocks` record is authoritative; dependent-side `blockedBy` may be absent. Otherwise the relation becomes obsolete. Merge-mode blockers resolve when the blocker PR is observed merged. Blockers requiring package publication, deployment, runtime health, human/external verification, or another post-merge fact use verified mode and resolve only through the explicit verified-resolution path. Existing failure evidence is preserved; DRAFT dependents use the normal release transition and FAILED/BLOCKED dependents use the normal retry transition to QUEUED.
|
|
107
107
|
|
|
108
108
|
## Worker lifecycle
|
|
109
109
|
|