agents-relay 2.0.7 → 2.0.9

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 CHANGED
@@ -193,6 +193,8 @@ External product feedback uses the reusable `src/feedback.ts` contract. See [doc
193
193
 
194
194
  reconcile leases and launches ready tasks without waiting inside the runtime scheduler; the CLI waits only long enough for its launched workers to persist results. agents-relayd (or the equivalent npx agents-relay serve) is the watchdog/dashboard mode. Model-backed workers require --events nats; the event transport carries authoritative terminal worker events as well as wake/progress traffic. See [docs/service.md](docs/service.md). --file PATH is explicit local demo/test mode only.
195
195
 
196
+ Router Rules are stored at `~/.agents-relay/execution-router-rules.md` by default. `agents-relayd --execution-router-rules PATH` selects a different file for that daemon process; the explicit CLI path takes precedence over `AGENTS_RELAY_EXECUTION_ROUTER_RULES`, which remains supported for compatibility.
197
+
196
198
  `agents-relayd` without `--pr`/`--id` runs in workspace mode by default: it discovers nested Git repositories under `~/Workspace`, keeps only GitHub `origin` remotes, and aggregates their managed PR jobs into one dashboard and worker pool. Pass `--workspace PATH` to use another root. `--repo OWNER/REPO` remains the single-repository pool mode.
197
199
 
198
200
  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 local-first: targeted webhook reconciliation is primary, dashboard auto-refresh reads the disposable in-process view restored from SQLite RelayStore state, and the broad watchdog drops to a 30-minute fallback. No separate persisted dashboard Job cache is maintained. See [docs/github-app-rate-limit.md](docs/github-app-rate-limit.md).
@@ -91,7 +91,20 @@ export const dashboardHtml = `<!doctype html>
91
91
  .event-meta { display:flex; flex-wrap:wrap; gap:.4rem; color:var(--muted); font-size:.68rem; }
92
92
  .event-kind { color:var(--accent); font-weight:800; }
93
93
  .event-message { margin:.12rem 0 0; overflow-wrap:anywhere; }
94
- #usage-view,#rules-view,#shortcuts-view,#worker-rules-view { grid-column:1 / -1; }
94
+ #usage-view,#graphs-view,#rules-view,#shortcuts-view,#worker-rules-view { grid-column:1 / -1; }
95
+ .graphs-browser { display:grid; grid-template-columns:minmax(190px,260px) minmax(0,1fr); gap:.75rem; min-height:60vh; }
96
+ .graph-list { display:grid; align-content:start; gap:.35rem; }
97
+ .graph-button { width:100%; display:grid; gap:.08rem; padding:.48rem .55rem; border:1px solid var(--border); border-radius:.4rem; background:var(--surface2); color:var(--text); text-align:left; font:inherit; cursor:pointer; }
98
+ .graph-button[aria-current="true"] { border-color:var(--accent); background:var(--accent2); }
99
+ .graph-button strong { font-size:.78rem; }
100
+ .graph-button span { color:var(--muted); font:.66rem ui-monospace,SFMono-Regular,Menlo,monospace; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
101
+ .graph-preview { min-width:0; display:grid; align-content:start; gap:.55rem; }
102
+ .graph-preview-head { display:flex; justify-content:space-between; gap:.7rem; align-items:baseline; flex-wrap:wrap; }
103
+ .graph-preview-head h3 { margin:0; font-size:.95rem; }
104
+ .graph-canvas { min-height:24rem; display:grid; place-items:center; overflow:auto; padding:1rem; border:1px solid var(--border); border-radius:.45rem; background:var(--surface2); }
105
+ .graph-canvas svg { max-width:none; height:auto; }
106
+ .graph-source { margin:0; max-height:22rem; overflow:auto; padding:.7rem; border:1px solid var(--border); border-radius:.4rem; background:var(--surface2); color:var(--muted); font:.72rem/1.5 ui-monospace,SFMono-Regular,Menlo,monospace; white-space:pre; }
107
+ .graph-source-details summary { cursor:pointer; color:var(--muted); font-size:.74rem; font-weight:700; }
95
108
  .rules-editor { display:grid; gap:.65rem; }
96
109
  .rules-help { margin:0; color:var(--muted); font-size:.8rem; }
97
110
  .rules-textarea { width:100%; min-height:55vh; resize:vertical; border:1px solid var(--border); border-radius:.45rem; background:var(--surface2); color:var(--text); padding:.8rem; font:13px/1.55 ui-monospace,SFMono-Regular,Menlo,monospace; }
@@ -135,6 +148,9 @@ export const dashboardHtml = `<!doctype html>
135
148
  #jobs-workspace .panel-body { max-height:none; }
136
149
  #usage-view .panel-head { padding:.6rem .7rem .35rem; }
137
150
  #usage-view .panel-head .subtle { display:none; }
151
+ .graphs-browser { grid-template-columns:1fr; min-height:0; }
152
+ .graph-list { grid-template-columns:repeat(2,minmax(0,1fr)); }
153
+ .graph-canvas { min-height:18rem; }
138
154
  #usage-view .panel-body { padding:.45rem .5rem .65rem; max-height:none; }
139
155
  #usage-view .usage-providers { gap:.45rem; }
140
156
  #usage-view .usage-provider {
@@ -170,7 +186,8 @@ export const dashboardHtml = `<!doctype html>
170
186
  <nav role="tablist" aria-label="Console views">
171
187
  <button id="jobs-tab" type="button" role="tab" aria-selected="true" aria-controls="jobs-view">Jobs</button>
172
188
  <button id="usage-tab" type="button" role="tab" aria-selected="false" aria-controls="usage-view">Usage</button>
173
- <button id="rules-tab" type="button" role="tab" aria-selected="false" aria-controls="rules-view">Execution Rules</button>
189
+ <button id="graphs-tab" type="button" role="tab" aria-selected="false" aria-controls="graphs-view">Graphs</button>
190
+ <button id="rules-tab" type="button" role="tab" aria-selected="false" aria-controls="rules-view">Router Rules</button>
174
191
  <button id="shortcuts-tab" type="button" role="tab" aria-selected="false" aria-controls="shortcuts-view">Shortcuts</button>
175
192
  </nav>
176
193
  </header>
@@ -201,9 +218,24 @@ export const dashboardHtml = `<!doctype html>
201
218
  <div class="panel-body" id="usage-root" aria-live="polite"><div class="skeleton"></div></div>
202
219
  </div>
203
220
  </section>
221
+ <section id="graphs-view" role="tabpanel" aria-labelledby="graphs-tab" hidden>
222
+ <div class="panel">
223
+ <div class="panel-head"><h2>Workflow Graphs</h2><span id="graphs-meta" class="subtle"></span></div>
224
+ <div class="panel-body">
225
+ <div class="graphs-browser">
226
+ <div id="graph-list" class="graph-list" aria-label="Workflow graphs"></div>
227
+ <div class="graph-preview">
228
+ <div class="graph-preview-head"><h3 id="graph-title">Select a graph</h3><span id="graph-path" class="subtle"></span></div>
229
+ <div id="graph-canvas" class="graph-canvas"><p class="empty">Select a graph to inspect it.</p></div>
230
+ <details class="graph-source-details"><summary>Mermaid source</summary><pre id="graph-source" class="graph-source"></pre></details>
231
+ </div>
232
+ </div>
233
+ </div>
234
+ </div>
235
+ </section>
204
236
  <section id="rules-view" role="tabpanel" aria-labelledby="rules-tab" hidden>
205
237
  <div class="panel">
206
- <div class="panel-head"><h2>Execution Router Rules</h2><span id="rules-meta" class="subtle"></span></div>
238
+ <div class="panel-head"><h2>Router Rules</h2><span id="rules-meta" class="subtle"></span></div>
207
239
  <div class="panel-body">
208
240
  <div class="rules-editor">
209
241
  <p class="rules-help">Free-text soft policy loaded by the Execution Router for every worker+model routing decision. Impossible routes remain excluded by code; Codex remains the deterministic fallback if decision fails.</p>
@@ -226,6 +258,7 @@ export const dashboardHtml = `<!doctype html>
226
258
  </div>
227
259
  </section>
228
260
  </main>
261
+ <script src="https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js"></script>
229
262
  <script>
230
263
  'use strict';
231
264
  // Compatibility contracts retained for dashboard tests:
@@ -244,10 +277,12 @@ const MAX_EVENTS=60;
244
277
  const DASHBOARD_VERSION=${JSON.stringify(dashboardVersion)};
245
278
  const jobsTab=document.querySelector('#jobs-tab');
246
279
  const usageTab=document.querySelector('#usage-tab');
280
+ const graphsTab=document.querySelector('#graphs-tab');
247
281
  const rulesTab=document.querySelector('#rules-tab');
248
282
  const shortcutsTab=document.querySelector('#shortcuts-tab');
249
283
  const jobsView=document.querySelector('#jobs-view');
250
284
  const usageView=document.querySelector('#usage-view');
285
+ const graphsView=document.querySelector('#graphs-view');
251
286
  const rulesView=document.querySelector('#rules-view');
252
287
  const shortcutsView=document.querySelector('#shortcuts-view');
253
288
  const jobsWorkspace=document.querySelector('#jobs-workspace');
@@ -271,6 +306,12 @@ const connection=document.querySelector('#connection');
271
306
  const selectedJobTitle=document.querySelector('#selected-job-title');
272
307
  const usageRoot=document.querySelector('#usage-root');
273
308
  const usageMeta=document.querySelector('#usage-meta');
309
+ const graphsMeta=document.querySelector('#graphs-meta');
310
+ const graphList=document.querySelector('#graph-list');
311
+ const graphTitle=document.querySelector('#graph-title');
312
+ const graphPath=document.querySelector('#graph-path');
313
+ const graphCanvas=document.querySelector('#graph-canvas');
314
+ const graphSource=document.querySelector('#graph-source');
274
315
  const rulesText=document.querySelector('#rules-text');
275
316
  const rulesMeta=document.querySelector('#rules-meta');
276
317
  const rulesStatus=document.querySelector('#rules-status');
@@ -287,6 +328,8 @@ let selectedRepo=sessionStorage.getItem('agents-relay:repo-filter')||'';
287
328
  let quickMode=sessionStorage.getItem('agents-relay:quick-filter')||'jobs';
288
329
  let selectedRuntimeStatus=sessionStorage.getItem('agents-relay:runtime-status-filter')||'';
289
330
  let searchQuery=sessionStorage.getItem('agents-relay:job-search')||'';
331
+ let workflowGraphs=[];
332
+ let selectedGraphId=sessionStorage.getItem('agents-relay:selected-graph')||'';
290
333
  let eventTaskId=selectedTaskId;
291
334
  const collapsedTasks=new Set();
292
335
  const expandedTasks=new Set();
@@ -408,11 +451,15 @@ function githubUsageCard(effective,status){
408
451
  return card;
409
452
  }
410
453
  async function drawUsage(){try{const overview=await loadJSON('/api/usage');usageRoot.replaceChildren();usageMeta.textContent='Real account telemetry when available';const providers=Array.isArray(overview.providers)?overview.providers:[];const credits=Array.isArray(overview.credits)?overview.credits:[];const grid=element('div','usage-providers');if(overview.githubEffective||overview.githubStatus)grid.append(githubUsageCard(overview.githubEffective,overview.githubStatus));for(const provider of providers)grid.append(usageProvider(provider));for(const provider of credits)grid.append(creditProvider(provider));if(!grid.children.length){usageRoot.append(element('p','empty',text('Usage telemetry is unavailable.')));return;}usageRoot.append(grid);}catch(error){usageMeta.textContent='Unavailable';usageRoot.replaceChildren(element('p','error-state',text('Usage unavailable: '+error.message)));}}
454
+ function selectedWorkflowGraph(){return workflowGraphs.find(graph=>graph.id===selectedGraphId)||workflowGraphs[0]||null;}
455
+ function renderGraphList(){graphList.replaceChildren();if(!workflowGraphs.length){graphList.append(element('p','empty',text('No workflow graphs found.')));return;}for(const graph of workflowGraphs){const button=document.createElement('button');button.type='button';button.className='graph-button';button.setAttribute('aria-current',String(graph.id===selectedGraphId));button.append(element('strong','',text(graph.name)),element('span','',text(graph.path)));button.addEventListener('click',()=>{selectedGraphId=graph.id;sessionStorage.setItem('agents-relay:selected-graph',selectedGraphId);renderGraphList();void renderSelectedGraph();});graphList.append(button);}}
456
+ async function renderSelectedGraph(){const graph=selectedWorkflowGraph();if(!graph){graphTitle.textContent='Select a graph';graphPath.textContent='';graphSource.textContent='';graphCanvas.replaceChildren(element('p','empty',text('No workflow graphs found.')));return;}selectedGraphId=graph.id;sessionStorage.setItem('agents-relay:selected-graph',selectedGraphId);graphTitle.textContent=graph.name;graphPath.textContent=graph.path;graphSource.textContent=graph.source;graphCanvas.replaceChildren(element('p','subtle',text('Rendering…')));try{if(!window.mermaid)throw new Error('Mermaid renderer unavailable');if(!window.__relayMermaidReady){window.mermaid.initialize({startOnLoad:false,securityLevel:'strict'});window.__relayMermaidReady=true;}const rendered=await window.mermaid.render('relay-graph-'+graph.id.replace(/[^A-Za-z0-9_-]/g,'-'),graph.source);const parsed=new DOMParser().parseFromString(rendered.svg,'image/svg+xml');const svg=parsed.documentElement;if(svg.nodeName.toLowerCase()!=='svg')throw new Error('Mermaid did not return SVG');graphCanvas.replaceChildren(document.importNode(svg,true));}catch(error){graphCanvas.replaceChildren(element('p','error-state',text('Visual preview unavailable: '+error.message)));}}
457
+ async function drawGraphs(){try{const graphs=await loadJSON('/api/graphs');workflowGraphs=Array.isArray(graphs)?graphs:[];if(!workflowGraphs.some(graph=>graph.id===selectedGraphId))selectedGraphId=workflowGraphs[0]?.id||'';graphsMeta.textContent=workflowGraphs.length+' graph'+(workflowGraphs.length===1?'':'s')+' · workflow/*.mmd';renderGraphList();await renderSelectedGraph();}catch(error){graphsMeta.textContent='Unavailable';workflowGraphs=[];renderGraphList();graphCanvas.replaceChildren(element('p','error-state',text('Graphs unavailable: '+error.message)));}}
411
458
  async function drawRules(){rulesStatus.textContent='';try{const rules=await loadJSON('/api/execution-router-rules');rulesText.value=typeof rules.text==='string'?rules.text:'';rulesMeta.textContent=rules.hash&&rules.hash!=='none'?'Loaded · '+rules.hash.slice(0,12):'No custom rules saved';}catch(error){rulesMeta.textContent='Unavailable';rulesStatus.textContent='Could not load rules: '+error.message;}}
412
459
  async function saveRules(){rulesSave.disabled=true;rulesStatus.textContent='Saving…';try{const response=await fetch('/api/execution-router-rules',{method:'PUT',headers:{'content-type':'text/plain; charset=utf-8'},body:rulesText.value});if(!response.ok)throw new Error(response.status+' '+await response.text());const rules=await response.json();rulesMeta.textContent='Saved · '+rules.hash.slice(0,12);rulesStatus.textContent='Saved. New routing decisions will load these rules.';}catch(error){rulesStatus.textContent='Save failed: '+error.message;}finally{rulesSave.disabled=false;}}
413
460
  async function drawShortcuts(){shortcutsStatus.textContent='';shortcutsText.value=chatShortcutText();}
414
461
  function saveShortcuts(){const value=shortcutsText.value.trim();if(value)localStorage.setItem(CHAT_SHORTCUTS_KEY,value);else localStorage.removeItem(CHAT_SHORTCUTS_KEY);shortcutsText.value=chatShortcutText();shortcutsStatus.textContent='Saved.';}
415
- function showView(view){const isJobs=view==='jobs';const isUsage=view==='usage';const isRules=view==='rules';const isShortcuts=view==='shortcuts';jobsView.hidden=!isJobs;jobsWorkspace.hidden=!isJobs;usageView.hidden=!isUsage;rulesView.hidden=!isRules;shortcutsView.hidden=!isShortcuts;jobsTab.setAttribute('aria-selected',String(isJobs));usageTab.setAttribute('aria-selected',String(isUsage));rulesTab.setAttribute('aria-selected',String(isRules));shortcutsTab.setAttribute('aria-selected',String(isShortcuts));if(isJobs)void refresh();else if(isUsage)void drawUsage();else if(isRules)void drawRules();else void drawShortcuts();}
462
+ function showView(view){const isJobs=view==='jobs';const isUsage=view==='usage';const isGraphs=view==='graphs';const isRules=view==='rules';const isShortcuts=view==='shortcuts';jobsView.hidden=!isJobs;jobsWorkspace.hidden=!isJobs;usageView.hidden=!isUsage;graphsView.hidden=!isGraphs;rulesView.hidden=!isRules;shortcutsView.hidden=!isShortcuts;jobsTab.setAttribute('aria-selected',String(isJobs));usageTab.setAttribute('aria-selected',String(isUsage));graphsTab.setAttribute('aria-selected',String(isGraphs));rulesTab.setAttribute('aria-selected',String(isRules));shortcutsTab.setAttribute('aria-selected',String(isShortcuts));if(isJobs)void refresh();else if(isUsage)void drawUsage();else if(isGraphs)void drawGraphs();else if(isRules)void drawRules();else void drawShortcuts();}
416
463
  jobsTab.addEventListener('click',()=>{location.hash=selectedJob?'#/jobs/'+encodeURIComponent(selectedJob):'#/jobs';showView('jobs');});
417
464
  jobsRefresh.addEventListener('click',()=>void refresh(true));
418
465
  eventsAllTasks.addEventListener('click',()=>{eventTaskId=null;clearEvents();renderEventScope();void loadEventHistory();});
@@ -426,19 +473,20 @@ mobileJobsBack.addEventListener('click',()=>{location.hash='#/jobs';syncMobileJo
426
473
  mobileJobs.addEventListener('change',syncMobileJobsView);
427
474
  window.addEventListener('hashchange',syncMobileJobsView);
428
475
  usageTab.addEventListener('click',()=>{location.hash='#/usage';showView('usage');});
476
+ graphsTab.addEventListener('click',()=>{location.hash='#/graphs';showView('graphs');});
429
477
  rulesTab.addEventListener('click',()=>{location.hash='#/execution-router-rules';showView('rules');});
430
478
  shortcutsTab.addEventListener('click',()=>{location.hash='#/shortcuts';showView('shortcuts');});
431
479
  rulesSave.addEventListener('click',()=>void saveRules());
432
480
  shortcutsSave.addEventListener('click',saveShortcuts);
433
- window.addEventListener('keydown',event=>{if(event.key==='Tab'&&event.altKey){event.preventDefault();const next=location.hash==='#/usage'?'rules':location.hash==='#/execution-router-rules'?'jobs':'usage';location.hash=next==='usage'?'#/usage':next==='rules'?'#/execution-router-rules':(selectedJob?'#/jobs/'+encodeURIComponent(selectedJob):'#/jobs');showView(next);}});
481
+ window.addEventListener('keydown',event=>{if(event.key==='Tab'&&event.altKey){event.preventDefault();const views=['jobs','usage','graphs','rules','shortcuts'];const current=location.hash==='#/usage'?'usage':location.hash==='#/graphs'?'graphs':location.hash==='#/execution-router-rules'?'rules':location.hash==='#/shortcuts'?'shortcuts':'jobs';const next=views[(views.indexOf(current)+1)%views.length];location.hash=next==='usage'?'#/usage':next==='graphs'?'#/graphs':next==='rules'?'#/execution-router-rules':next==='shortcuts'?'#/shortcuts':(selectedJob?'#/jobs/'+encodeURIComponent(selectedJob):'#/jobs');showView(next);}});
434
482
  function parseMessage(message){try{return JSON.parse(message.data);}catch{return null;}}
435
483
  const stream=new EventSource('/events');
436
484
  stream.onopen=()=>{connection.textContent='';connection.setAttribute('aria-label','Event stream connected');connection.className='status-pill ok';};
437
485
  stream.onerror=()=>{connection.textContent='';connection.setAttribute('aria-label','Event stream reconnecting');connection.className='status-pill warn';};
438
486
  stream.onmessage=message=>{const payload=parseMessage(message);if(payload?.job){const index=jobs.findIndex(job=>job.id===payload.job.id);if(index>=0)jobs[index]=payload.job;else jobs.push(payload.job);renderAll();void drawRuntimeSummary();}};
439
487
  stream.addEventListener('relay',message=>{const payload=parseMessage(message);if(payload?.job){const index=jobs.findIndex(job=>job.id===payload.job.id);if(index>=0)jobs[index]=payload.job;else jobs.push(payload.job);}if(payload?.event)addEvent(payload.event);renderAll();void drawRuntimeSummary();});
440
- if(location.hash==='#/usage')showView('usage');else if(location.hash==='#/execution-router-rules')showView('rules');else if(location.hash==='#/shortcuts')showView('shortcuts');else showView('jobs');
441
- setInterval(()=>{if(location.hash!=='#/usage'&&location.hash!=='#/execution-router-rules'&&location.hash!=='#/shortcuts')void refresh();},300000);
488
+ if(location.hash==='#/usage')showView('usage');else if(location.hash==='#/graphs')showView('graphs');else if(location.hash==='#/execution-router-rules')showView('rules');else if(location.hash==='#/shortcuts')showView('shortcuts');else showView('jobs');
489
+ setInterval(()=>{if(location.hash!=='#/usage'&&location.hash!=='#/graphs'&&location.hash!=='#/execution-router-rules'&&location.hash!=='#/shortcuts')void refresh();},300000);
442
490
  setInterval(()=>{if(location.hash==='#/usage')void drawUsage();},30000);
443
491
  setInterval(()=>void refreshVersion(),30000);
444
492
  async function refreshSelected(){if(!selectedJob){await refresh();return;}selectedJobRefresh.disabled=true;selectedJobRefresh.textContent='Refreshing…';try{const updated=await loadJSON('/api/job?id='+encodeURIComponent(selectedJob));const index=jobs.findIndex(job=>job.id===selectedJob);if(index>=0)jobs[index]=updated;else jobs.push(updated);rememberSelection();renderAll();connection.textContent='';connection.setAttribute('aria-label','Live durable state connected');connection.className='status-pill ok';}catch(error){connection.textContent='';connection.setAttribute('aria-label','Durable state unavailable');connection.className='status-pill bad';}finally{selectedJobRefresh.disabled=false;selectedJobRefresh.textContent='Refresh';}}
package/dist/dashboard.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import { createServer } from 'node:http';
2
- import { readFile } from 'node:fs/promises';
2
+ import { readFile, readdir } from 'node:fs/promises';
3
3
  import { homedir } from 'node:os';
4
4
  import { join } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
5
6
  import { readUsageSnapshot } from './usage.js';
6
7
  import { deriveJobRuntimeStatus } from './runtime-status.js';
7
8
  import { DeliveryDeduper, parseGitHubWebhook, verifyGitHubWebhook } from './github-webhook.js';
@@ -10,6 +11,16 @@ import { summarizeRuntime } from './runtime-summary.js';
10
11
  const GITHUB_REPOSITORY_PATTERN = /^([A-Za-z0-9](?:[A-Za-z0-9-]{0,36}[A-Za-z0-9])?)\/([A-Za-z0-9._-]{1,100})$/;
11
12
  const SSE_HEARTBEAT_MS = 15000;
12
13
  const EVENTS_HISTORY_FILE = process.env.EVENTS_BUS_DATA_DIR ? join(process.env.EVENTS_BUS_DATA_DIR, 'events.jsonl') : join(homedir(), 'Library', 'Application Support', 'EventsBus', 'events.jsonl');
14
+ const WORKFLOW_DIR = fileURLToPath(new URL('../workflow/', import.meta.url));
15
+ export async function dashboardWorkflowGraphs() {
16
+ const names = (await readdir(WORKFLOW_DIR)).filter(name => name.endsWith('.mmd')).sort();
17
+ return Promise.all(names.map(async (name) => ({
18
+ id: name.replace(/\.mmd$/, ''),
19
+ name: name.replace(/\.mmd$/, '').split('-').map(part => part === 'v2' ? 'V2' : part.charAt(0).toUpperCase() + part.slice(1)).join(' '),
20
+ path: `workflow/${name}`,
21
+ source: await readFile(join(WORKFLOW_DIR, name), 'utf8'),
22
+ })));
23
+ }
13
24
  function effectiveGitHubUsage(snapshot, runtime) {
14
25
  const candidates = [snapshot?.resources.core, snapshot?.resources.graphql, snapshot?.resources.search, runtime?.resources.core, runtime?.resources.graphql, runtime?.resources.search]
15
26
  .filter((candidate) => candidate !== undefined && candidate !== null)
@@ -171,6 +182,18 @@ export function serveDashboard(store, port = 8787, bus, refreshMs = 300000, jobI
171
182
  res.end(JSON.stringify({ version: dashboardVersion }));
172
183
  return;
173
184
  }
185
+ if (req.url === '/api/graphs') {
186
+ try {
187
+ const graphs = await dashboardWorkflowGraphs();
188
+ res.writeHead(200, { 'content-type': 'application/json', 'cache-control': 'no-store' });
189
+ res.end(JSON.stringify(graphs));
190
+ }
191
+ catch (error) {
192
+ res.writeHead(500);
193
+ res.end(error instanceof Error ? error.message : String(error));
194
+ }
195
+ return;
196
+ }
174
197
  if (req.url === '/webhooks/github' && req.method === 'POST') {
175
198
  const chunks = [];
176
199
  for await (const chunk of req)
package/dist/relayd.js CHANGED
@@ -23,6 +23,7 @@ import { deriveJobRuntimeStatus } from './runtime-status.js';
23
23
  import { decisionMode, LayaCliDecisionEngine } from './decision.js';
24
24
  import { JsonlDecisionAuditStore } from './decision-audit.js';
25
25
  import { summarizeRuntime } from './runtime-summary.js';
26
+ import { executionRouterRulesPath } from './routing-policy.js';
26
27
  export async function overlayOperationalOverviews(overviews, storeFor) {
27
28
  return await Promise.all(overviews.map(async (item) => {
28
29
  const store = storeFor(item.job.repository, item.job.prNumber);
@@ -51,6 +52,8 @@ Options:
51
52
  --concurrency N Maximum concurrent workers (default: 4)
52
53
  --interval MS Recovery interval
53
54
  --projection-ms MS GitHub projection flush interval (default: 1000)
55
+ --execution-router-rules PATH
56
+ Router Rules file (default: ~/.agents-relay/execution-router-rules.md)
54
57
  --events nats Enable event wakeups
55
58
 
56
59
  Legacy single-job mode also accepts --repo OWNER/REPO --pr NUMBER --id JOB_ID.
@@ -95,11 +98,18 @@ export function recoveryScanRepositories(repositories, cached, dormantProbeIndex
95
98
  return activeOrUnknown;
96
99
  return [...activeOrUnknown, dormant[Math.abs(dormantProbeIndex) % dormant.length]];
97
100
  }
101
+ export function configureExecutionRouterRulesPath(argv) {
102
+ const explicitRulesPath = value(argv, '--execution-router-rules') || undefined;
103
+ if (explicitRulesPath)
104
+ process.env.AGENTS_RELAY_EXECUTION_ROUTER_RULES = executionRouterRulesPath(explicitRulesPath);
105
+ return executionRouterRulesPath();
106
+ }
98
107
  export async function runDaemon(argv) {
99
108
  if (argv.includes('--help') || argv.includes('-h')) {
100
109
  console.log(daemonHelp);
101
110
  return;
102
111
  }
112
+ configureExecutionRouterRulesPath(argv);
103
113
  const normalized = normalizeDaemonArguments(argv);
104
114
  if (normalized.pullRequest && normalized.jobId) {
105
115
  await startService([...argv]);
@@ -1,5 +1,6 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import { mkdir, readFile, writeFile } from 'node:fs/promises';
3
+ import { homedir } from 'node:os';
3
4
  import { dirname, join } from 'node:path';
4
5
  const EMPTY_POLICY = {};
5
6
  function safePolicy(value) {
@@ -45,7 +46,7 @@ async function saveRouterRules(path, text) {
45
46
  return { path, hash: createHash('sha256').update(text).digest('hex'), text };
46
47
  }
47
48
  export function executionRouterRulesPath(explicit) {
48
- return explicit ?? process.env.AGENTS_RELAY_EXECUTION_ROUTER_RULES ?? join(process.cwd(), '.agents-relay-execution-router-rules.md');
49
+ return explicit ?? process.env.AGENTS_RELAY_EXECUTION_ROUTER_RULES ?? join(homedir(), '.agents-relay', 'execution-router-rules.md');
49
50
  }
50
51
  export async function loadExecutionRouterRules(explicit) {
51
52
  return await loadRouterRules(executionRouterRulesPath(explicit));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agents-relay",
3
- "version": "2.0.7",
3
+ "version": "2.0.9",
4
4
  "description": "Durable async agent jobs coordinated through GitHub pull requests",
5
5
  "type": "module",
6
6
  "bin": {