From 64ed933ba5384de70ca344fca8fa0c2fecadb5c1 Mon Sep 17 00:00:00 2001 From: PunGrumpy <108584943+PunGrumpy@users.noreply.github.com> Date: Fri, 4 Sep 2026 03:05:45 +0000 Subject: [PATCH 1/5] fix(runner): handle agent run status and map failures to non-zero exit code --- __tests__/runner.test.ts | 68 +++++++++++++++++++++++++++++++++-- src/runner.ts | 76 +++++++++++++++++++++++++++++++++------- src/types.ts | 12 +++++++ 3 files changed, 141 insertions(+), 15 deletions(-) diff --git a/__tests__/runner.test.ts b/__tests__/runner.test.ts index 153c0c4..0189f66 100644 --- a/__tests__/runner.test.ts +++ b/__tests__/runner.test.ts @@ -96,6 +96,7 @@ describe("runAgent", () => { expect(mockAgentSend).toHaveBeenCalledWith("Analyze this code"); expect(mockRunCancel).not.toHaveBeenCalled(); expect(result.exitCode).toBe(0); + expect(result.status).toBe("finished"); expect(result.stdout).toBe("Hello from stream chunk 1. And chunk 2."); expect(result.stderr).toBe(""); }); @@ -114,6 +115,7 @@ describe("runAgent", () => { const result = await runAgent(baseInputs); expect(result.exitCode).toBe(1); + expect(result.status).toBe("error"); expect(result.stderr).toContain("Invalid API key"); expect(mockWarning).toHaveBeenCalledWith( expect.stringContaining("Invalid API key") @@ -126,12 +128,20 @@ describe("runAgent", () => { cancel: mockRunCancel, stream: streamAfter1100ms, supports: (op: string) => op === "cancel", - wait: finishedWait(""), + wait: () => + Promise.resolve({ + id: "run-test", + result: "", + status: "cancelled" as const, + }), }); - await runAgent({ ...baseInputs, timeout: 1 }); + const result = await runAgent({ ...baseInputs, timeout: 1 }); expect(mockRunCancel).toHaveBeenCalled(); + expect(result.exitCode).toBe(1); + expect(result.status).toBe("cancelled"); + expect(result.stderr).toContain("timed out"); }, 5000); it("does not call run.cancel when cancel is unsupported", async () => { @@ -147,6 +157,7 @@ describe("runAgent", () => { expect(mockRunCancel).not.toHaveBeenCalled(); expect(result.exitCode).toBe(0); + expect(result.status).toBe("finished"); }, 5000); it("returns exitCode 1 when stream throws an error", async () => { @@ -160,9 +171,62 @@ describe("runAgent", () => { const result = await runAgent(baseInputs); expect(result.exitCode).toBe(1); + expect(result.status).toBe("error"); expect(result.stderr).toContain("Stream aborted"); expect(mockWarning).toHaveBeenCalledWith( expect.stringContaining("Stream aborted") ); }); + + it("returns exitCode 1 and surfaces error message when run.wait() finishes with error status", async () => { + mockAgentSend.mockResolvedValue({ + cancel: mockRunCancel, + stream: mockStreamSuccess, + supports: (op: string) => op === "cancel", + wait: () => + Promise.resolve({ + error: { message: "Internal server error during turn" }, + id: "run-test", + result: "", + status: "error" as const, + }), + }); + + const result = await runAgent(baseInputs); + + expect(result.exitCode).toBe(1); + expect(result.status).toBe("error"); + expect(result.stderr).toContain("Internal server error during turn"); + expect(mockWarning).toHaveBeenCalledWith( + expect.stringContaining("Internal server error during turn") + ); + }); + + it("captures usage and duration from run.wait()", async () => { + mockAgentSend.mockResolvedValue({ + cancel: mockRunCancel, + stream: mockStreamSuccess, + supports: (op: string) => op === "cancel", + wait: () => + Promise.resolve({ + durationMs: 4200, + id: "run-test", + result: "Done analysis", + status: "finished" as const, + usage: { + cacheReadTokens: 10, + cacheWriteTokens: 5, + inputTokens: 100, + outputTokens: 50, + totalTokens: 150, + }, + }), + }); + + const result = await runAgent(baseInputs); + + expect(result.exitCode).toBe(0); + expect(result.durationMs).toBe(4200); + expect(result.usage?.totalTokens).toBe(150); + }); }); diff --git a/src/runner.ts b/src/runner.ts index ef3c3ec..a6e1517 100644 --- a/src/runner.ts +++ b/src/runner.ts @@ -2,8 +2,38 @@ import path from "node:path"; import { info, warning } from "@actions/core"; import { Agent } from "@cursor/sdk"; +import type { RunResult } from "@cursor/sdk"; -import type { ActionInputs, AgentResult } from "./types"; +import type { ActionInputs, AgentResult, TokenUsageStats } from "./types"; + +const extractErrorMessage = (error: unknown): string => { + if (error instanceof Error) { + return error.cause + ? `${error.message}\nCause: ${error.cause}` + : error.message; + } + return String(error); +}; + +const mapUsage = (usage: RunResult["usage"]): TokenUsageStats | undefined => { + if (!usage) { + return undefined; + } + const { + cacheReadTokens, + cacheWriteTokens, + inputTokens, + outputTokens, + totalTokens, + } = usage; + return { + cacheReadTokens, + cacheWriteTokens, + inputTokens, + outputTokens, + totalTokens, + }; +}; export const runAgent = async (inputs: ActionInputs): Promise => { const cwd = path.resolve(inputs.workingDirectory); @@ -20,6 +50,9 @@ export const runAgent = async (inputs: ActionInputs): Promise => { let stdout = ""; let stderr = ""; let exitCode = 0; + let status = "finished"; + let durationMs: number | undefined; + let usage: TokenUsageStats | undefined; try { const agent = await Agent.create({ @@ -32,8 +65,11 @@ export const runAgent = async (inputs: ActionInputs): Promise => { const timeoutMs = inputs.timeout * 1000; let cancelTimer: ReturnType | undefined; + let timedOut = false; + if (timeoutMs > 0 && Number.isFinite(timeoutMs)) { cancelTimer = setTimeout(() => { + timedOut = true; // Fire-and-forget: timeout handler must not block the timer callback. void (async () => { if (run.supports("cancel")) { @@ -54,10 +90,27 @@ export const runAgent = async (inputs: ActionInputs): Promise => { } } - await run.wait(); - const finalResult = run.result; - if (finalResult && typeof finalResult === "string") { - stdout = finalResult; + const runResult = await run.wait(); + ({ durationMs } = runResult); + usage = mapUsage(runResult.usage); + + if (runResult.result && typeof runResult.result === "string") { + stdout = runResult.result; + } + + ({ status } = runResult); + if (status === "error") { + exitCode = 1; + const msg = runResult.error?.message ?? "Agent run failed with error."; + stderr += stderr ? `\n${msg}` : msg; + warning(`Agent execution failed: ${msg}`); + } else if (status === "cancelled") { + exitCode = 1; + const msg = timedOut + ? `Agent run timed out after ${inputs.timeout}s and was cancelled.` + : "Agent run was cancelled."; + stderr += stderr ? `\n${msg}` : msg; + warning(msg); } } finally { if (cancelTimer !== undefined) { @@ -66,21 +119,18 @@ export const runAgent = async (inputs: ActionInputs): Promise => { } } catch (error) { exitCode = 1; - if (error instanceof Error) { - stderr += error.message; - if (error.cause) { - stderr += `\nCause: ${error.cause}`; - } - } else { - stderr += String(error); - } + status = "error"; + stderr += extractErrorMessage(error); warning(`Agent execution failed: ${stderr}`); } return { diagnostics: exitCode === 0 ? undefined : stderr, + durationMs, exitCode, + status, stderr, stdout, + usage, }; }; diff --git a/src/types.ts b/src/types.ts index aee9cad..9f6686b 100644 --- a/src/types.ts +++ b/src/types.ts @@ -13,11 +13,23 @@ export interface ActionInputs { export interface ActionOutputs { summary: string; exitCode: number; + status: string; +} + +export interface TokenUsageStats { + inputTokens?: number; + outputTokens?: number; + cacheReadTokens?: number; + cacheWriteTokens?: number; + totalTokens?: number; } export interface AgentResult { stdout: string; stderr: string; exitCode: number; + status?: string; + durationMs?: number; + usage?: TokenUsageStats; diagnostics?: string; } From be5d324c7f55973e07f32891fd42c82259205c55 Mon Sep 17 00:00:00 2001 From: PunGrumpy <108584943+PunGrumpy@users.noreply.github.com> Date: Fri, 4 Sep 2026 03:06:01 +0000 Subject: [PATCH 2/5] feat(action): expose status output and display metrics in job summary --- __tests__/output.test.ts | 27 ++++++++++++++ action.yml | 4 +++ dist/index.mjs | 76 +++++++++++++++++++++++++++++----------- src/output.ts | 24 +++++++++++-- 4 files changed, 109 insertions(+), 22 deletions(-) diff --git a/__tests__/output.test.ts b/__tests__/output.test.ts index eb19f26..2cfbd59 100644 --- a/__tests__/output.test.ts +++ b/__tests__/output.test.ts @@ -43,8 +43,10 @@ describe("setOutputs", () => { "Here is my analysis." ); expect(mockSetOutput).toHaveBeenCalledWith("exit-code", "0"); + expect(mockSetOutput).toHaveBeenCalledWith("status", "finished"); expect(outputs.summary).toBe("Here is my analysis."); expect(outputs.exitCode).toBe(0); + expect(outputs.status).toBe("finished"); expect(mockSummaryChain.write).toHaveBeenCalled(); }); @@ -123,6 +125,31 @@ describe("setOutputs", () => { const outputs = await setOutputs(result); expect(outputs.exitCode).toBe(1); expect(mockSetOutput).toHaveBeenCalledWith("exit-code", "1"); + expect(mockSetOutput).toHaveBeenCalledWith("status", "error"); + expect(outputs.status).toBe("error"); + }); + + it("includes custom status, duration, and token usage in job summary", async () => { + const result = { + durationMs: 3500, + exitCode: 0, + status: "finished", + stderr: "", + stdout: "Done", + usage: { + totalTokens: 250, + }, + }; + const outputs = await setOutputs(result); + expect(outputs.status).toBe("finished"); + expect(mockSetOutput).toHaveBeenCalledWith("status", "finished"); + expect(mockSummaryChain.addTable).toHaveBeenCalledWith( + expect.arrayContaining([ + ["Agent Status", "finished"], + ["Duration", "3.5s"], + ["Total Tokens", "250"], + ]) + ); }); }); diff --git a/action.yml b/action.yml index cfd82dc..6666d94 100644 --- a/action.yml +++ b/action.yml @@ -49,6 +49,10 @@ outputs: description: "Exit code from the cursor-agent process" value: ${{ steps.agent.outputs.exit-code }} + status: + description: "Run status returned by the cursor-agent (finished, error, or cancelled)" + value: ${{ steps.agent.outputs.status }} + # @cursor/sdk cannot be bundled: it dynamically imports its own webpack chunks # at runtime and resolves a native @cursor/sdk- package. It has to be # installed as a real dependency tree, which a `node24` action cannot do. diff --git a/dist/index.mjs b/dist/index.mjs index 2af7f2d..f132642 100644 --- a/dist/index.mjs +++ b/dist/index.mjs @@ -50,18 +50,17 @@ const parseSummary = (stdout) => { return trimmed.replaceAll(/\u001B\[[0-9;]*[mGKHF]/gu, ""); }; const writeJobSummary = async (text, result) => { - const status = result.exitCode === 0 ? "✅ Success" : `❌ Failed (exit ${result.exitCode})`; - await summary.addHeading("Cursor Agent Run", 2).addTable([ - [{ - data: "Field", - header: true - }, { - data: "Value", - header: true - }], - ["Status", status], - ["Exit Code", String(result.exitCode)] - ]).addHeading("Agent Response", 3).addRaw(text ? `\n\`\`\`\n${text}\n\`\`\`\n` : "_No output was produced._"); + const tableRows = [["Status", result.exitCode === 0 ? "✅ Success" : `❌ Failed (exit ${result.exitCode})`], ["Exit Code", String(result.exitCode)]]; + if (result.status) tableRows.push(["Agent Status", result.status]); + if (result.durationMs !== void 0) tableRows.push(["Duration", `${(result.durationMs / 1e3).toFixed(1)}s`]); + if (result.usage?.totalTokens !== void 0) tableRows.push(["Total Tokens", String(result.usage.totalTokens)]); + await summary.addHeading("Cursor Agent Run", 2).addTable([[{ + data: "Field", + header: true + }, { + data: "Value", + header: true + }], ...tableRows]).addHeading("Agent Response", 3).addRaw(text ? `\n\`\`\`\n${text}\n\`\`\`\n` : "_No output was produced._"); const errText = result.stderr.trim(); if (errText) await summary.addHeading("Agent Error (stderr)", 3).addRaw(`\n\`\`\`\n${errText.slice(0, 2e4)}${errText.length > 2e4 ? "\n… (truncated)" : ""}\n\`\`\`\n`); const diag = result.diagnostics?.trim(); @@ -70,17 +69,35 @@ const writeJobSummary = async (text, result) => { }; const setOutputs = async (result) => { const text = parseSummary(result.stdout); + const status = result.status ?? (result.exitCode === 0 ? "finished" : "error"); setOutput("summary", text); setOutput("exit-code", String(result.exitCode)); + setOutput("status", status); await writeJobSummary(text, result); return { exitCode: result.exitCode, + status, summary: text }; }; const maskSecret = (apiKey) => setSecret(apiKey); //#endregion //#region src/runner.ts +const extractErrorMessage = (error) => { + if (error instanceof Error) return error.cause ? `${error.message}\nCause: ${error.cause}` : error.message; + return String(error); +}; +const mapUsage = (usage) => { + if (!usage) return; + const { cacheReadTokens, cacheWriteTokens, inputTokens, outputTokens, totalTokens } = usage; + return { + cacheReadTokens, + cacheWriteTokens, + inputTokens, + outputTokens, + totalTokens + }; +}; const runAgent = async (inputs) => { const cwd = path.resolve(inputs.workingDirectory); info(`Running Cursor Agent in: ${cwd}`); @@ -89,6 +106,9 @@ const runAgent = async (inputs) => { let stdout = ""; let stderr = ""; let exitCode = 0; + let status = "finished"; + let durationMs; + let usage; try { const run = await (await Agent.create({ apiKey: inputs.apiKey, @@ -97,7 +117,9 @@ const runAgent = async (inputs) => { })).send(inputs.prompt); const timeoutMs = inputs.timeout * 1e3; let cancelTimer; + let timedOut = false; if (timeoutMs > 0 && Number.isFinite(timeoutMs)) cancelTimer = setTimeout(() => { + timedOut = true; (async () => { if (run.supports("cancel")) try { await run.cancel(); @@ -106,25 +128,39 @@ const runAgent = async (inputs) => { }, timeoutMs); try { for await (const event of run.stream()) if ("text" in event && typeof event.text === "string") stdout += event.text; - await run.wait(); - const finalResult = run.result; - if (finalResult && typeof finalResult === "string") stdout = finalResult; + const runResult = await run.wait(); + ({durationMs} = runResult); + usage = mapUsage(runResult.usage); + if (runResult.result && typeof runResult.result === "string") stdout = runResult.result; + ({status} = runResult); + if (status === "error") { + exitCode = 1; + const msg = runResult.error?.message ?? "Agent run failed with error."; + stderr += stderr ? `\n${msg}` : msg; + warning(`Agent execution failed: ${msg}`); + } else if (status === "cancelled") { + exitCode = 1; + const msg = timedOut ? `Agent run timed out after ${inputs.timeout}s and was cancelled.` : "Agent run was cancelled."; + stderr += stderr ? `\n${msg}` : msg; + warning(msg); + } } finally { if (cancelTimer !== void 0) clearTimeout(cancelTimer); } } catch (error) { exitCode = 1; - if (error instanceof Error) { - stderr += error.message; - if (error.cause) stderr += `\nCause: ${error.cause}`; - } else stderr += String(error); + status = "error"; + stderr += extractErrorMessage(error); warning(`Agent execution failed: ${stderr}`); } return { diagnostics: exitCode === 0 ? void 0 : stderr, + durationMs, exitCode, + status, stderr, - stdout + stdout, + usage }; }; //#endregion diff --git a/src/output.ts b/src/output.ts index 05f9193..3089834 100644 --- a/src/output.ts +++ b/src/output.ts @@ -49,6 +49,23 @@ const writeJobSummary = async ( ? "✅ Success" : `❌ Failed (exit ${result.exitCode})`; + const tableRows: string[][] = [ + ["Status", status], + ["Exit Code", String(result.exitCode)], + ]; + + if (result.status) { + tableRows.push(["Agent Status", result.status]); + } + + if (result.durationMs !== undefined) { + tableRows.push(["Duration", `${(result.durationMs / 1000).toFixed(1)}s`]); + } + + if (result.usage?.totalTokens !== undefined) { + tableRows.push(["Total Tokens", String(result.usage.totalTokens)]); + } + await summary .addHeading("Cursor Agent Run", 2) .addTable([ @@ -56,8 +73,7 @@ const writeJobSummary = async ( { data: "Field", header: true }, { data: "Value", header: true }, ], - ["Status", status], - ["Exit Code", String(result.exitCode)], + ...tableRows, ]) .addHeading("Agent Response", 3) .addRaw(text ? `\n\`\`\`\n${text}\n\`\`\`\n` : "_No output was produced._"); @@ -87,14 +103,18 @@ export const setOutputs = async ( result: AgentResult ): Promise => { const text = parseSummary(result.stdout); + const status = + result.status ?? (result.exitCode === 0 ? "finished" : "error"); setOutput("summary", text); setOutput("exit-code", String(result.exitCode)); + setOutput("status", status); await writeJobSummary(text, result); return { exitCode: result.exitCode, + status, summary: text, }; }; From 01282323b42b63a9bc67d7c49658d3827d2ee8c3 Mon Sep 17 00:00:00 2001 From: PunGrumpy <108584943+PunGrumpy@users.noreply.github.com> Date: Fri, 4 Sep 2026 03:06:16 +0000 Subject: [PATCH 3/5] docs: update behavior, quickstart, and reference for status handling --- README.md | 9 +++++---- docs/content/behaviour.mdx | 12 ++++++------ docs/content/quickstart.mdx | 5 +---- docs/content/reference.mdx | 15 +++++++++------ docs/content/troubleshooting.mdx | 4 ++-- 5 files changed, 23 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index efa8799..034d438 100644 --- a/README.md +++ b/README.md @@ -44,10 +44,11 @@ The action runs on `ubuntu-latest`, `windows-latest`, and `macos-latest`. ### Outputs -| Output | Description | -| ----------- | ----------------------------------------- | -| `summary` | Text summary returned by the cursor-agent | -| `exit-code` | Exit code from the cursor-agent process | +| Output | Description | +| --- | --- | +| `summary` | Text summary returned by the cursor-agent | +| `exit-code` | Exit code from the cursor-agent process | +| `status` | Run status returned by the cursor-agent (finished, error, or cancelled) | diff --git a/docs/content/behaviour.mdx b/docs/content/behaviour.mdx index 221f512..baea40a 100644 --- a/docs/content/behaviour.mdx +++ b/docs/content/behaviour.mdx @@ -1,6 +1,6 @@ --- title: Behaviour -description: What the action actually does at runtime, including what it gets wrong. +description: What the action does at runtime and how it reports results. --- ## The run @@ -9,13 +9,13 @@ description: What the action actually does at runtime, including what it gets wr 2. The API key is registered with `::add-mask::`. 3. A local SDK agent is created against the resolved `working-directory` (`Agent.create({ local: { cwd } })`) and the prompt is sent. 4. Streamed text is collected, then replaced by the final run result when the SDK provides one. -5. A job summary is written with the status, exit code, agent response, and any stderr or diagnostics. +5. A job summary is written with the status, exit code, agent response, duration, token usage, and any stderr or diagnostics. -## Timeouts report success +## Timeouts and failures -When `timeout` elapses the action asks the SDK to cancel the run, and a cancelled run still exits `0`. The same is true when the run itself ends in an error state: `run.wait()` returns a result carrying `status` and `error`, and the action currently ignores both. +When `timeout` elapses, the action requests a run cancellation. Cancelled runs exit with code `1`. The step fails and reports the cancellation in the job summary. -Until v2 maps those onto real exit codes, a green step does not prove the agent finished. +The same applies when a run ends with an error. The action reads `status` and `error` from `run.wait()`, sets exit code `1`, and writes the error details to stderr. ## Why it installs instead of shipping one file @@ -27,7 +27,7 @@ The action is a composite action: it sets up Node.js 24, runs `npm ci --omit=dev ## Treat the summary as untrusted -`summary` is model output. Interpolating it into a `run:` script or a `github-script` body splices the text into the script before it executes. Pass it through `env:` instead — every example on [Examples](/examples) does. +`summary` is model output. Interpolating it into a `run:` script or a `github-script` body splices the text into the script before it executes. Pass it through `env:` instead. Every example on [Examples](/examples) does. ## Isolation diff --git a/docs/content/quickstart.mdx b/docs/content/quickstart.mdx index b4dda4f..7ad1018 100644 --- a/docs/content/quickstart.mdx +++ b/docs/content/quickstart.mdx @@ -27,9 +27,6 @@ The action runs on `ubuntu-latest`, `windows-latest`, and `macos-latest`. ## What it does not do yet -Two inputs are accepted but have no effect. They are documented on [Reference](/reference), and both are addressed in v2: - -- `permissions` does **not** restrict the agent. `read-only` will not stop it from editing files or running shell commands. -- A run that times out or fails inside the agent still reports success. +The `permissions` input does not restrict the agent today. Passing `read-only` will not stop it from editing files or running shell commands. Tool restrictions connect to the SDK in v2. Read [Behaviour](/behaviour) before wiring this into anything that gates a merge. diff --git a/docs/content/reference.mdx b/docs/content/reference.mdx index a452acb..7cc07fb 100644 --- a/docs/content/reference.mdx +++ b/docs/content/reference.mdx @@ -21,10 +21,11 @@ The tables below are generated from `action.yml` by `bun run docs:reference`, an ### Outputs -| Output | Description | -| ----------- | ----------------------------------------- | -| `summary` | Text summary returned by the cursor-agent | -| `exit-code` | Exit code from the cursor-agent process | +| Output | Description | +| --- | --- | +| `summary` | Text summary returned by the cursor-agent | +| `exit-code` | Exit code from the cursor-agent process | +| `status` | Run status returned by the cursor-agent (finished, error, or cancelled) | {/* reference:end */} @@ -42,6 +43,8 @@ Ignored since v1.0.0. The SDK resolves the agent version itself, and pinning a C ## Outputs -`summary` is model output. Pass it through `env:` rather than interpolating `${{ steps..outputs.summary }}` into a `run:` script or a `github-script` body — interpolation splices the text into the script before it executes. Every example on [Examples](/examples) does this. +`summary` is model output. Pass it through `env:` instead of interpolating `${{ steps..outputs.summary }}` into a `run:` script or a `github-script` body, because interpolation splices text before execution. Every example on [Examples](/examples) follows this pattern. -`exit-code` is `0` when the SDK call succeeded and `1` when it threw. It does not yet distinguish a timeout from a failure; see [Behaviour](/behaviour). +`exit-code` is `0` when the agent run finishes successfully. It is `1` when the run times out, cancels, or encounters an error. + +`status` reports the terminal state of the agent run: `finished`, `error`, or `cancelled`. diff --git a/docs/content/troubleshooting.mdx b/docs/content/troubleshooting.mdx index df62edf..5753605 100644 --- a/docs/content/troubleshooting.mdx +++ b/docs/content/troubleshooting.mdx @@ -15,9 +15,9 @@ The key is valid but the account cannot run agents. This is billing, not configu Start from `default`. The SDK rejects `auto`, and the error lists the ids your account can actually use. `model` is passed through untouched. -## The step succeeded but nothing happened +## The step timed out or failed -Check the job summary. A cancelled or errored run currently still exits `0`, so an empty `summary` with a green step usually means the run hit `timeout`. See [Behaviour](/behaviour). +Check the job summary. A cancelled or timed-out run exits with code `1` and marks the step as failed. You can increase `timeout` if the agent needs more time to complete. ## `Cannot find module` From af584e2a5a6a3a6cd91c4c8aa247cb9d4fb7ab1e Mon Sep 17 00:00:00 2001 From: PunGrumpy <108584943+PunGrumpy@users.noreply.github.com> Date: Fri, 4 Sep 2026 03:06:33 +0000 Subject: [PATCH 4/5] chore: add patch changeset for status handling and failure reporting --- .changeset/report-agent-status-and-failures.md | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 .changeset/report-agent-status-and-failures.md diff --git a/.changeset/report-agent-status-and-failures.md b/.changeset/report-agent-status-and-failures.md new file mode 100644 index 0000000..c596c09 --- /dev/null +++ b/.changeset/report-agent-status-and-failures.md @@ -0,0 +1,10 @@ +--- +"@pungrumpy/cursor-action": patch +--- + +Handle agent run statuses and report failures accurately: + +- Map cancelled and errored agent runs from `run.wait()` to exit code `1` instead of reporting success. +- Surface error messages from `runResult.error` in stderr and the step summary when a run fails. +- Expose the new `status` output (`finished`, `error`, or `cancelled`). +- Display run duration and total token usage in the job summary table when available from the SDK. From a106f8ad654b6f99f7713b4deb78e43b704a3b08 Mon Sep 17 00:00:00 2001 From: PunGrumpy <108584943+PunGrumpy@users.noreply.github.com> Date: Fri, 4 Sep 2026 03:14:13 +0000 Subject: [PATCH 5/5] fix: address review comments on status propagation test and timeout docs --- __tests__/output.test.ts | 10 +++++----- docs/content/troubleshooting.mdx | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/__tests__/output.test.ts b/__tests__/output.test.ts index 2cfbd59..14cc539 100644 --- a/__tests__/output.test.ts +++ b/__tests__/output.test.ts @@ -132,8 +132,8 @@ describe("setOutputs", () => { it("includes custom status, duration, and token usage in job summary", async () => { const result = { durationMs: 3500, - exitCode: 0, - status: "finished", + exitCode: 1, + status: "cancelled", stderr: "", stdout: "Done", usage: { @@ -141,11 +141,11 @@ describe("setOutputs", () => { }, }; const outputs = await setOutputs(result); - expect(outputs.status).toBe("finished"); - expect(mockSetOutput).toHaveBeenCalledWith("status", "finished"); + expect(outputs.status).toBe("cancelled"); + expect(mockSetOutput).toHaveBeenCalledWith("status", "cancelled"); expect(mockSummaryChain.addTable).toHaveBeenCalledWith( expect.arrayContaining([ - ["Agent Status", "finished"], + ["Agent Status", "cancelled"], ["Duration", "3.5s"], ["Total Tokens", "250"], ]) diff --git a/docs/content/troubleshooting.mdx b/docs/content/troubleshooting.mdx index 5753605..a59a678 100644 --- a/docs/content/troubleshooting.mdx +++ b/docs/content/troubleshooting.mdx @@ -17,7 +17,7 @@ Start from `default`. The SDK rejects `auto`, and the error lists the ids your a ## The step timed out or failed -Check the job summary. A cancelled or timed-out run exits with code `1` and marks the step as failed. You can increase `timeout` if the agent needs more time to complete. +Check the job summary. When a run reports `cancelled` (including timeout-triggered cancellation) or `error`, the action exits with code `1`. If an agent does not support cancellation, it may finish with exit code `0`. You can increase `timeout` if the agent needs more time to complete. ## `Cannot find module`