agents-relay 1.0.72 → 1.0.74
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/README.md +4 -4
- package/dist/agent-graph.js +74 -0
- package/dist/cli.js +6 -3
- package/dist/dashboard.js +1 -4
- package/dist/execution-context.js +25 -0
- package/dist/markers.js +13 -3
- package/dist/planner.js +2 -2
- package/dist/reconciler.js +10 -7
- package/dist/store.js +41 -18
- package/package.json +1 -1
- package/skills/agents-relay/contracts/task-job-contract.md +2 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Agents Relay
|
|
2
2
|
|
|
3
|
-
Agents Relay is a generic TypeScript runtime for durable asynchronous agent jobs coordinated through GitHub pull requests.
|
|
3
|
+
Agents Relay is a generic TypeScript runtime for durable asynchronous agent jobs coordinated through GitHub pull requests. The PR body stores the durable Job marker plus human-readable Job objective, while PR comments store durable Task and other append-friendly records; correlated terminal events are authoritative for model-backed task lifecycle transitions.
|
|
4
4
|
|
|
5
5
|
## Quick start
|
|
6
6
|
|
|
@@ -53,7 +53,7 @@ Lifecycle transition authority lives in the directly renderable canonical source
|
|
|
53
53
|
|
|
54
54
|
Graph-engine rollout is confidence-gated with `AGENTS_RELAY_WORKFLOW_MODE=legacy|shadow|graph` (default `legacy`). `shadow` keeps legacy authoritative while comparing the Mermaid decision and exposing `{mode,matches,mismatches}` at `/api/workflow-rollout`; `graph` is explicit opt-in until the shadow cutover gate is approved.
|
|
55
55
|
|
|
56
|
-
Job objectives and Task inputs are Markdown. Prefer `--objective-file` / `--input-file` (use `-` for stdin) for durable long-form context; inline `--objective` / `--input` remain for short/simple text and are mutually exclusive with the file variants. When a diagram materially clarifies the work, put Mermaid first and then Markdown detail. A newly created Job must capture enough context to finish independently of the originating chat
|
|
56
|
+
Job objectives and Task inputs are Markdown. Prefer `--objective-file` / `--input-file` (use `-` for stdin) for durable long-form context; inline `--objective` / `--input` remain for short/simple text and are mutually exclusive with the file variants. When a diagram materially clarifies the work, put Mermaid first and then Markdown detail. A newly created Job must capture enough context to finish independently of the originating chat. Its PR body contains exactly one hidden `agents-relay:job:v1` metadata marker followed by the objective Markdown; there is no separate Job comment duplicating the objective. Objective and Job metadata updates rewrite that same PR body. Legacy Job-comment markers remain readable and are migrated into the PR body on the next Job save.
|
|
57
57
|
|
|
58
58
|
Jobs default to `fixed`. `job create` persists only the durable job and never
|
|
59
59
|
creates executable tasks. The top-level orchestrator decides whether the user's
|
|
@@ -141,7 +141,7 @@ reconcile leases and launches ready tasks without waiting inside the runtime sch
|
|
|
141
141
|
|
|
142
142
|
For automation, Agents Relay can use GitHub App installation authentication via `AGENTS_RELAY_GITHUB_APP_ID`, `AGENTS_RELAY_GITHUB_APP_INSTALLATION_ID`, and `AGENTS_RELAY_GITHUB_APP_PRIVATE_KEY_FILE` (or matching CLI flags). Webhook mode is cache-first: targeted webhook reconciliation is primary, dashboard auto-refresh reads cached job state, and the broad watchdog drops to a 30-minute fallback. See [docs/github-app-rate-limit.md](docs/github-app-rate-limit.md).
|
|
143
143
|
|
|
144
|
-
V1 assumes one active runner/daemon per job. Use a distributed atomic lease before running multiple reconcilers. Do not put secrets, credentials, private prompts, or large private payloads in PR marker
|
|
144
|
+
V1 assumes one active runner/daemon per job. Use a distributed atomic lease before running multiple reconcilers. Do not put secrets, credentials, private prompts, or large private payloads in the PR-body Job marker or PR-comment Task markers; store only summaries and artifact references.
|
|
145
145
|
|
|
146
146
|
Codex/model-backed tasks require --provider and --model (or equivalent routing metadata) before launch. See [docs/architecture.md](docs/architecture.md) and [skills/agents-relay/SKILL.md](skills/agents-relay/SKILL.md).
|
|
147
147
|
|
|
@@ -176,4 +176,4 @@ npx agents-relay task create --repo OWNER/REPO --pr 5 --id job-1 \
|
|
|
176
176
|
|
|
177
177
|
GitHub PR state is authoritative for terminal lifecycle: a merged PR reconciles its managed job to `COMPLETED`; a closed, unmerged PR reconciles to `CANCELLED` and cannot launch queued work. The dashboard exposes `/api/jobs` and shows PR state, durable Job lifecycle, and derived runtime status separately. A merged PR reconciles lifecycle to `COMPLETED`; a closed, unmerged PR reconciles lifecycle to `CANCELLED`.
|
|
178
178
|
|
|
179
|
-
For work that was completed outside the relay but must be represented truthfully in durable history, use `npx agents-relay record --task-id <id> --summary <summary> --commit <sha>`. Recorded tasks use the `orchestrator` adapter and terminal `SUCCEEDED` state; they do not pretend a shell/model worker executed the work. This is a repair/backfill mechanism—normal work should be created as a durable Task before execution.
|
|
179
|
+
For work that was completed outside the relay but must be represented truthfully in durable history, use `npx agents-relay record --task-id <id> --summary <summary> --commit <sha>`. Recorded tasks use the `orchestrator` adapter and terminal `SUCCEEDED` state; they do not pretend a shell/model worker executed the work. This is a repair/backfill mechanism—normal work should be created as a durable Task before execution. For managed jobs, the GitHub PR body is the canonical Job surface: hidden Job metadata marker plus the visible Markdown objective.
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { agentContractPrompt } from './execution-context.js';
|
|
2
|
+
const ID = '[A-Za-z_][A-Za-z0-9_.-]*';
|
|
3
|
+
const EDGE = new RegExp(`^(${ID})(?:\\[agent:(${ID})\\])?\\s*-->\\s*(${ID})(?:\\[agent:(${ID})\\])?$`);
|
|
4
|
+
const NODE = new RegExp(`^(${ID})\\[agent:(${ID})\\]$`);
|
|
5
|
+
export function parseAgentGraph(source) {
|
|
6
|
+
const lines = source.split(/\r?\n/);
|
|
7
|
+
let header = false;
|
|
8
|
+
const agents = new Map();
|
|
9
|
+
const edges = [];
|
|
10
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
11
|
+
const line = lines[index].trim();
|
|
12
|
+
if (!line || line.startsWith('%%'))
|
|
13
|
+
continue;
|
|
14
|
+
if (!header) {
|
|
15
|
+
if (!/^flowchart\s+(?:TD|TB|LR|RL)$/.test(line))
|
|
16
|
+
throw new Error(`Agent Graph must start with Mermaid flowchart TD/TB/LR/RL (line ${index + 1})`);
|
|
17
|
+
header = true;
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
const edge = line.match(EDGE);
|
|
21
|
+
if (edge) {
|
|
22
|
+
const [, from, fromAgent, to, toAgent] = edge;
|
|
23
|
+
agents.set(from, fromAgent || agents.get(from) || from);
|
|
24
|
+
agents.set(to, toAgent || agents.get(to) || to);
|
|
25
|
+
edges.push({ from, to });
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
const node = line.match(NODE);
|
|
29
|
+
if (node) {
|
|
30
|
+
agents.set(node[1], node[2]);
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
throw new Error(`Unsupported Agent Graph syntax at line ${index + 1}: ${line}`);
|
|
34
|
+
}
|
|
35
|
+
if (!header)
|
|
36
|
+
throw new Error('Agent Graph is empty');
|
|
37
|
+
if (agents.size === 0)
|
|
38
|
+
throw new Error('Agent Graph requires at least one agent node');
|
|
39
|
+
const incoming = new Map([...agents.keys()].map(id => [id, 0]));
|
|
40
|
+
const outgoing = new Map([...agents.keys()].map(id => [id, []]));
|
|
41
|
+
for (const edge of edges) {
|
|
42
|
+
if (edge.from === edge.to)
|
|
43
|
+
throw new Error(`Agent Graph self-cycle at ${edge.from}`);
|
|
44
|
+
incoming.set(edge.to, (incoming.get(edge.to) ?? 0) + 1);
|
|
45
|
+
outgoing.get(edge.from)?.push(edge.to);
|
|
46
|
+
}
|
|
47
|
+
const remaining = new Map(incoming);
|
|
48
|
+
const waves = [];
|
|
49
|
+
let ready = [...remaining.entries()].filter(([, degree]) => degree === 0).map(([id]) => id).sort();
|
|
50
|
+
let visited = 0;
|
|
51
|
+
while (ready.length) {
|
|
52
|
+
waves.push(ready);
|
|
53
|
+
visited += ready.length;
|
|
54
|
+
const next = new Set();
|
|
55
|
+
for (const id of ready) {
|
|
56
|
+
for (const target of outgoing.get(id) ?? []) {
|
|
57
|
+
const degree = (remaining.get(target) ?? 0) - 1;
|
|
58
|
+
remaining.set(target, degree);
|
|
59
|
+
if (degree === 0)
|
|
60
|
+
next.add(target);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
ready = [...next].sort();
|
|
64
|
+
}
|
|
65
|
+
if (visited !== agents.size)
|
|
66
|
+
throw new Error('Agent Graph contains a cycle');
|
|
67
|
+
return { source, nodes: [...agents].map(([id, agent]) => ({ id, agent })), edges, waves };
|
|
68
|
+
}
|
|
69
|
+
export async function agentGraphPrompt(context, source) {
|
|
70
|
+
const graph = parseAgentGraph(source);
|
|
71
|
+
const contracts = await Promise.all(graph.nodes.map(async (node) => ({ node, prompt: await agentContractPrompt(context, node.agent) })));
|
|
72
|
+
const incoming = new Map(graph.nodes.map(node => [node.id, graph.edges.filter(edge => edge.to === node.id).map(edge => edge.from)]));
|
|
73
|
+
return `[Task Agent Graph]\n${source.trim()}\n\nExecution waves: ${graph.waves.map((wave, index) => `${index + 1}:${wave.join(',')}`).join(' | ')}\n\nExecute this graph inside the current durable Task. Nodes are reasoning roles, NOT durable child Tasks. Preserve the original Job/Task context for every node. Nodes in the same wave are independent: evaluate each from the same original context without using sibling prose. For each later node, provide the original context plus structured outputs only from its declared predecessors. Do not emit outer task.started or task terminal events per node; the current durable Task owns exactly one start and one terminal event.\n\n${contracts.map(({ node, prompt }) => `[Node ${node.id}; predecessors=${(incoming.get(node.id) ?? []).join(',') || 'none'}]\n${prompt}`).join('\n\n')}`;
|
|
74
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -110,7 +110,7 @@ Actions:
|
|
|
110
110
|
'task create': `Create one durable Task under an existing Job.
|
|
111
111
|
|
|
112
112
|
Required: normal Job storage options plus --input-file PATH|- (preferred) or --input MARKDOWN, and --output task-pr|file. --input and --input-file are mutually exclusive. Codex/ChatGPT tasks also require --model MODEL.
|
|
113
|
-
Common: --task-id ID, --adapter codex|chatgpt|orchestrator, --priority P0|P1|P2|P3, --story-size S|M|L, --deps a,b, --parent ID, --draft.\nTasks with dependencies start DRAFT; tasks without dependencies start QUEUED unless --draft is given.
|
|
113
|
+
Common: --task-id ID, --adapter codex|chatgpt|orchestrator, --priority P0|P1|P2|P3, --story-size S|M|L, --agent NAME, --agent-graph-file PATH|-, --deps a,b, --parent ID, --draft.\nTasks with dependencies start DRAFT; tasks without dependencies start QUEUED unless --draft is given.
|
|
114
114
|
Directly-created Tasks are not wrapped in a synthetic Plan. Planner-generated Tasks are created internally by Relay from planner output.
|
|
115
115
|
|
|
116
116
|
Example:
|
|
@@ -118,7 +118,7 @@ Example:
|
|
|
118
118
|
'task update': `Update mutable durable Task fields in place.
|
|
119
119
|
|
|
120
120
|
Required: --task-id ID and the normal Job storage options.
|
|
121
|
-
Mutable while not RUNNING/SUCCEEDED/CANCELLED: --adapter, --priority, --story-size, --input-file PATH|- or --input MARKDOWN, --project, --agent, --parent, --deps, --capabilities, --timeout, --max-attempts, --provider, --model, --profile, --reasoning, --cwd, --chatgpt-project, --chatgpt-tab-close-policy, --draft, --queue.
|
|
121
|
+
Mutable while not RUNNING/SUCCEEDED/CANCELLED: --adapter, --priority, --story-size, --input-file PATH|- or --input MARKDOWN, --project, --agent, --agent-graph-file PATH|-, --parent, --deps, --capabilities, --timeout, --max-attempts, --provider, --model, --profile, --reasoning, --cwd, --chatgpt-project, --chatgpt-tab-close-policy, --draft, --queue.
|
|
122
122
|
Use --clear-routing to remove routing metadata.
|
|
123
123
|
|
|
124
124
|
Example:
|
|
@@ -352,6 +352,7 @@ async function workerTaskFromArgs(job, args, now) {
|
|
|
352
352
|
const outputKind = arg(args, '--output');
|
|
353
353
|
const outputPath = arg(args, '--output-path');
|
|
354
354
|
const input = await textOption(args, '--input', '--input-file');
|
|
355
|
+
const agentGraph = hasArg(args, '--agent-graph') || hasArg(args, '--agent-graph-file') ? await textOption(args, '--agent-graph', '--agent-graph-file') : undefined;
|
|
355
356
|
if (!input.trim())
|
|
356
357
|
throw new Error('task create requires --input-file PATH|- or --input MARKDOWN');
|
|
357
358
|
if (!['codex', 'chatgpt', 'orchestrator'].includes(adapter))
|
|
@@ -374,6 +375,7 @@ async function workerTaskFromArgs(job, args, now) {
|
|
|
374
375
|
storySize: size,
|
|
375
376
|
projectName: arg(args, '--project') || undefined,
|
|
376
377
|
agentName: arg(args, '--agent') || undefined,
|
|
378
|
+
agentGraph,
|
|
377
379
|
parentTaskId: arg(args, '--parent') || null,
|
|
378
380
|
dependencies,
|
|
379
381
|
capabilities: arg(args, '--capabilities').split(',').filter(Boolean),
|
|
@@ -474,6 +476,8 @@ async function runTaskCommand(action, args) {
|
|
|
474
476
|
task.projectName = arg(args, '--project') || undefined;
|
|
475
477
|
if (hasArg(args, '--agent'))
|
|
476
478
|
task.agentName = arg(args, '--agent') || undefined;
|
|
479
|
+
if (hasArg(args, '--agent-graph') || hasArg(args, '--agent-graph-file'))
|
|
480
|
+
task.agentGraph = await textOption(args, '--agent-graph', '--agent-graph-file');
|
|
477
481
|
if (hasArg(args, '--parent'))
|
|
478
482
|
task.parentTaskId = arg(args, '--parent') || null;
|
|
479
483
|
if (hasArg(args, '--deps'))
|
|
@@ -635,7 +639,6 @@ async function runJobCommand(action, args) {
|
|
|
635
639
|
if (!objective.trim())
|
|
636
640
|
throw new Error('Job objective cannot be empty');
|
|
637
641
|
job.objective = objective;
|
|
638
|
-
await store.updatePullRequestBody?.(objective);
|
|
639
642
|
}
|
|
640
643
|
if (hasArg(args, '--priority'))
|
|
641
644
|
job.priority = priority(args, job.priority ?? 'P2');
|
package/dist/dashboard.js
CHANGED
|
@@ -158,7 +158,7 @@ export const dashboardHtml = `<!doctype html>
|
|
|
158
158
|
</header>
|
|
159
159
|
<main>
|
|
160
160
|
<section id="jobs-view" role="tabpanel" aria-labelledby="jobs-tab" class="panel jobs-panel">
|
|
161
|
-
<div class="panel-head"><h2>Jobs</h2><div class="panel-actions"><div class="job-filters"><
|
|
161
|
+
<div class="panel-head"><h2>Jobs</h2><div class="panel-actions"><div class="job-filters"><select id="quick-status" class="job-filter" aria-label="Filter jobs by derived status"><option value="">STATUS</option></select><button id="quick-autonomous" class="quick-filter" type="button" aria-pressed="false">AUTONOMOUS</button><button id="quick-draft" class="quick-filter" type="button" aria-pressed="false">DRAFT</button><select id="repo-filter" class="job-filter" aria-label="Filter jobs by repository"><option value="">All repos</option></select></div><button id="jobs-refresh" class="refresh-button" type="button">Refresh</button></div></div>
|
|
162
162
|
<div class="panel-body" aria-live="polite"><ul id="job-list" class="job-list"><li class="skeleton" aria-hidden="true"></li></ul></div>
|
|
163
163
|
</section>
|
|
164
164
|
<div class="workspace" id="jobs-workspace">
|
|
@@ -207,7 +207,6 @@ const jobsWorkspace=document.querySelector('#jobs-workspace');
|
|
|
207
207
|
const jobList=document.querySelector('#job-list');
|
|
208
208
|
const jobsRefresh=document.querySelector('#jobs-refresh');
|
|
209
209
|
const repoFilter=document.querySelector('#repo-filter');
|
|
210
|
-
const quickJobs=document.querySelector('#quick-jobs');
|
|
211
210
|
const quickStatus=document.querySelector('#quick-status');
|
|
212
211
|
const quickAutonomous=document.querySelector('#quick-autonomous');
|
|
213
212
|
const quickDraft=document.querySelector('#quick-draft');
|
|
@@ -263,7 +262,6 @@ function syncFilters(){
|
|
|
263
262
|
selectedRuntimeStatus=statuses.includes(previousStatus)?previousStatus:'';
|
|
264
263
|
if(quickMode==='status'&&!selectedRuntimeStatus)quickMode='jobs';
|
|
265
264
|
quickStatus.value=quickMode==='status'?selectedRuntimeStatus:'';
|
|
266
|
-
quickJobs.setAttribute('aria-pressed',String(quickMode==='jobs'));
|
|
267
265
|
quickAutonomous.setAttribute('aria-pressed',String(quickMode==='autonomous'));
|
|
268
266
|
quickDraft.setAttribute('aria-pressed',String(quickMode==='draft'));
|
|
269
267
|
}
|
|
@@ -307,7 +305,6 @@ jobsTab.addEventListener('click',()=>{location.hash=selectedJob?'#/jobs/'+encode
|
|
|
307
305
|
jobsRefresh.addEventListener('click',()=>void refresh(true));
|
|
308
306
|
eventsAllTasks.addEventListener('click',()=>{eventTaskId=null;clearEvents();renderEventScope();void loadEventHistory();});
|
|
309
307
|
repoFilter.addEventListener('change',()=>{selectedRepo=repoFilter.value;sessionStorage.setItem('agents-relay:repo-filter',selectedRepo);renderJobList();});
|
|
310
|
-
quickJobs.addEventListener('click',()=>setQuickMode('jobs'));
|
|
311
308
|
quickAutonomous.addEventListener('click',()=>setQuickMode('autonomous'));
|
|
312
309
|
quickDraft.addEventListener('click',()=>setQuickMode('draft'));
|
|
313
310
|
quickStatus.addEventListener('change',()=>setQuickMode(quickStatus.value?'status':'jobs',quickStatus.value));
|
|
@@ -224,6 +224,31 @@ export async function resolveExecutionContext(projectPath, role, options = {}) {
|
|
|
224
224
|
delegation: { mode: 'relay-child-task', parentTaskIdRequired: role === 'nested-worker' },
|
|
225
225
|
};
|
|
226
226
|
}
|
|
227
|
+
export async function resolveAgentContract(context, agentName) {
|
|
228
|
+
const agent = context.agents.find(candidate => candidate.name === agentName);
|
|
229
|
+
if (!agent)
|
|
230
|
+
throw new Error(`Agent ${agentName} is not visible in the resolved execution context`);
|
|
231
|
+
if (!agent.path)
|
|
232
|
+
throw new Error(`Agent ${agentName} has no readable Markdown contract`);
|
|
233
|
+
const definition = await readFile(agent.path, 'utf8');
|
|
234
|
+
if (!definition.trim())
|
|
235
|
+
throw new Error(`Agent ${agentName} Markdown contract is empty`);
|
|
236
|
+
return { agent, definition };
|
|
237
|
+
}
|
|
238
|
+
export async function agentContractPrompt(context, agentName) {
|
|
239
|
+
const { agent, definition } = await resolveAgentContract(context, agentName);
|
|
240
|
+
return `[Selected task agent contract]
|
|
241
|
+
Agent: ${agent.name}
|
|
242
|
+
Source: ${agent.path ?? agent.source}
|
|
243
|
+
|
|
244
|
+
The following Markdown is the authoritative behavioral contract for this Task agent.
|
|
245
|
+
Follow it in addition to the shared Job/Task and events-bus contracts. Worker/model routing
|
|
246
|
+
does not change the selected agent identity.
|
|
247
|
+
|
|
248
|
+
--- BEGIN AGENT CONTRACT ---
|
|
249
|
+
${definition.trim()}
|
|
250
|
+
--- END AGENT CONTRACT ---`;
|
|
251
|
+
}
|
|
227
252
|
export function executionContextPrompt(context) {
|
|
228
253
|
const instructions = context.instructions.map(item => `- ${item.path} [${item.source}]`).join('\n') || '- none';
|
|
229
254
|
const skills = context.skills.map(item => `- ${item.name}: ${item.path} [${item.source}]`).join('\n') || '- none';
|
package/dist/markers.js
CHANGED
|
@@ -32,10 +32,18 @@ function contentBeforeDelimiter(body, heading, end) {
|
|
|
32
32
|
return undefined;
|
|
33
33
|
return body.slice(contentStart, endAt);
|
|
34
34
|
}
|
|
35
|
+
function jobMetadata(job) {
|
|
36
|
+
const { objective: _objective, description: _description, tasks: _tasks, ...metadata } = job;
|
|
37
|
+
return metadata;
|
|
38
|
+
}
|
|
39
|
+
export function renderJobBody(job) {
|
|
40
|
+
const objective = job.objective?.trim() || '_No objective recorded._';
|
|
41
|
+
return `${marker(JOB_MARKER, jobMetadata(job))}
|
|
42
|
+
${objective}`;
|
|
43
|
+
}
|
|
35
44
|
export function renderJobComment(job) {
|
|
36
45
|
const objective = job.objective?.trim() || '_No objective recorded._';
|
|
37
|
-
|
|
38
|
-
return `${marker(JOB_MARKER, metadata)}
|
|
46
|
+
return `${marker(JOB_MARKER, jobMetadata(job))}
|
|
39
47
|
# Agents Relay job: ${job.title}
|
|
40
48
|
|
|
41
49
|
- **State:** ${job.state}
|
|
@@ -70,7 +78,9 @@ export function parseJob(body) {
|
|
|
70
78
|
const job = parseMarker(body, JOB_MARKER);
|
|
71
79
|
if (!job)
|
|
72
80
|
return null;
|
|
73
|
-
const
|
|
81
|
+
const markerEnd = body.indexOf('-->');
|
|
82
|
+
const bodyObjective = markerEnd >= 0 ? body.slice(markerEnd + 3).trim() || undefined : undefined;
|
|
83
|
+
const objective = job.objective ?? job.description ?? contentBeforeDelimiter(body, '\n## Objective\n\n', JOB_OBJECTIVE_END) ?? markdownSection(body, '\n## Objective\n') ?? bodyObjective;
|
|
74
84
|
const parsed = { ...job, objective };
|
|
75
85
|
if (parsed.executionMode === undefined)
|
|
76
86
|
return { ...parsed, executionMode: 'fixed' };
|
package/dist/planner.js
CHANGED
|
@@ -9,7 +9,7 @@ export function planningSpec(task) {
|
|
|
9
9
|
throw new Error(`Initial task ${task.id} requires an output contract`);
|
|
10
10
|
if (!['codex', 'chatgpt', 'orchestrator'].includes(task.adapter))
|
|
11
11
|
throw new Error(`Initial task ${task.id} requires a supported adapter`);
|
|
12
|
-
return { id: task.id, input: task.input, priority: task.priority, storySize: task.storySize, projectName: task.projectName, agentName: task.agentName, 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 };
|
|
12
|
+
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 };
|
|
13
13
|
}
|
|
14
14
|
function record(value) {
|
|
15
15
|
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
@@ -126,7 +126,7 @@ export function parsePlannerResult(value) {
|
|
|
126
126
|
throw new Error(`Planner next_tasks[${index}].timeoutMs is invalid`);
|
|
127
127
|
return {
|
|
128
128
|
id, input: task.input, priority, storySize,
|
|
129
|
-
projectName: optionalString(task.projectName, 'projectName'), agentName: optionalString(task.agentName, 'agentName'),
|
|
129
|
+
projectName: optionalString(task.projectName, 'projectName'), agentName: optionalString(task.agentName, 'agentName'), agentGraph: optionalString(task.agentGraph, 'agentGraph'),
|
|
130
130
|
dependencies: stringArray(task.dependencies, 'dependencies'), capabilities: stringArray(task.capabilities, 'capabilities'), workerRequirements: requirements,
|
|
131
131
|
adapter: adapter, output: output, routing: route,
|
|
132
132
|
continuation: task.continuation, maxAttempts, timeoutMs
|
package/dist/reconciler.js
CHANGED
|
@@ -5,16 +5,19 @@ import { implementationComplete } from './runtime-status.js';
|
|
|
5
5
|
import { LaunchFailure, StartAckTimeout } from './adapters.js';
|
|
6
6
|
import { eventFor } from './events.js';
|
|
7
7
|
import { parsePlannerResult, plannerInput } from './planner.js';
|
|
8
|
-
import { executionContextPrompt, resolveExecutionContext } from './execution-context.js';
|
|
8
|
+
import { agentContractPrompt, executionContextPrompt, resolveExecutionContext } from './execution-context.js';
|
|
9
9
|
import { selectReviewer } from './reviewer-routing.js';
|
|
10
|
+
import { agentGraphPrompt } from './agent-graph.js';
|
|
10
11
|
import { selectWorker, usageForProvider, workerCandidates } from './worker-routing.js';
|
|
11
12
|
const PRIORITY_RANK = { P0: 0, P1: 1, P2: 2, P3: 3 };
|
|
12
13
|
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); }
|
|
13
14
|
export function executionEventSource(jobId, taskId, executionId) { return `job/${jobId}/task/${taskId}/execution/${executionId}`; }
|
|
14
15
|
function eventSourceId(event) { return typeof event.source?.id === 'string' ? event.source.id : undefined; }
|
|
15
|
-
export function managedWorkerInput(job, task, context) {
|
|
16
|
+
export async function managedWorkerInput(job, task, context) {
|
|
16
17
|
if (!['codex', 'chatgpt'].includes(task.adapter) || !job.repository || job.prNumber <= 0)
|
|
17
18
|
return task.input;
|
|
19
|
+
const selectedAgent = task.agentName && context ? await agentContractPrompt(context, task.agentName) : '';
|
|
20
|
+
const selectedGraph = task.agentGraph && context ? await agentGraphPrompt(context, task.agentGraph) : '';
|
|
18
21
|
if (!task.output)
|
|
19
22
|
throw new Error(`Task ${task.id} requires an explicit output contract`);
|
|
20
23
|
if (!task.executionId)
|
|
@@ -34,7 +37,7 @@ Project: ${task.projectName ?? context?.project.name ?? 'unspecified'}
|
|
|
34
37
|
|
|
35
38
|
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.
|
|
36
39
|
|
|
37
|
-
${context ? executionContextPrompt(context) + '\n\n' : ''}Follow the resolved project instructions and Agents Relay/events-bus worker contracts.
|
|
40
|
+
${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.
|
|
38
41
|
|
|
39
42
|
[Output contract]
|
|
40
43
|
${output}
|
|
@@ -243,7 +246,7 @@ export class Reconciler {
|
|
|
243
246
|
const now = new Date().toISOString();
|
|
244
247
|
const implementationTasks = job.tasks.filter(item => item.agentName !== 'reviewer' && !this.isSupersededTask(item));
|
|
245
248
|
const reviewerRouting = await this.selectReviewer(implementationTasks);
|
|
246
|
-
const task = { jobId: job.id, id, kind: 'work', priority: job.priority, storySize: 'M', projectName: job.title, agentName: 'reviewer', parentTaskId: null, dependencies: implementationTasks.map(item => item.id), capabilities: ['review'], workerSelection: reviewerRouting.selection, adapter: reviewerRouting.candidate.adapter, input: `Review PR #${job.prNumber} at head SHA ${headSha}.
|
|
249
|
+
const task = { jobId: job.id, id, kind: 'work', priority: job.priority, storySize: 'M', projectName: job.title, agentName: 'reviewer', parentTaskId: null, dependencies: implementationTasks.map(item => item.id), capabilities: ['review'], workerSelection: reviewerRouting.selection, adapter: reviewerRouting.candidate.adapter, input: `Review existing PR #${job.prNumber} at exactly head SHA ${headSha}. Job objective: ${job.objective ?? job.description ?? job.title}. Use the selected reviewer agent contract as the authoritative review protocol.`, output: { kind: 'task_pr' }, routing: reviewerRouting.candidate.routing, continuation: null, continuationDeliveredAt: null, reviewHeadSha: headSha, state: 'QUEUED', attempt: 0, maxAttempts: this.options.reviewer?.maxAttempts ?? 2, leaseOwner: null, leaseExpiresAt: null, executionId: null, threadId: null, result: null, plannerResult: null, error: null, timeoutMs: this.options.reviewer?.timeoutMs ?? storySizeTimeoutMs('M'), createdAt: now, updatedAt: now };
|
|
247
250
|
await this.store.appendTask(task);
|
|
248
251
|
job.review = { outcome: 'pending', headSha, taskId: id, evidence: null, updatedAt: now };
|
|
249
252
|
job.updatedAt = now;
|
|
@@ -505,7 +508,7 @@ export class Reconciler {
|
|
|
505
508
|
const controller = new AbortController();
|
|
506
509
|
let execution;
|
|
507
510
|
const sourceId = executionEventSource(job.id, task.id, durableExecutionId);
|
|
508
|
-
const workerInput = managedWorkerInput(job, task, executionContext);
|
|
511
|
+
const workerInput = await managedWorkerInput(job, task, executionContext);
|
|
509
512
|
const workerTask = { ...task, input: workerInput, routing: task.routing ? { ...task.routing, cwd: task.routing.cwd ?? executionContext.project.path } : task.routing };
|
|
510
513
|
const startAckSetup = task.adapter === 'chatgpt' ? await this.startAck(job.id, task.id, sourceId, Date.now(), this.options.startAckTimeoutMs ?? chatGptStartAckTimeoutMs(task.timeoutMs)) : null;
|
|
511
514
|
const closePolicy = task.chatgptTabClosePolicy ?? this.options.chatgptTabClosePolicy ?? adapter.tabClosePolicy ?? 'after-start';
|
|
@@ -676,7 +679,7 @@ export class Reconciler {
|
|
|
676
679
|
}
|
|
677
680
|
plannerChild(job, planner, spec) {
|
|
678
681
|
const now = new Date().toISOString();
|
|
679
|
-
return { jobId: job.id, id: spec.id, kind: 'work', priority: spec.priority ?? job.priority, storySize: spec.storySize ?? 'M', projectName: spec.projectName, agentName: spec.agentName, 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 };
|
|
682
|
+
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 };
|
|
680
683
|
}
|
|
681
684
|
clearExecution(taskId, executionId) {
|
|
682
685
|
const live = this.live.get(taskId);
|
|
@@ -956,5 +959,5 @@ export class Reconciler {
|
|
|
956
959
|
} }
|
|
957
960
|
}
|
|
958
961
|
function samePlannerChildDefinition(left, right) {
|
|
959
|
-
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, 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, dependencies: right.dependencies, capabilities: right.capabilities, adapter: right.adapter, routing: right.routing, continuation: right.continuation ?? null, maxAttempts: right.maxAttempts, timeoutMs: right.timeoutMs });
|
|
962
|
+
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 });
|
|
960
963
|
}
|
package/dist/store.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import { createSign } from 'node:crypto';
|
|
3
|
-
import { parseAgent, parseJob, parseTask, renderAgentComment, renderJobComment, renderTaskComment, AGENT_MARKER, JOB_MARKER, TASK_MARKER } from './markers.js';
|
|
3
|
+
import { parseAgent, parseJob, parseTask, renderAgentComment, renderJobBody, renderJobComment, renderTaskComment, AGENT_MARKER, JOB_MARKER, TASK_MARKER } from './markers.js';
|
|
4
4
|
export async function loadManagedGitHubPullRequestGraphql(client, repository, prNumber, trustedAuthors) {
|
|
5
5
|
const [owner, name] = repository.split('/', 2);
|
|
6
6
|
if (!owner || !name || !Number.isInteger(prNumber) || prNumber <= 0 || !client.graphql)
|
|
@@ -17,7 +17,9 @@ export async function loadManagedGitHubPullRequestGraphql(client, repository, pr
|
|
|
17
17
|
const author = row.author;
|
|
18
18
|
return typeof author?.login === 'string' && trustedAuthors.has(author.login);
|
|
19
19
|
});
|
|
20
|
-
const
|
|
20
|
+
const bodyJob = parseJob(typeof pr.body === 'string' ? pr.body : '');
|
|
21
|
+
const commentJobs = trusted.map(item => parseJob(String(item.body ?? ''))).filter((item) => item !== null);
|
|
22
|
+
const jobs = bodyJob ? [bodyJob] : commentJobs;
|
|
21
23
|
const tasks = trusted.map(item => parseTask(String(item.body ?? ''))).filter((item) => item !== null);
|
|
22
24
|
const state = String(pr.state ?? '').toUpperCase();
|
|
23
25
|
const githubState = pr.merged === true ? 'MERGED' : state === 'CLOSED' ? 'CLOSED' : 'OPEN';
|
|
@@ -83,7 +85,7 @@ function immutableTaskDefinition(task) {
|
|
|
83
85
|
reasoning: task.routing.reasoning, cwd: task.routing.cwd, projectId: task.routing.projectId
|
|
84
86
|
} : undefined;
|
|
85
87
|
return {
|
|
86
|
-
jobId: task.jobId, id: task.id, priority: task.priority, storySize: task.storySize ?? 'M', projectName: task.projectName, agentName: task.agentName,
|
|
88
|
+
jobId: task.jobId, id: task.id, priority: task.priority, storySize: task.storySize ?? 'M', projectName: task.projectName, agentName: task.agentName, agentGraph: task.agentGraph,
|
|
87
89
|
kind: task.kind ?? 'work', planningDecisionId: task.planningDecisionId, planningSource: task.planningSource, planningRound: task.planningRound, parentTaskId: task.parentTaskId, dependencies: task.dependencies, capabilities: task.capabilities, workerRequirements: task.workerRequirements, workerSelection: task.workerSelection, adapter: task.adapter,
|
|
88
90
|
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
|
|
89
91
|
};
|
|
@@ -424,13 +426,17 @@ export class GitHubStore {
|
|
|
424
426
|
}
|
|
425
427
|
async prNode() {
|
|
426
428
|
const { owner, name } = this.repoParts();
|
|
427
|
-
const data = await this.graphql('query($owner:String!,$name:String!,$number:Int!){repository(owner:$owner,name:$name){pullRequest(number:$number){id state merged mergedAt closedAt}}}', { owner, name, number: this.prNumber });
|
|
429
|
+
const data = await this.graphql('query($owner:String!,$name:String!,$number:Int!){repository(owner:$owner,name:$name){pullRequest(number:$number){id state merged mergedAt closedAt body}}}', { owner, name, number: this.prNumber });
|
|
428
430
|
const repository = data.repository;
|
|
429
431
|
const pr = repository?.pullRequest;
|
|
430
432
|
if (!pr)
|
|
431
433
|
throw new Error(`Pull request #${this.prNumber} not found in ${this.repository}`);
|
|
432
434
|
return pr;
|
|
433
435
|
}
|
|
436
|
+
async pullRequestBody() {
|
|
437
|
+
const pr = await this.prNode();
|
|
438
|
+
return typeof pr.body === 'string' ? pr.body : '';
|
|
439
|
+
}
|
|
434
440
|
async list() {
|
|
435
441
|
const { owner, name } = this.repoParts();
|
|
436
442
|
const all = [];
|
|
@@ -490,10 +496,16 @@ export class GitHubStore {
|
|
|
490
496
|
}
|
|
491
497
|
async load(jobId) {
|
|
492
498
|
const comments = (await this.list()).filter(comment => this.isTrusted(comment));
|
|
493
|
-
const
|
|
499
|
+
const bodyJob = parseJob(await this.pullRequestBody());
|
|
500
|
+
const commentJobs = comments.map(x => parseJob(x.body)).filter((x) => x !== null && typeof x.id === 'string' && x.id.length > 0);
|
|
501
|
+
const jobs = bodyJob ? [bodyJob] : commentJobs;
|
|
494
502
|
const job = jobs.find(x => x.id === jobId);
|
|
495
|
-
if (!job)
|
|
503
|
+
if (!job) {
|
|
504
|
+
const conflicting = bodyJob ?? commentJobs[0];
|
|
505
|
+
if (conflicting)
|
|
506
|
+
throw new Error(`JOB_ID_MISMATCH: PR #${this.prNumber} is bound to ${conflicting.id}, not ${jobId}`);
|
|
496
507
|
throw new Error(`JOB_MARKER_MISSING: Managed GitHub job ${jobId} has no durable ${JOB_MARKER} marker in PR #${this.prNumber}`);
|
|
508
|
+
}
|
|
497
509
|
const soleTrustedJob = jobs.length === 1;
|
|
498
510
|
job.tasks = comments.map(comment => {
|
|
499
511
|
const task = parseTask(comment.body);
|
|
@@ -505,6 +517,16 @@ export class GitHubStore {
|
|
|
505
517
|
return job;
|
|
506
518
|
}
|
|
507
519
|
async validateJobBinding(jobId) {
|
|
520
|
+
const body = await this.pullRequestBody();
|
|
521
|
+
const bodyHasMarker = body.includes(`<!-- ${JOB_MARKER} `);
|
|
522
|
+
const bodyJob = parseJob(body);
|
|
523
|
+
if (bodyHasMarker && !bodyJob)
|
|
524
|
+
throw new Error(`JOB_MARKER_MALFORMED: PR #${this.prNumber} body contains a ${JOB_MARKER} marker that cannot be parsed`);
|
|
525
|
+
if (bodyJob) {
|
|
526
|
+
if (bodyJob.id !== jobId)
|
|
527
|
+
throw new Error(`JOB_ID_MISMATCH: PR #${this.prNumber} is bound to ${bodyJob.id}, not ${jobId}`);
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
508
530
|
const comments = (await this.list()).filter(comment => this.isTrusted(comment));
|
|
509
531
|
const markerComments = comments.filter(comment => comment.body.includes(JOB_MARKER));
|
|
510
532
|
const parsed = markerComments.map(comment => parseJob(comment.body));
|
|
@@ -539,22 +561,21 @@ export class GitHubStore {
|
|
|
539
561
|
await this.graphql('mutation($id:ID!){deleteIssueComment(input:{id:$id}){clientMutationId}}', { id: comment.nodeId });
|
|
540
562
|
}
|
|
541
563
|
async saveJob(job) {
|
|
564
|
+
const currentBody = await this.pullRequestBody();
|
|
565
|
+
const bodyJob = parseJob(currentBody);
|
|
566
|
+
if (bodyJob && bodyJob.id !== job.id)
|
|
567
|
+
throw new Error(`PR #${this.prNumber} body already has a different ${JOB_MARKER} marker`);
|
|
542
568
|
const comments = (await this.list()).filter(comment => this.isTrusted(comment));
|
|
543
569
|
const jobComments = comments.filter(comment => parseJob(comment.body) !== null);
|
|
544
570
|
const conflicts = jobComments.filter(comment => parseJob(comment.body)?.id !== job.id);
|
|
545
571
|
if (conflicts.length > 0)
|
|
546
572
|
throw new Error(`PR #${this.prNumber} already has a different ${JOB_MARKER} marker`);
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
await this.deleteComment(duplicate);
|
|
554
|
-
}
|
|
555
|
-
const durable = (await this.list()).filter(comment => this.isTrusted(comment)).filter(comment => parseJob(comment.body)?.id === job.id);
|
|
556
|
-
if (durable.length !== 1)
|
|
557
|
-
throw new Error(`Failed to persist exactly one ${JOB_MARKER} marker for job ${job.id}`);
|
|
573
|
+
await this.updatePullRequestBody(renderJobBody(job));
|
|
574
|
+
const persisted = parseJob(await this.pullRequestBody());
|
|
575
|
+
if (!persisted || persisted.id !== job.id)
|
|
576
|
+
throw new Error(`Failed to persist ${JOB_MARKER} marker in PR #${this.prNumber} body for job ${job.id}`);
|
|
577
|
+
for (const legacy of jobComments.filter(comment => parseJob(comment.body)?.id === job.id))
|
|
578
|
+
await this.deleteComment(legacy);
|
|
558
579
|
}
|
|
559
580
|
async appendTask(task) {
|
|
560
581
|
if (!task.jobId)
|
|
@@ -615,7 +636,9 @@ export async function listManagedGitHubJobs(client, repository, trustedAuthors)
|
|
|
615
636
|
const author = item.author;
|
|
616
637
|
return typeof author?.login === 'string' && trustedAuthors.has(author.login);
|
|
617
638
|
});
|
|
618
|
-
const
|
|
639
|
+
const bodyJob = parseJob(typeof pr.body === 'string' ? pr.body : '');
|
|
640
|
+
const commentJobs = trusted.map(item => parseJob(String(item.body ?? ''))).filter((item) => item !== null);
|
|
641
|
+
const jobs = bodyJob ? [bodyJob] : commentJobs;
|
|
619
642
|
const tasks = trusted.map(item => parseTask(String(item.body ?? ''))).filter((item) => item !== null);
|
|
620
643
|
const state = String(pr.state ?? '').toUpperCase();
|
|
621
644
|
const githubState = pr.merged === true || state === 'MERGED' ? 'MERGED' : state === 'CLOSED' ? 'CLOSED' : 'OPEN';
|
package/package.json
CHANGED
|
@@ -6,7 +6,7 @@ This contract is the shared protocol for every component that creates, schedules
|
|
|
6
6
|
|
|
7
7
|
Job objectives and Task inputs are **Markdown** and are durable execution context, not short labels. Use Mermaid where a diagram materially explains a workflow, state model, architecture, dependency graph, relationship, or other visual structure. **Mermaid-first applies to that explanation/section, not to the entire Job objective or Task input.** Put the diagram before the prose that explains that visual model, but keep normal document structure: start with the goal/context when that is what the reader needs first. Do not force a Mermaid block at the top of every objective/input, and do not add decorative or redundant diagrams when Markdown is clearer.
|
|
8
8
|
|
|
9
|
-
The trusted machine-readable Job/Task marker stores lifecycle and execution metadata only. Do **not** duplicate the Job objective or Task input inside
|
|
9
|
+
The trusted machine-readable Job/Task marker stores lifecycle and execution metadata only. Do **not** duplicate the Job objective or Task input inside marker JSON. The Job marker lives in the PR body immediately before the human-readable Job objective; Task markers live in their Task comments immediately before the human-readable Task input/result. Readers reconstruct `objective` / `input` from that visible Markdown while remaining backward-compatible with legacy markers that embedded long-form content or stored the Job marker in a separate comment.
|
|
10
10
|
|
|
11
11
|
For long-form content, use files instead of shell-quoted arguments:
|
|
12
12
|
|
|
@@ -15,7 +15,7 @@ For long-form content, use files instead of shell-quoted arguments:
|
|
|
15
15
|
|
|
16
16
|
When creating a new Job from an active discussion, the orchestrator must consolidate **all context needed to finish the Job without the originating thread** into the Job objective before creation. Include, as applicable: goal, relevant background, decisions already made, requirements, constraints, lifecycle/workflow semantics, non-goals, implementation boundaries, acceptance criteria, delivery/review instructions, and any important known issues. Do not create a Job whose objective is only a one-line summary when the discussion contains material implementation context.
|
|
17
17
|
|
|
18
|
-
For a newly created PR (`job create --head`), standalone objective Markdown is required
|
|
18
|
+
For a newly created PR (`job create --head`), standalone objective Markdown is required. The PR body is the single durable Job surface: one hidden `agents-relay:job:v1` marker followed by the objective Markdown. Updating Job metadata or objective updates that same body. New Jobs must not create a separate Job comment that repeats the objective. Readers must remain backward-compatible with legacy Job-comment markers, and a successful Job save may migrate such a legacy marker into the PR body only after validating the replacement before deleting the redundant legacy Job comment.
|
|
19
19
|
|
|
20
20
|
## Task producers
|
|
21
21
|
|