patchrelay 0.35.10 → 0.35.12

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.
Files changed (50) hide show
  1. package/README.md +41 -9
  2. package/dist/build-info.json +3 -3
  3. package/dist/cli/args.js +0 -1
  4. package/dist/cli/commands/issues.js +2 -56
  5. package/dist/cli/commands/watch.js +5 -0
  6. package/dist/cli/data.js +110 -47
  7. package/dist/cli/formatters/text.js +6 -90
  8. package/dist/cli/help.js +3 -8
  9. package/dist/cli/index.js +0 -48
  10. package/dist/cli/operator-client.js +0 -82
  11. package/dist/cli/watch/App.js +1 -12
  12. package/dist/cli/watch/HelpBar.js +2 -2
  13. package/dist/cli/watch/IssueDetailView.js +57 -26
  14. package/dist/cli/watch/IssueRow.js +71 -27
  15. package/dist/cli/watch/StatusBar.js +7 -4
  16. package/dist/cli/watch/state-visualization.js +48 -23
  17. package/dist/cli/watch/timeline-builder.js +2 -1
  18. package/dist/cli/watch/use-detail-stream.js +10 -104
  19. package/dist/cli/watch/use-watch-stream.js +11 -102
  20. package/dist/cli/watch/watch-state.js +18 -50
  21. package/dist/codex-thread-utils.js +3 -0
  22. package/dist/db/migrations.js +239 -2
  23. package/dist/db.js +628 -39
  24. package/dist/github-app-token.js +7 -0
  25. package/dist/github-failure-context.js +44 -1
  26. package/dist/github-rollup.js +47 -0
  27. package/dist/github-webhook-handler.js +248 -51
  28. package/dist/github-webhooks.js +5 -0
  29. package/dist/http.js +12 -264
  30. package/dist/idle-reconciliation.js +275 -74
  31. package/dist/issue-query-service.js +221 -129
  32. package/dist/issue-session-events.js +151 -0
  33. package/dist/issue-session.js +99 -0
  34. package/dist/linear-client.js +39 -25
  35. package/dist/linear-session-reporting.js +12 -0
  36. package/dist/linear-session-sync.js +253 -24
  37. package/dist/linear-workflow.js +33 -0
  38. package/dist/merge-queue-protocol.js +0 -51
  39. package/dist/preflight.js +1 -4
  40. package/dist/queue-health-monitor.js +11 -7
  41. package/dist/run-orchestrator.js +1295 -146
  42. package/dist/run-reporting.js +5 -3
  43. package/dist/service.js +279 -102
  44. package/dist/status-note.js +56 -0
  45. package/dist/waiting-reason.js +65 -0
  46. package/dist/webhook-handler.js +270 -79
  47. package/package.json +1 -1
  48. package/dist/cli/commands/feed.js +0 -60
  49. package/dist/cli/watch/FeedView.js +0 -28
  50. package/dist/cli/watch/use-feed-stream.js +0 -92
package/dist/http.js CHANGED
@@ -1,9 +1,6 @@
1
1
  import fastify from "fastify";
2
2
  import rawBody from "fastify-raw-body";
3
3
  import { getBuildInfo } from "./build-info.js";
4
- import { matchesOperatorFeedEvent } from "./operator-feed.js";
5
- import { buildStateHistory } from "./cli/watch/history-builder.js";
6
- import { buildPatchRelayQueueObservations, buildPatchRelayStateGraph } from "./cli/watch/state-visualization.js";
7
4
  export async function buildHttpServer(config, service, logger) {
8
5
  const buildInfo = getBuildInfo();
9
6
  const loopbackBind = isLoopbackBind(config.server.bind);
@@ -247,6 +244,9 @@ export async function buildHttpServer(config, service, logger) {
247
244
  });
248
245
  }
249
246
  if (managementRoutesEnabled) {
247
+ app.get("/api/issues", async (_request, reply) => {
248
+ return reply.send({ ok: true, issues: service.listTrackedIssues() });
249
+ });
250
250
  app.get("/api/issues/:issueKey", async (request, reply) => {
251
251
  const issueKey = request.params.issueKey;
252
252
  const result = await service.getIssueOverview(issueKey);
@@ -255,22 +255,6 @@ export async function buildHttpServer(config, service, logger) {
255
255
  }
256
256
  return reply.send({ ok: true, ...result });
257
257
  });
258
- app.get("/api/issues/:issueKey/report", async (request, reply) => {
259
- const issueKey = request.params.issueKey;
260
- const result = await service.getIssueReport(issueKey);
261
- if (!result) {
262
- return reply.code(404).send({ ok: false, reason: "issue_not_found" });
263
- }
264
- return reply.send({ ok: true, ...result });
265
- });
266
- app.get("/api/issues/:issueKey/timeline", async (request, reply) => {
267
- const issueKey = request.params.issueKey;
268
- const result = await service.getIssueTimeline(issueKey);
269
- if (!result) {
270
- return reply.code(404).send({ ok: false, reason: "issue_not_found" });
271
- }
272
- return reply.send({ ok: true, ...result });
273
- });
274
258
  app.get("/api/issues/:issueKey/live", async (request, reply) => {
275
259
  const issueKey = request.params.issueKey;
276
260
  const result = await service.getActiveRunStatus(issueKey);
@@ -279,14 +263,6 @@ export async function buildHttpServer(config, service, logger) {
279
263
  }
280
264
  return reply.send({ ok: true, ...result });
281
265
  });
282
- app.get("/api/issues/:issueKey/runs/:runId/events", async (request, reply) => {
283
- const { issueKey, runId } = request.params;
284
- const result = await service.getRunEvents(issueKey, Number(runId));
285
- if (!result) {
286
- return reply.code(404).send({ ok: false, reason: "run_not_found" });
287
- }
288
- return reply.send({ ok: true, ...result });
289
- });
290
266
  app.get("/api/issues/:issueKey/session-url", async (request, reply) => {
291
267
  const issueKey = request.params.issueKey;
292
268
  const ttlSeconds = getPositiveIntegerQueryParam(request, "ttlSeconds");
@@ -338,92 +314,6 @@ export async function buildHttpServer(config, service, logger) {
338
314
  }
339
315
  return reply.send({ ok: true, ...result });
340
316
  });
341
- app.get("/api/feed", async (request, reply) => {
342
- const feedQuery = {
343
- limit: getPositiveIntegerQueryParam(request, "limit") ?? 50,
344
- ...readFeedQueryFilters(request),
345
- };
346
- if (getQueryParam(request, "follow") !== "1") {
347
- return reply.send({ ok: true, events: service.listOperatorFeed(feedQuery) });
348
- }
349
- reply.hijack();
350
- reply.raw.writeHead(200, {
351
- "content-type": "text/event-stream; charset=utf-8",
352
- "cache-control": "no-cache, no-transform",
353
- connection: "keep-alive",
354
- "x-accel-buffering": "no",
355
- });
356
- const writeEvent = (event) => {
357
- reply.raw.write(`event: feed\n`);
358
- reply.raw.write(`data: ${JSON.stringify(event)}\n\n`);
359
- };
360
- for (const event of service.listOperatorFeed(feedQuery)) {
361
- writeEvent(event);
362
- }
363
- const cleanup = () => {
364
- clearInterval(keepAlive);
365
- unsubscribe();
366
- if (!reply.raw.destroyed)
367
- reply.raw.end();
368
- };
369
- const unsubscribe = service.subscribeOperatorFeed((event) => {
370
- if (!matchesOperatorFeedEvent(event, feedQuery)) {
371
- return;
372
- }
373
- writeEvent(event);
374
- });
375
- const keepAlive = setInterval(() => {
376
- reply.raw.write(": keepalive\n\n");
377
- }, 15000);
378
- reply.raw.on("error", cleanup);
379
- request.raw.on("close", cleanup);
380
- });
381
- app.get("/api/watch/issues", async (_request, reply) => {
382
- return reply.send({ ok: true, issues: service.listTrackedIssues() });
383
- });
384
- app.get("/api/watch", async (request, reply) => {
385
- reply.hijack();
386
- reply.raw.writeHead(200, {
387
- "content-type": "text/event-stream; charset=utf-8",
388
- "cache-control": "no-cache, no-transform",
389
- connection: "keep-alive",
390
- "x-accel-buffering": "no",
391
- });
392
- const writeSse = (eventType, data) => {
393
- reply.raw.write(`event: ${eventType}\ndata: ${JSON.stringify(data)}\n\n`);
394
- };
395
- // Send initial issue snapshot
396
- writeSse("issues", service.listTrackedIssues());
397
- // Stream operator feed events
398
- const issueFilter = getQueryParam(request, "issue");
399
- const unsubscribeFeed = service.subscribeOperatorFeed((event) => {
400
- if (issueFilter && event.issueKey !== issueFilter) {
401
- return;
402
- }
403
- writeSse("feed", event);
404
- });
405
- // When filtered to a specific issue, also stream codex notifications
406
- const unsubscribeCodex = issueFilter
407
- ? service.subscribeCodexNotifications((event) => {
408
- if (event.issueKey !== issueFilter) {
409
- return;
410
- }
411
- writeSse("codex", { method: event.method, params: event.params });
412
- })
413
- : undefined;
414
- const cleanup = () => {
415
- clearInterval(keepAlive);
416
- unsubscribeFeed();
417
- unsubscribeCodex?.();
418
- if (!reply.raw.destroyed)
419
- reply.raw.end();
420
- };
421
- const keepAlive = setInterval(() => {
422
- reply.raw.write(": keepalive\n\n");
423
- }, 15000);
424
- reply.raw.on("error", cleanup);
425
- request.raw.on("close", cleanup);
426
- });
427
317
  app.get("/api/installations", async (_request, reply) => {
428
318
  return reply.send({ ok: true, installations: service.listLinearInstallations() });
429
319
  });
@@ -515,22 +405,6 @@ function getQueryParam(request, key) {
515
405
  const value = request.query?.[key];
516
406
  return typeof value === "string" ? value : undefined;
517
407
  }
518
- function readFeedQueryFilters(request) {
519
- const issueKey = getQueryParam(request, "issue")?.trim() || undefined;
520
- const projectId = getQueryParam(request, "project")?.trim() || undefined;
521
- const kind = (getQueryParam(request, "kind")?.trim() || undefined);
522
- const stage = getQueryParam(request, "stage")?.trim() || undefined;
523
- const status = getQueryParam(request, "status")?.trim() || undefined;
524
- const workflowId = getQueryParam(request, "workflow")?.trim() || undefined;
525
- return {
526
- ...(issueKey ? { issueKey } : {}),
527
- ...(projectId ? { projectId } : {}),
528
- ...(kind ? { kind } : {}),
529
- ...(stage ? { stage } : {}),
530
- ...(status ? { status } : {}),
531
- ...(workflowId ? { workflowId } : {}),
532
- };
533
- }
534
408
  function getPositiveIntegerQueryParam(request, key) {
535
409
  const value = getQueryParam(request, key);
536
410
  if (!value || !/^\d+$/.test(value)) {
@@ -596,6 +470,7 @@ function renderAgentSessionStatusPage(params) {
596
470
  const commandCount = params.sessionStatus.liveThread?.commandCount ?? params.sessionStatus.latestReportSummary?.commandCount ?? 0;
597
471
  const fileChangeCount = params.sessionStatus.liveThread?.fileChangeCount ?? params.sessionStatus.latestReportSummary?.fileChangeCount ?? 0;
598
472
  const toolCallCount = params.sessionStatus.liveThread?.toolCallCount ?? params.sessionStatus.latestReportSummary?.toolCallCount ?? 0;
473
+ const sessionState = params.sessionStatus.issue.sessionState ?? "unknown";
599
474
  const factoryState = params.sessionStatus.issue.factoryState ?? "unknown";
600
475
  const linearState = params.sessionStatus.issue.currentLinearState ?? "unknown";
601
476
  const prState = params.sessionStatus.issue.prState ?? "unknown";
@@ -603,23 +478,8 @@ function renderAgentSessionStatusPage(params) {
603
478
  const checkState = params.sessionStatus.issue.prCheckStatus ?? "unknown";
604
479
  const ciAttempts = params.sessionStatus.issue.ciRepairAttempts ?? 0;
605
480
  const queueAttempts = params.sessionStatus.issue.queueRepairAttempts ?? 0;
606
- const queueProtocol = params.sessionStatus.issue.queueProtocol;
607
- const history = buildPublicStateHistory({
608
- currentFactoryState: factoryState,
609
- activeRunId: params.sessionStatus.activeRunId ?? null,
610
- ...(params.sessionStatus.feedEvents ? { feedEvents: params.sessionStatus.feedEvents } : {}),
611
- runs: params.sessionStatus.runs,
612
- });
613
- const graph = buildPatchRelayStateGraph(history, factoryState);
614
- const queueObservations = buildPatchRelayQueueObservations({
615
- factoryState,
616
- ...(params.sessionStatus.activeRun?.runType ? { activeRunType: params.sessionStatus.activeRun.runType } : {}),
617
- ...(params.sessionStatus.issue.prNumber !== undefined ? { prNumber: params.sessionStatus.issue.prNumber } : {}),
618
- ...(params.sessionStatus.issue.prReviewState ? { prReviewState: params.sessionStatus.issue.prReviewState } : {}),
619
- }, normalizeFeedEvents(params.sessionStatus.feedEvents));
620
- const pathHtml = renderStatePath(history, factoryState);
621
- const graphHtml = renderStateGraph(graph.main, graph.prLoops, graph.queueLoop, graph.exits);
622
- const observationsHtml = renderObservationList(queueObservations);
481
+ const waitingReason = params.sessionStatus.issue.waitingReason ?? "No outstanding wait reason.";
482
+ const lastWakeReason = params.sessionStatus.issue.lastWakeReason ?? "unknown";
623
483
  return `<!doctype html>
624
484
  <html lang="en">
625
485
  <head>
@@ -693,7 +553,9 @@ function renderAgentSessionStatusPage(params) {
693
553
  ${issueUrl ? `<p><a href="${escapeHtml(issueUrl)}" target="_blank" rel="noopener noreferrer">Open issue in Linear</a></p>` : ""}
694
554
  ${prUrl ? `<p><a href="${escapeHtml(prUrl)}" target="_blank" rel="noopener noreferrer">Open pull request ${escapeHtml(prLabel ?? "")}</a></p>` : ""}
695
555
  <div class="chips">
696
- <span class="chip"><strong>Factory:</strong> <code>${escapeHtml(factoryState)}</code></span>
556
+ <span class="chip"><strong>Session:</strong> <code>${escapeHtml(sessionState)}</code></span>
557
+ <span class="chip"><strong>Waiting reason:</strong> <code>${escapeHtml(waitingReason)}</code></span>
558
+ <span class="chip"><strong>Debug stage:</strong> <code>${escapeHtml(factoryState)}</code></span>
697
559
  <span class="chip"><strong>Linear:</strong> <code>${escapeHtml(linearState)}</code></span>
698
560
  <span class="chip"><strong>Active:</strong> ${activeStage}</span>
699
561
  <span class="chip"><strong>Latest:</strong> ${latestStage}</span>
@@ -706,14 +568,8 @@ function renderAgentSessionStatusPage(params) {
706
568
  <tr><th>Pull request</th><td>${escapeHtml(prLabel ?? "none")} (${escapeHtml(prState)})</td></tr>
707
569
  <tr><th>Review</th><td>${escapeHtml(reviewState)}</td></tr>
708
570
  <tr><th>Checks</th><td>${escapeHtml(checkState)}</td></tr>
709
- <tr><th>Queue label</th><td><code>${escapeHtml(queueProtocol?.admissionLabel ?? "queue")}</code></td></tr>
710
- <tr><th>Queue check</th><td><code>${escapeHtml(queueProtocol?.evictionCheckName ?? "merge-steward/queue")}</code></td></tr>
711
- <tr><th>Last queue signal</th><td><code>${escapeHtml(queueProtocol?.lastQueueSignalAt ?? queueProtocol?.lastFailureAt ?? "none")}</code></td></tr>
712
- <tr><th>Last queue incident</th><td>${queueProtocol?.lastIncidentUrl
713
- ? `<a href="${escapeHtml(queueProtocol.lastIncidentUrl)}" target="_blank" rel="noopener noreferrer">${escapeHtml(queueProtocol.lastIncidentId ?? queueProtocol.lastIncidentUrl)}</a>`
714
- : escapeHtml(queueProtocol?.lastIncidentId ?? "none")}</td></tr>
715
- <tr><th>Queue failure class</th><td><code>${escapeHtml(queueProtocol?.lastIncidentFailureClass ?? "unknown")}</code></td></tr>
716
- <tr><th>Queue incident summary</th><td>${escapeHtml(queueProtocol?.lastIncidentSummary ?? "none")}</td></tr>
571
+ <tr><th>Waiting reason</th><td>${escapeHtml(waitingReason)}</td></tr>
572
+ <tr><th>Last wake</th><td><code>${escapeHtml(lastWakeReason)}</code></td></tr>
717
573
  <tr><th>Latest plan</th><td>${escapeHtml(latestPlan)}</td></tr>
718
574
  <tr><th>Active command</th><td><code>${escapeHtml(activeCommand)}</code></td></tr>
719
575
  <tr><th>Latest summary</th><td>${escapeHtml(latestAgentMessage)}</td></tr>
@@ -725,24 +581,7 @@ function renderAgentSessionStatusPage(params) {
725
581
  <span class="chip"><strong>File changes:</strong> ${escapeHtml(String(fileChangeCount))}</span>
726
582
  <span class="chip"><strong>Tool calls:</strong> ${escapeHtml(String(toolCallCount))}</span>
727
583
  <span class="chip"><strong>CI repairs:</strong> ${escapeHtml(String(ciAttempts))}</span>
728
- <span class="chip"><strong>Queue repairs:</strong> ${escapeHtml(String(queueAttempts))}</span>
729
- </div>
730
- <div class="section">
731
- <h2>State Path</h2>
732
- <div class="grid">
733
- <div class="card">
734
- <h3>Native Graph</h3>
735
- ${graphHtml}
736
- </div>
737
- <div class="card">
738
- <h3>Queue Observation</h3>
739
- ${observationsHtml}
740
- </div>
741
- </div>
742
- <div class="card" style="margin-top: 18px;">
743
- <h3>Observed Path</h3>
744
- ${pathHtml}
745
- </div>
584
+ <span class="chip"><strong>Steward repairs:</strong> ${escapeHtml(String(queueAttempts))}</span>
746
585
  </div>
747
586
  <div class="section">
748
587
  <h2>Recent Stages</h2>
@@ -791,94 +630,3 @@ function formatStageRow(run) {
791
630
  const endedAt = run.endedAt ?? "-";
792
631
  return `<tr><td><code>${escapeHtml(runType)}</code></td><td>${escapeHtml(status)}</td><td><code>${escapeHtml(startedAt)}</code></td><td><code>${escapeHtml(endedAt)}</code></td></tr>`;
793
632
  }
794
- function normalizeFeedEvents(feedEvents) {
795
- return (feedEvents ?? []).map((event, index) => ({
796
- id: event.id ?? -(index + 1),
797
- at: event.at,
798
- level: event.level === "warn" || event.level === "error" ? event.level : "info",
799
- kind: event.kind === "service"
800
- || event.kind === "webhook"
801
- || event.kind === "agent"
802
- || event.kind === "comment"
803
- || event.kind === "stage"
804
- || event.kind === "turn"
805
- || event.kind === "workflow"
806
- || event.kind === "hook"
807
- || event.kind === "github"
808
- || event.kind === "linear"
809
- ? event.kind
810
- : "service",
811
- summary: event.summary ?? "",
812
- ...(event.detail ? { detail: event.detail } : {}),
813
- ...(event.issueKey ? { issueKey: event.issueKey } : {}),
814
- ...(event.projectId ? { projectId: event.projectId } : {}),
815
- ...(event.stage ? { stage: event.stage } : {}),
816
- ...(event.status ? { status: event.status } : {}),
817
- ...(event.workflowId ? { workflowId: event.workflowId } : {}),
818
- ...(event.nextStage ? { nextStage: event.nextStage } : {}),
819
- }));
820
- }
821
- function buildPublicStateHistory(params) {
822
- const runs = params.runs.flatMap((entry, index) => {
823
- if (!entry.run?.runType || !entry.run?.status || !entry.run?.startedAt) {
824
- return [];
825
- }
826
- return [{
827
- id: entry.run.id ?? index + 1,
828
- runType: entry.run.runType,
829
- status: entry.run.status,
830
- startedAt: entry.run.startedAt,
831
- endedAt: entry.run.endedAt,
832
- ...(entry.report ? {
833
- report: {
834
- runType: entry.run.runType,
835
- status: entry.run.status,
836
- prompt: "",
837
- assistantMessages: entry.report.assistantMessages ?? [],
838
- plans: [],
839
- reasoning: [],
840
- commands: (entry.report.commands ?? []),
841
- fileChanges: (entry.report.fileChanges ?? []),
842
- toolCalls: [],
843
- eventCounts: {},
844
- },
845
- } : {}),
846
- }];
847
- });
848
- return buildStateHistory(runs, normalizeFeedEvents(params.feedEvents), params.currentFactoryState, params.activeRunId);
849
- }
850
- function renderStateGraph(main, prLoops, queueLoop, exits) {
851
- return [
852
- renderGraphRow("main", main, true),
853
- renderGraphRow("pr loops", prLoops, false),
854
- renderGraphRow("queue loop", queueLoop, false),
855
- renderGraphRow("exits", exits, false),
856
- ].join("");
857
- }
858
- function renderGraphRow(label, nodes, withConnectors) {
859
- const items = nodes.map((node, index) => {
860
- const connector = withConnectors && index > 0 ? '<span class="graph-connector">→</span>' : "";
861
- return `${connector}<span class="node ${escapeHtml(node.status)}">${escapeHtml(node.label)}</span>`;
862
- }).join("");
863
- return `<div class="graph-row"><strong>${escapeHtml(label)}:</strong> ${items}</div>`;
864
- }
865
- function renderObservationList(observations) {
866
- if (observations.length === 0) {
867
- return '<p>No queue observation is available yet.</p>';
868
- }
869
- return `<ul class="observation-list">${observations.map((observation) => `<li class="tone-${escapeHtml(observation.tone)}">${escapeHtml(observation.text)}</li>`).join("")}</ul>`;
870
- }
871
- function renderStatePath(history, currentFactoryState) {
872
- if (history.length === 0) {
873
- return `<p>Current native state: <code>${escapeHtml(currentFactoryState)}</code>.</p>`;
874
- }
875
- const items = [];
876
- for (const node of history) {
877
- items.push(`<li><code>${escapeHtml(node.state)}</code>${node.reason ? ` — ${escapeHtml(node.reason)}` : ""}${node.isCurrent ? " (current)" : ""}</li>`);
878
- for (const trip of node.sideTrips) {
879
- const returnText = trip.returnState ? ` → ${trip.returnedAt ? escapeHtml(trip.returnState) : escapeHtml(trip.returnState)}` : "";
880
- items.push(`<li><code>${escapeHtml(trip.state)}</code> side trip${trip.reason ? ` — ${escapeHtml(trip.reason)}` : ""}${returnText ? ` ${returnText}` : ""}</li>`);
881
- }
882
- }
883
- return `<ul class="path-list">${items.join("")}</ul>`;
884
- }