From 46b02a51bb976f4e14689937caa4f03b14e36e23 Mon Sep 17 00:00:00 2001 From: yashrajbasav Date: Mon, 5 Oct 2026 15:02:15 +0530 Subject: [PATCH 1/3] fix(opencode): allow the default LLM workflow to finish Signed-off-by: yashrajbasav --- .opencode/tools/skillspector_scan_lib.ts | 11 +++++-- tests/opencode/skillspector_scan_lib.test.ts | 33 ++++++++++++++++++++ 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/.opencode/tools/skillspector_scan_lib.ts b/.opencode/tools/skillspector_scan_lib.ts index b562042de..1da42fd98 100644 --- a/.opencode/tools/skillspector_scan_lib.ts +++ b/.opencode/tools/skillspector_scan_lib.ts @@ -4,6 +4,9 @@ import path from "node:path" export const TIMEOUT_MS = 120_000 +// The CLI's default workflow budget is 600s. Allow time to start and report +// after that deadline; static-only scans retain the shorter process limit. +export const LLM_TIMEOUT_MS = 630_000 export const MAX_STDOUT = 12_000 export const MAX_STDERR = 6_000 export const INSTALL_HINT = @@ -169,6 +172,7 @@ export function formatExecError( bin: string, err: unknown, env: Env = process.env, + timeoutMs: number = TIMEOUT_MS, ): string { const e = err as ExecFailure const partialOut = typeof e.stdout === "string" ? e.stdout : "" @@ -185,7 +189,7 @@ export function formatExecError( } if (e.killed) { return ( - `SkillSpector scan timed out after ${TIMEOUT_MS / 1000}s (killed).` + + `SkillSpector scan timed out after ${timeoutMs / 1000}s (killed).` + (evidence ? ` Partial output:\n${evidence}` : "") ) } @@ -518,6 +522,7 @@ export async function executeScan( ? pathApi.resolve(baseDir, configuredBin) : configuredBin const cliArgs = buildCliArgs(prepared) + const timeoutMs = (args.noLlm ?? true) ? TIMEOUT_MS : LLM_TIMEOUT_MS for (const request of buildPermissionRequests(prepared, { directory: baseDir, @@ -552,7 +557,7 @@ export async function executeScan( try { const result = await deps.runFile(bin, cliArgs, { - timeout: TIMEOUT_MS, + timeout: timeoutMs, maxBuffer: 32 * 1024 * 1024, cwd: baseDir, signal: context.abort, @@ -561,6 +566,6 @@ export async function executeScan( }) return formatSuccess(output, result.stdout, result.stderr, env) } catch (err: unknown) { - return formatExecError(bin, err, env) + return formatExecError(bin, err, env, timeoutMs) } } diff --git a/tests/opencode/skillspector_scan_lib.test.ts b/tests/opencode/skillspector_scan_lib.test.ts index 84a58f7a4..7b759cb2b 100644 --- a/tests/opencode/skillspector_scan_lib.test.ts +++ b/tests/opencode/skillspector_scan_lib.test.ts @@ -7,6 +7,7 @@ import assert from "node:assert/strict" import path from "node:path" import { CREDENTIAL_ENV_NAMES, + LLM_TIMEOUT_MS, MAX_STDERR, MAX_STDOUT, TIMEOUT_MS, @@ -257,6 +258,37 @@ describe("permissioned execution", () => { } } + it("lets opted-in LLM scans finish the CLI budget while bounding static scans", async () => { + for (const noLlm of [undefined, true, false]) { + await executeScan( + { target: "./skill", noLlm }, + context(async () => {}), + { + runFile: async (_bin, _args, options) => { + assert.equal(options.timeout, noLlm === false ? 630_000 : 120_000) + return { stdout: "complete report", stderr: "" } + }, + env: {}, + }, + ) + } + }) + + it("reports the actual LLM process limit and retains partial output on timeout", async () => { + const out = await executeScan( + { target: "./skill", noLlm: false }, + context(async () => {}), + { + runFile: async () => { + throw { killed: true, stdout: "partial report" } + }, + env: {}, + }, + ) + assert.ok(out.includes("timed out after 630s")) + assert.ok(out.includes("partial report")) + }) + it("requests scoped host, external-path, process, and LLM permissions", async () => { const requests: PermissionRequest[] = [] let runCalls = 0 @@ -688,6 +720,7 @@ describe("childProcessEnv", () => { describe("constants", () => { it("keeps the documented caps", () => { assert.equal(TIMEOUT_MS, 120_000) + assert.equal(LLM_TIMEOUT_MS, 630_000) assert.equal(MAX_STDOUT, 12_000) assert.equal(MAX_STDERR, 6_000) }) From 94355049d2df2d2bd8d64bb9df84648e45471d2f Mon Sep 17 00:00:00 2001 From: yashrajbasav Date: Mon, 5 Oct 2026 15:14:46 +0530 Subject: [PATCH 2/3] docs: explain the OpenCode scan process limits Signed-off-by: yashrajbasav --- docs/OPENCODE_EXTENSION.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/OPENCODE_EXTENSION.md b/docs/OPENCODE_EXTENSION.md index 957891767..4dd2839e9 100644 --- a/docs/OPENCODE_EXTENSION.md +++ b/docs/OPENCODE_EXTENSION.md @@ -52,6 +52,11 @@ the subprocess starts. - `output`: optional report path. - `noLlm`: default `true`. +Static scans have a 120-second subprocess limit. With `noLlm=false`, the limit +is 630 seconds to allow the CLI's default 600-second workflow to finish and +write its report. Session cancellation still stops the scan; a longer configured +CLI workflow budget does not extend this tool limit. + Unlike the [Pi extension](PI_EXTENSION.md), this tool has no `provider`, `model`, `yaraRulesDir`, or `verbose` parameters: LLM-backed analysis is configured through the environment instead (see below). ## LLM-backed analysis From 104b7d0e42a89e672f075569775d27d6eb5df83d Mon Sep 17 00:00:00 2001 From: yashrajbasav Date: Mon, 5 Oct 2026 21:24:38 +0530 Subject: [PATCH 3/3] fix(pi): allow explicit LLM scans to use the workflow budget Signed-off-by: yashrajbasav --- docs/PI_EXTENSION.md | 5 +++++ extensions/skillspector.ts | 3 ++- tests/unit/test_pi_extension.mjs | 9 +++++++++ 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/docs/PI_EXTENSION.md b/docs/PI_EXTENSION.md index d889807ea..58e4b86e4 100644 --- a/docs/PI_EXTENSION.md +++ b/docs/PI_EXTENSION.md @@ -52,6 +52,11 @@ Equivalent CLI: Static scan is default. To use semantic LLM analysis, configure provider credentials in your shell before launching Pi, then call the tool with `noLlm=false` and a provider. +Static scans have a 120-second process limit. Explicit LLM scans have a +630-second limit, allowing the CLI's default 600-second workflow budget plus +startup and report writing. This tool limit stays fixed even if +`SKILLSPECTOR_MAX_WORKFLOW_SECONDS` is configured above 600. + Example: ```text diff --git a/extensions/skillspector.ts b/extensions/skillspector.ts index bec77df11..8e0ae8667 100644 --- a/extensions/skillspector.ts +++ b/extensions/skillspector.ts @@ -147,7 +147,8 @@ export default function (pi: ExtensionAPI) { cwd: ctx.cwd, env, signal, - timeout: 120000, + // Match the CLI's 600s workflow budget plus startup/report headroom. + timeout: (params.noLlm ?? true) ? 120000 : 630000, }); const stdout = truncateText(redactSecrets(result.stdout ?? "")); diff --git a/tests/unit/test_pi_extension.mjs b/tests/unit/test_pi_extension.mjs index 68ad607f2..3bee78250 100644 --- a/tests/unit/test_pi_extension.mjs +++ b/tests/unit/test_pi_extension.mjs @@ -90,6 +90,15 @@ test("finds the Windows virtualenv executable without a PATH fallback", async (t } }); +for (const [noLlm, timeout] of [[undefined, 120000], [true, 120000], [false, 630000]]) { + test(`uses ${timeout}ms for noLlm=${noLlm}`, async (t) => { + const ctx = await setup(t); + await ctx.scan({ noLlm }); + assert.equal(ctx.calls[0].options.timeout, timeout); + assert.equal(ctx.calls[0].args.includes("--no-llm"), noLlm ?? true); + }); +} + test("uses an absolute operator override and preserves URL targets", async (t) => { const ctx = await setup(t); process.env.SKILLSPECTOR_BIN = join(ctx.root, "custom-cli");