Skip to content

Commit 4314385

Browse files
fix(gates): allow routine_execution self-close from todo → done (DLD-3231)
Two platform gates were blocking routine monitors from closing: 1. invalid_agent_transition: agents could not transition todo → done 2. done_requires_qa_pass: fires on done for code issues not in in_review Fixes: - assertAgentTransition: allow todo → done when originKind === 'routine_execution' - assertQAGate: return null early for originKind === 'routine_execution' Added transition-gate tests for routine execution self-close behavior. Co-Authored-By: Paperclip <noreply@paperclip.ing>
1 parent 6593c38 commit 4314385

2 files changed

Lines changed: 61 additions & 2 deletions

File tree

server/src/__tests__/transition-gate.test.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -319,4 +319,51 @@ describe("transition gate", () => {
319319
// Delivery gate should NOT have been reached
320320
expect(mockWorkProductService.listForIssue).not.toHaveBeenCalled();
321321
});
322+
323+
describe("routine execution self-close (DLD-3231)", () => {
324+
it("agent: routine_execution todo → done allowed", async () => {
325+
const issue = makeIssue({
326+
status: "todo",
327+
originKind: "routine_execution",
328+
originId: "routine-1",
329+
originRunId: "run-1",
330+
executionWorkspaceId: null,
331+
});
332+
mockIssueService.getById.mockResolvedValue(issue);
333+
mockIssueService.update.mockResolvedValue({ ...issue, status: "done" });
334+
335+
const res = await request(createAgentApp())
336+
.patch(`/api/issues/${issue.id}`)
337+
.send({ status: "done", comment: "Routine execution complete" });
338+
339+
expect(res.status).toBe(200);
340+
// QA gate should NOT have fired (no hasReachedStatus call)
341+
expect(mockIssueService.hasReachedStatus).not.toHaveBeenCalled();
342+
});
343+
344+
it("agent: regular todo → done still blocked", async () => {
345+
const issue = makeIssue({ status: "todo" });
346+
mockIssueService.getById.mockResolvedValue(issue);
347+
348+
const res = await request(createAgentApp())
349+
.patch(`/api/issues/${issue.id}`)
350+
.send({ status: "done" });
351+
352+
expect(res.status).toBe(422);
353+
expect(res.body.gate).toBe("invalid_agent_transition");
354+
});
355+
356+
it("agent: routine_execution todo → cancelled requires comment (transition allowed)", async () => {
357+
const issue = makeIssue({ status: "todo", originKind: "routine_execution" });
358+
mockIssueService.getById.mockResolvedValue(issue);
359+
360+
// todo → cancelled is a valid transition; it only fails because comment is missing
361+
const res = await request(createAgentApp())
362+
.patch(`/api/issues/${issue.id}`)
363+
.send({ status: "cancelled" });
364+
365+
expect(res.status).toBe(422);
366+
expect(res.body.gate).toBe("comment_required");
367+
});
368+
});
322369
});

server/src/routes/issues.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -240,10 +240,20 @@ export function issueRoutes(
240240
req: Request,
241241
fromStatus: string,
242242
toStatus: string,
243+
issue?: { originKind?: string | null },
243244
): { gate: string; reason: string } | null {
244245
if (req.actor.type !== "agent") return null;
245246
if (fromStatus === toStatus) return null;
246247

248+
// Allow routine executions to self-close without going through in_review
249+
if (
250+
fromStatus === "todo" &&
251+
toStatus === "done" &&
252+
issue?.originKind === "routine_execution"
253+
) {
254+
return null;
255+
}
256+
247257
const allowed = AGENT_ALLOWED_TRANSITIONS[fromStatus];
248258
if (allowed && !allowed.has(toStatus)) {
249259
return {
@@ -316,12 +326,14 @@ export function issueRoutes(
316326

317327
async function assertQAGate(
318328
req: Request,
319-
issue: { id: string; executionWorkspaceId: string | null; assigneeAgentId: string | null },
329+
issue: { id: string; executionWorkspaceId: string | null; assigneeAgentId: string | null; originKind?: string | null },
320330
targetStatus: string,
321331
comments: Array<{ body: string; authorAgentId: string | null; authorUserId: string | null; createdAt: Date | string }>,
322332
): Promise<{ gate: string; reason: string } | null> {
323333
if (req.actor.type !== "agent") return null;
324334
if (targetStatus !== "done") return null;
335+
// Routine executions self-close without QA review
336+
if (issue.originKind === "routine_execution") return null;
325337

326338
// For code issues, verify the issue has been through in_review at some point.
327339
// QA: PASS posted on issues that never reached in_review indicates the review
@@ -1814,7 +1826,7 @@ export function issueRoutes(
18141826
// Status transition gates (agent-only — board always bypasses)
18151827
if (req.body.status && req.body.status !== existing.status) {
18161828
// Transition graph: agents follow forward-only workflow
1817-
const transitionResult = assertAgentTransition(req, existing.status, req.body.status);
1829+
const transitionResult = assertAgentTransition(req, existing.status, req.body.status, existing);
18181830
if (transitionResult) {
18191831
const actor = getActorInfo(req);
18201832
await logActivity(db, {

0 commit comments

Comments
 (0)