fix(runner): let comment_on_task request follow-up so a queued requirement survives completion

The comment API keeps an agent's comment on another agent's task as a note
unless it sends resume: true, and a deferred wake without that intent is
cancelled when the assignee completes the task. The native comment_on_task tool
only sent { body }, so a lead's requirement change saved behind an active run
was dropped once the worker finished Done. Add an optional resume flag, forward
it to the comment API, and tell agents to set it for requirement changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
DottaandClaude Opus 5.5 committed 2026-11-08 06:06:00 -06:00
1 parent 152af6161c
commit 28d1e89b35
3 files changed
+14 -3

No files matched your search

@@ -291,11 +291,11 @@ const TASK_API_PATHS: Readonly<Record<string, readonly [string, string]>> = {
write_task_document: ["PUT", "/api/issues/{id}/documents/{key}"],
};
function taskBridgeApiRequest(tool: string, input: Record<string, any>): RunnerApiCall {
export function taskBridgeApiRequest(tool: string, input: Record<string, any>): RunnerApiCall {
const [method, path] = TASK_API_PATHS[tool]!;
const operation = runnerApiCatalog().find(operation => operation.method === method && operation.path === path);
if (!operation) throw new Error("runner_task_api_operation_unavailable");
const body = tool === "comment_on_task" ? { body: input.body } : tool === "write_task_document"
const body = tool === "comment_on_task" ? { body: input.body, ...(input.resume === true ? { resume: true } : {}) } : tool === "write_task_document"
? { title: input.title, format: "markdown", body: input.body, baseRevisionId: input.baseRevisionId } : undefined;
return { operationId: operation.operationId, pathParams: { id: input.taskId, ...(input.key ? { key: input.key } : {}) }, ...(body ? { body } : {}) };
}
@@ -1,6 +1,7 @@
import { mkdir, mkdtemp, readFile, rm, symlink, writeFile } from "node:fs/promises";
import { createHash } from "node:crypto";
import { materializeAsset } from "./runtime-context.js";
import { taskBridgeApiRequest } from "./paperclip-runner-tool-authority.js";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { afterEach, describe, expect, it } from "vitest";
@@ -93,7 +94,17 @@ describe("Runner workspace bridge", () => {
expect(comment?.description).toContain("existing task's assignee a new or changed requirement");
expect(comment?.description).toContain("rather than creating a duplicate task or editing that task's workspace files");
expect(comment?.description).toContain("delivers it at the start of the assignee's next turn on that task");
expect(comment?.description).toContain("set resume: true for such a requirement");
expect(comment?.description).toContain("delivery still happens if the assignee completes the task first");
expect(comment?.description).toContain("not as already applied");
expect((comment?.inputSchema as { properties?: Record<string, unknown> }).properties?.resume).toEqual({ type: "boolean" });
});
it("forwards a comment's explicit resume intent to the comment API and omits it otherwise", () => {
const taskId = "8ba8b5a2-066a-4bb2-b591-e396942ecdf5";
expect(taskBridgeApiRequest("comment_on_task", { taskId, body: "Set the headline to Launch sooner.", resume: true }).body)
.toEqual({ body: "Set the headline to Launch sooner.", resume: true });
expect(taskBridgeApiRequest("comment_on_task", { taskId, body: "FYI only." }).body).toEqual({ body: "FYI only." });
expect(taskBridgeApiRequest("comment_on_task", { taskId, body: "FYI only.", resume: false }).body).toEqual({ body: "FYI only." });
});
it.skipIf(process.platform === "linux")("does not expose or execute commands without descendant containment", async () => {
@@ -14,7 +14,7 @@ const specs = [
["get_identity", "Read your agent identity, responsible person, permissions and currently available tools.", z.object({}).strict(), "read"],
["list_people", "List active company people and their IDs for human assignment; roles identify the owner.", z.object({ after: z.string().optional(), limit: z.number().int().min(1).max(100).default(50) }).strict(), "read"],
["get_task", "Read another task through normal task permissions.", z.object({ taskId: z.uuid() }).strict(), "read"],
["comment_on_task", "Post an attributed comment on an authorized task, without reopening or interrupting it. Use it to give an existing task's assignee a new or changed requirement, rather than creating a duplicate task or editing that task's workspace files. If the assignee is running on that task now, the comment does not reach the run in progress: Paperclip saves it in the task's queue and delivers it at the start of the assignee's next turn on that task. Report it as saved for that next turn, not as already applied.", z.object({ taskId: z.uuid(), body: z.string().trim().min(1).max(20000) }).strict(), "mutation"],
["comment_on_task", "Post an attributed comment on an authorized task, without reopening or interrupting it. Use it to give an existing task's assignee a new or changed requirement, rather than creating a duplicate task or editing that task's workspace files, and set resume: true for such a requirement. If the assignee is running on that task now, the comment does not reach the run in progress: Paperclip saves it in the task's queue and delivers it at the start of the assignee's next turn on that task. With resume: true that delivery still happens if the assignee completes the task first; without it, a comment on finished work is only a note. Report a queued requirement as saved for the next turn, not as already applied.", z.object({ taskId: z.uuid(), body: z.string().trim().min(1).max(20000), resume: z.boolean().optional() }).strict(), "mutation"],
["list_task_documents", "List documents on an authorized task.", z.object({ taskId: z.uuid() }).strict(), "read"],
["read_task_document", "Read a document on an authorized task.", z.object({ taskId: z.uuid(), key: documentKey }).strict(), "read"],
["write_task_document", "Write a document on an authorized task using its current revision. Permissions and document locks apply.", z.object({ taskId: z.uuid(), key: documentKey, title: z.string().min(1).max(200), body: z.string().max(20000), baseRevisionId: z.uuid().nullable() }).strict(), "mutation"],