From c2d00dc842ea2bec699f691c3a42cf8a50bca82c Mon Sep 17 00:00:00 2001 From: Jory Irving Date: Wed, 27 May 2026 11:04:19 -0600 Subject: [PATCH] feat: expose lane/status/labels metadata in active-work response Include lane, status, and labels fields in GET /api/agents/{agent}/active-work so workers can verify execution lane without extra GitHub scraping or DB queries. Fixes #237 --- .../[agentName]/active-work/route.test.ts | 151 ++++++++++++++++++ src/lib/lease.ts | 3 + src/lib/next-action.ts | 3 + 3 files changed, 157 insertions(+) diff --git a/src/app/api/agents/[agentName]/active-work/route.test.ts b/src/app/api/agents/[agentName]/active-work/route.test.ts index fc549ad..1a01316 100644 --- a/src/app/api/agents/[agentName]/active-work/route.test.ts +++ b/src/app/api/agents/[agentName]/active-work/route.test.ts @@ -53,6 +53,9 @@ describe("GET /api/agents/:agentName/active-work", () => { issue: { number: 42, repository: { fullName: "org/repo" }, + state: "status/in-progress", + labels: ["agent/test-agent", "priority/p1"], + currentLane: "normal", }, }); mocks.issueFindUnique.mockResolvedValue({ id: "issue-abc" }); @@ -68,6 +71,9 @@ describe("GET /api/agents/:agentName/active-work", () => { expect(body.context.issueNumber).toBe(42); expect(body.context.branch).toBe("feat/my-feature"); expect(body.context.leaseId).toBe("l-1"); + expect(body.context.lane).toBe("normal"); + expect(body.context.status).toBe("status/in-progress"); + expect(body.context.labels).toEqual(["agent/test-agent", "priority/p1"]); }); it("returns hasActiveWork: false when no active lease exists", async () => { @@ -92,6 +98,9 @@ describe("GET /api/agents/:agentName/active-work", () => { issue: { number: 42, repository: { fullName: "org/repo" }, + state: "status/in-progress", + labels: [], + currentLane: "normal", }, }); @@ -114,6 +123,9 @@ describe("GET /api/agents/:agentName/active-work", () => { issue: { number: 42, repository: { fullName: "org/repo" }, + state: "status/in-progress", + labels: [], + currentLane: "normal", }, }); @@ -136,6 +148,145 @@ describe("GET /api/agents/:agentName/active-work", () => { issue: { number: 999, repository: { fullName: "org/repo" }, + state: "status/in-progress", + labels: [], + currentLane: "normal", + }, + }); + + mocks.issueFindUnique.mockResolvedValueOnce(null); + + const res = await makeActiveWorkRequest("test-agent"); + expect(res.status).toBe(200); + const body = await res.json(); + expect(body.hasActiveWork).toBe(false); + }); + + it("returns escalated lane metadata when issue is in escalated lane", async () => { + mocks.leaseFindFirst.mockResolvedValueOnce({ + id: "l-2", + agentName: "escalated-agent", + issueId: "issue-xyz", + checkpoint: "changes_made", + branch: "feat/esc-fix", + prUrl: null, + expiredAt: new Date(Date.now() + 60000), + renewedAt: new Date(), + issue: { + number: 55, + repository: { fullName: "org/repo" }, + state: "status/in-progress", + labels: ["agent/escalated-agent", "priority/p1"], + currentLane: "escalated", + }, + }); + + const res = await makeActiveWorkRequest("escalated-agent"); + expect(res.status).toBe(200); + const body = await res.json(); + expect(body.hasActiveWork).toBe(true); + expect(body.context.lane).toBe("escalated"); + expect(body.context.status).toBe("status/in-progress"); + expect(body.context.labels).toEqual(["agent/escalated-agent", "priority/p1"]); + }); + + it("returns backlog lane metadata when issue is in backlog lane", async () => { + mocks.leaseFindFirst.mockResolvedValueOnce({ + id: "l-3", + agentName: "test-agent", + issueId: "issue-backlog", + checkpoint: "issue_claimed", + branch: null, + prUrl: null, + expiredAt: new Date(Date.now() + 60000), + renewedAt: new Date(), + issue: { + number: 77, + repository: { fullName: "org/repo" }, + state: "status/backlog", + labels: ["agent/test-agent"], + currentLane: "backlog", + }, + }); + + const res = await makeActiveWorkRequest("test-agent"); + expect(res.status).toBe(200); + const body = await res.json(); + expect(body.hasActiveWork).toBe(true); + expect(body.context.lane).toBe("backlog"); + expect(body.context.status).toBe("status/backlog"); + }); + + it("defaults lane to normal when currentLane is null", async () => { + mocks.leaseFindFirst.mockResolvedValueOnce({ + id: "l-4", + agentName: "test-agent", + issueId: "issue-null-lane", + checkpoint: "issue_claimed", + branch: null, + prUrl: null, + expiredAt: new Date(Date.now() + 60000), + renewedAt: new Date(), + issue: { + number: 88, + repository: { fullName: "org/repo" }, + state: "status/ready", + labels: [], + currentLane: null, + }, + }); + + const res = await makeActiveWorkRequest("test-agent"); + expect(res.status).toBe(200); + const body = await res.json(); + expect(body.hasActiveWork).toBe(true); + expect(body.context.lane).toBe("normal"); + }); + + it("returns lane/status metadata for closed issue (stale lease still resolvable)", async () => { + mocks.leaseFindFirst.mockResolvedValueOnce({ + id: "l-5", + agentName: "test-agent", + issueId: "issue-closed", + checkpoint: "changes_made", + branch: "feat/closed-issue-fix", + prUrl: null, + expiredAt: new Date(Date.now() + 60000), + renewedAt: new Date(), + issue: { + number: 200, + repository: { fullName: "org/repo" }, + state: "status/done", + labels: ["type/bug", "priority/p1"], + currentLane: "normal", + }, + }); + + const res = await makeActiveWorkRequest("test-agent"); + expect(res.status).toBe(200); + const body = await res.json(); + expect(body.hasActiveWork).toBe(true); + expect(body.context.lane).toBe("normal"); + expect(body.context.status).toBe("status/done"); + expect(body.context.labels).toEqual(["type/bug", "priority/p1"]); + }); + + it("returns hasActiveWork: false when referenced issue is missing (orphaned lease)", async () => { + mocks.leaseFindFirst.mockResolvedValueOnce({ + id: "l-1", + agentName: "test-agent", + issueId: "issue-missing", + checkpoint: "issue_claimed", + branch: null, + prUrl: null, + expiredAt: new Date(Date.now() + 60000), + renewedAt: new Date(), + issue: { + number: 999, + repository: { fullName: "org/repo" }, + state: "status/in-progress", + labels: [], + currentLane: "normal", }, }); diff --git a/src/lib/lease.ts b/src/lib/lease.ts index f6e49d6..f93ea71 100644 --- a/src/lib/lease.ts +++ b/src/lib/lease.ts @@ -211,6 +211,9 @@ export async function resolveActiveWork(agentName: string): Promise