diff --git a/plugins/codex/scripts/codex-companion.mjs b/plugins/codex/scripts/codex-companion.mjs index 201d1c7..aafe136 100644 --- a/plugins/codex/scripts/codex-companion.mjs +++ b/plugins/codex/scripts/codex-companion.mjs @@ -11,8 +11,8 @@ import { buildPersistentTaskThreadName, DEFAULT_CONTINUE_PROMPT, findLatestTaskThread, + getCodexAuthStatus, getCodexAvailability, - getCodexLoginStatus, getSessionRuntimeStatus, interruptAppServerTurn, parseStructuredOutput, @@ -176,19 +176,19 @@ function firstMeaningfulLine(text, fallback) { return line ?? fallback; } -function buildSetupReport(cwd, actionsTaken = []) { +async function buildSetupReport(cwd, actionsTaken = []) { const workspaceRoot = resolveWorkspaceRoot(cwd); const nodeStatus = binaryAvailable("node", ["--version"], { cwd }); const npmStatus = binaryAvailable("npm", ["--version"], { cwd }); const codexStatus = getCodexAvailability(cwd); - const authStatus = getCodexLoginStatus(cwd); + const authStatus = await getCodexAuthStatus(cwd); const config = getConfig(workspaceRoot); const nextSteps = []; if (!codexStatus.available) { nextSteps.push("Install Codex with `npm install -g @openai/codex`."); } - if (codexStatus.available && !authStatus.loggedIn) { + if (codexStatus.available && !authStatus.loggedIn && authStatus.requiresOpenaiAuth) { nextSteps.push("Run `!codex login`."); nextSteps.push("If browser login is blocked, retry with `!codex login --device-auth` or `!codex login --with-api-key`."); } @@ -209,7 +209,7 @@ function buildSetupReport(cwd, actionsTaken = []) { }; } -function handleSetup(argv) { +async function handleSetup(argv) { const { options } = parseCommandInput(argv, { valueOptions: ["cwd"], booleanOptions: ["json", "enable-review-gate", "disable-review-gate"] @@ -231,7 +231,7 @@ function handleSetup(argv) { actionsTaken.push(`Disabled the stop-time review gate for ${workspaceRoot}.`); } - const finalReport = buildSetupReport(cwd, actionsTaken); + const finalReport = await buildSetupReport(cwd, actionsTaken); outputResult(options.json ? finalReport : renderSetupReport(finalReport), options.json); } @@ -245,14 +245,11 @@ function buildAdversarialReviewPrompt(context, focusText) { }); } -function ensureCodexReady(cwd) { - const authStatus = getCodexLoginStatus(cwd); - if (!authStatus.available) { +function ensureCodexAvailable(cwd) { + const availability = getCodexAvailability(cwd); + if (!availability.available) { throw new Error("Codex CLI is not installed or is missing required runtime support. Install it with `npm install -g @openai/codex`, then rerun `/codex:setup`."); } - if (!authStatus.loggedIn) { - throw new Error("Codex CLI is not authenticated. Run `!codex login` and retry."); - } } function buildNativeReviewTarget(target) { @@ -325,7 +322,7 @@ async function resolveLatestTrackedTaskThread(cwd, options = {}) { } async function executeReviewRun(request) { - ensureCodexReady(request.cwd); + ensureCodexAvailable(request.cwd); ensureGitRepository(request.cwd); const target = resolveReviewTarget(request.cwd, { @@ -429,7 +426,7 @@ async function executeReviewRun(request) { async function executeTaskRun(request) { const workspaceRoot = resolveWorkspaceRoot(request.cwd); - ensureCodexReady(request.cwd); + ensureCodexAvailable(request.cwd); const taskMetadata = buildTaskRunMetadata({ prompt: request.prompt, @@ -728,7 +725,7 @@ async function handleTask(argv) { }); if (options.background) { - ensureCodexReady(cwd); + ensureCodexAvailable(cwd); requireTaskRequest(prompt, resumeLast); const job = buildTaskJob(workspaceRoot, taskMetadata, write); @@ -967,7 +964,7 @@ async function main() { switch (subcommand) { case "setup": - handleSetup(argv); + await handleSetup(argv); break; case "review": await handleReview(argv); diff --git a/plugins/codex/scripts/lib/app-server-protocol.d.ts b/plugins/codex/scripts/lib/app-server-protocol.d.ts index 7553dc8..cc6446d 100644 --- a/plugins/codex/scripts/lib/app-server-protocol.d.ts +++ b/plugins/codex/scripts/lib/app-server-protocol.d.ts @@ -51,6 +51,7 @@ export interface CodexAppServerClientOptions { capabilities?: InitializeCapabilities; brokerEndpoint?: string; disableBroker?: boolean; + reuseExistingBroker?: boolean; } export interface AppServerMethodMap { diff --git a/plugins/codex/scripts/lib/app-server.mjs b/plugins/codex/scripts/lib/app-server.mjs index 0ca4435..7a075a4 100644 --- a/plugins/codex/scripts/lib/app-server.mjs +++ b/plugins/codex/scripts/lib/app-server.mjs @@ -13,7 +13,7 @@ import process from "node:process"; import { spawn } from "node:child_process"; import readline from "node:readline"; import { parseBrokerEndpoint } from "./broker-endpoint.mjs"; -import { ensureBrokerSession } from "./broker-lifecycle.mjs"; +import { ensureBrokerSession, loadBrokerSession } from "./broker-lifecycle.mjs"; import { terminateProcessTree } from "./process.mjs"; const PLUGIN_MANIFEST_URL = new URL("../../.claude-plugin/plugin.json", import.meta.url); @@ -333,7 +333,10 @@ export class CodexAppServerClient { let brokerEndpoint = null; if (!options.disableBroker) { brokerEndpoint = options.brokerEndpoint ?? options.env?.[BROKER_ENDPOINT_ENV] ?? process.env[BROKER_ENDPOINT_ENV] ?? null; - if (!brokerEndpoint) { + if (!brokerEndpoint && options.reuseExistingBroker) { + brokerEndpoint = loadBrokerSession(cwd)?.endpoint ?? null; + } + if (!brokerEndpoint && !options.reuseExistingBroker) { const brokerSession = await ensureBrokerSession(cwd, { env: options.env }); brokerEndpoint = brokerSession?.endpoint ?? null; } diff --git a/plugins/codex/scripts/lib/codex.mjs b/plugins/codex/scripts/lib/codex.mjs index bf7e8c8..ae53006 100644 --- a/plugins/codex/scripts/lib/codex.mjs +++ b/plugins/codex/scripts/lib/codex.mjs @@ -37,7 +37,7 @@ import { readJsonFile } from "./fs.mjs"; import { BROKER_BUSY_RPC_CODE, BROKER_ENDPOINT_ENV, CodexAppServerClient } from "./app-server.mjs"; import { loadBrokerSession } from "./broker-lifecycle.mjs"; -import { binaryAvailable, runCommand } from "./process.mjs"; +import { binaryAvailable } from "./process.mjs"; const SERVICE_NAME = "claude_code_codex_plugin"; const TASK_THREAD_PREFIX = "Codex Companion Task"; @@ -652,6 +652,134 @@ function buildResultStatus(turnState) { return turnState.finalTurn?.status === "completed" ? 0 : 1; } +const BUILTIN_PROVIDER_LABELS = new Map([ + ["openai", "OpenAI"], + ["ollama", "Ollama"], + ["lmstudio", "LM Studio"] +]); + +function normalizeProviderId(value) { + const providerId = typeof value === "string" ? value.trim() : ""; + return providerId || null; +} + +function formatProviderLabel(providerId, providerConfig = null) { + const configuredName = typeof providerConfig?.name === "string" ? providerConfig.name.trim() : ""; + if (configuredName) { + return configuredName; + } + if (!providerId) { + return "The active provider"; + } + return BUILTIN_PROVIDER_LABELS.get(providerId) ?? providerId; +} + +function buildAuthStatus(fields = {}) { + return { + available: true, + loggedIn: false, + detail: "not authenticated", + source: "unknown", + authMethod: null, + verified: null, + requiresOpenaiAuth: null, + provider: null, + ...fields + }; +} + +function resolveProviderConfig(configResponse) { + const config = configResponse?.config; + if (!config || typeof config !== "object") { + return { + providerId: null, + providerConfig: null + }; + } + + const providerId = normalizeProviderId(config.model_provider); + const providers = + config.model_providers && typeof config.model_providers === "object" && !Array.isArray(config.model_providers) + ? config.model_providers + : null; + const providerConfig = + providerId && providers?.[providerId] && typeof providers[providerId] === "object" ? providers[providerId] : null; + + return { + providerId, + providerConfig + }; +} + +function buildAppServerAuthStatus(accountResponse, configResponse) { + const account = accountResponse?.account ?? null; + const requiresOpenaiAuth = + typeof accountResponse?.requiresOpenaiAuth === "boolean" ? accountResponse.requiresOpenaiAuth : null; + const { providerId, providerConfig } = resolveProviderConfig(configResponse); + const providerLabel = formatProviderLabel(providerId, providerConfig); + + if (account?.type === "chatgpt") { + const email = typeof account.email === "string" && account.email.trim() ? account.email.trim() : null; + return buildAuthStatus({ + loggedIn: true, + detail: email ? `ChatGPT login active for ${email}` : "ChatGPT login active", + source: "app-server", + authMethod: "chatgpt", + verified: true, + requiresOpenaiAuth, + provider: providerId + }); + } + + if (account?.type === "apiKey") { + return buildAuthStatus({ + loggedIn: true, + detail: "API key configured (unverified)", + source: "app-server", + authMethod: "apiKey", + verified: false, + requiresOpenaiAuth, + provider: providerId + }); + } + + if (requiresOpenaiAuth === false) { + return buildAuthStatus({ + loggedIn: true, + detail: `${providerLabel} is configured and does not require OpenAI authentication`, + source: "app-server", + requiresOpenaiAuth, + provider: providerId + }); + } + + return buildAuthStatus({ + loggedIn: false, + detail: `${providerLabel} requires OpenAI authentication`, + source: "app-server", + requiresOpenaiAuth, + provider: providerId + }); +} + +async function getCodexAuthStatusFromClient(client, cwd) { + try { + const accountResponse = await client.request("account/read", { refreshToken: false }); + const configResponse = await client.request("config/read", { + includeLayers: false, + cwd + }); + + return buildAppServerAuthStatus(accountResponse, configResponse); + } catch (error) { + return buildAuthStatus({ + loggedIn: false, + detail: error instanceof Error ? error.message : String(error), + source: "app-server" + }); + } +} + export function getCodexAvailability(cwd) { const versionStatus = binaryAvailable("codex", ["--version"], { cwd }); if (!versionStatus.available) { @@ -691,38 +819,39 @@ export function getSessionRuntimeStatus(env = process.env, cwd = process.cwd()) }; } -export function getCodexLoginStatus(cwd) { +export async function getCodexAuthStatus(cwd, options = {}) { const availability = getCodexAvailability(cwd); if (!availability.available) { return { available: false, loggedIn: false, - detail: availability.detail + detail: availability.detail, + source: "availability", + authMethod: null, + verified: null, + requiresOpenaiAuth: null, + provider: null }; } - const result = runCommand("codex", ["login", "status"], { cwd }); - if (result.error) { - return { - available: true, + let client = null; + try { + client = await CodexAppServerClient.connect(cwd, { + env: options.env, + reuseExistingBroker: true + }); + return await getCodexAuthStatusFromClient(client, cwd); + } catch (error) { + return buildAuthStatus({ loggedIn: false, - detail: result.error.message - }; - } - - if (result.status === 0) { - return { - available: true, - loggedIn: true, - detail: result.stdout.trim() || "authenticated" - }; + detail: error instanceof Error ? error.message : String(error), + source: "app-server" + }); + } finally { + if (client) { + await client.close().catch(() => {}); + } } - - return { - available: true, - loggedIn: false, - detail: result.stderr.trim() || result.stdout.trim() || "not authenticated" - }; } export async function interruptAppServerTurn(cwd, { threadId, turnId }) { @@ -745,12 +874,9 @@ export async function interruptAppServerTurn(cwd, { threadId, turnId }) { }; } - const brokerEndpoint = process.env[BROKER_ENDPOINT_ENV] ?? loadBrokerSession(cwd)?.endpoint ?? null; let client = null; try { - client = brokerEndpoint - ? await CodexAppServerClient.connect(cwd, { brokerEndpoint }) - : await CodexAppServerClient.connect(cwd, { disableBroker: true }); + client = await CodexAppServerClient.connect(cwd, { reuseExistingBroker: true }); await client.request("turn/interrupt", { threadId, turnId }); return { attempted: true, diff --git a/plugins/codex/scripts/stop-review-gate-hook.mjs b/plugins/codex/scripts/stop-review-gate-hook.mjs index c22edbd..2346bdc 100644 --- a/plugins/codex/scripts/stop-review-gate-hook.mjs +++ b/plugins/codex/scripts/stop-review-gate-hook.mjs @@ -6,7 +6,7 @@ import path from "node:path"; import { spawnSync } from "node:child_process"; import { fileURLToPath } from "node:url"; -import { getCodexLoginStatus } from "./lib/codex.mjs"; +import { getCodexAvailability } from "./lib/codex.mjs"; import { loadPromptTemplate, interpolateTemplate } from "./lib/prompts.mjs"; import { getConfig, listJobs } from "./lib/state.mjs"; import { sortJobsNewestFirst } from "./lib/job-control.mjs"; @@ -57,13 +57,13 @@ function buildStopReviewPrompt(input = {}) { } function buildSetupNote(cwd) { - const authStatus = getCodexLoginStatus(cwd); - if (authStatus.available && authStatus.loggedIn) { + const availability = getCodexAvailability(cwd); + if (availability.available) { return null; } - const detail = authStatus.detail ? ` ${authStatus.detail}.` : ""; - return `Codex is not set up for the review gate.${detail} Run /codex:setup and, if needed, !codex login.`; + const detail = availability.detail ? ` ${availability.detail}.` : ""; + return `Codex is not set up for the review gate.${detail} Run /codex:setup.`; } function parseStopReviewOutput(rawOutput) { @@ -175,4 +175,10 @@ function main() { logNote(runningTaskNote); } -main(); +try { + main(); +} catch (error) { + const message = error instanceof Error ? error.message : String(error); + process.stderr.write(`${message}\n`); + process.exitCode = 1; +} diff --git a/tests/fake-codex-fixture.mjs b/tests/fake-codex-fixture.mjs index 1e6f13d..debcadc 100644 --- a/tests/fake-codex-fixture.mjs +++ b/tests/fake-codex-fixture.mjs @@ -63,6 +63,54 @@ function buildTurn(id, status = "inProgress", error = null) { return { id, status, items: [], error }; } +function buildAccountReadResult() { + switch (BEHAVIOR) { + case "logged-out": + case "refreshable-auth": + case "auth-run-fails": + return { account: null, requiresOpenaiAuth: true }; + case "provider-no-auth": + case "env-key-provider": + return { account: null, requiresOpenaiAuth: false }; + case "api-key-account-only": + return { account: { type: "apiKey" }, requiresOpenaiAuth: true }; + default: + return { + account: { type: "chatgpt", email: "test@example.com", planType: "plus" }, + requiresOpenaiAuth: true + }; + } +} + +function buildConfigReadResult() { + switch (BEHAVIOR) { + case "provider-no-auth": + return { + config: { model_provider: "ollama" }, + origins: {} + }; + case "env-key-provider": + return { + config: { + model_provider: "openai-custom", + model_providers: { + "openai-custom": { + name: "OpenAI custom", + env_key: "OPENAI_API_KEY", + requires_openai_auth: false + } + } + }, + origins: {} + }; + default: + return { + config: { model_provider: "openai" }, + origins: {} + }; + } +} + function send(message) { process.stdout.write(JSON.stringify(message) + "\\n"); } @@ -193,7 +241,7 @@ if (args[0] === "app-server" && args[1] === "--help") { process.exit(0); } if (args[0] === "login" && args[1] === "status") { - if (BEHAVIOR === "logged-out") { + if (BEHAVIOR === "logged-out" || BEHAVIOR === "refreshable-auth" || BEHAVIOR === "auth-run-fails" || BEHAVIOR === "provider-no-auth" || BEHAVIOR === "env-key-provider" || BEHAVIOR === "api-key-account-only") { console.error("not authenticated"); process.exit(1); } @@ -230,7 +278,21 @@ rl.on("line", (line) => { case "initialized": break; + case "account/read": + send({ id: message.id, result: buildAccountReadResult() }); + break; + + case "config/read": + if (BEHAVIOR === "config-read-fails") { + throw new Error("config/read failed for cwd"); + } + send({ id: message.id, result: buildConfigReadResult() }); + break; + case "thread/start": { + if (BEHAVIOR === "auth-run-fails") { + throw new Error("authentication expired; run codex login"); + } if (requiresExperimental("persistExtendedHistory", message, state) || requiresExperimental("persistFullHistory", message, state)) { throw new Error("thread/start.persistFullHistory requires experimentalApi capability"); } diff --git a/tests/runtime.test.mjs b/tests/runtime.test.mjs index 6000c89..9a39baa 100644 --- a/tests/runtime.test.mjs +++ b/tests/runtime.test.mjs @@ -65,6 +65,77 @@ test("setup is ready without npm when Codex is already installed and authenticat assert.equal(payload.auth.loggedIn, true); }); +test("setup trusts app-server API key auth even when login status alone would fail", () => { + const binDir = makeTempDir(); + installFakeCodex(binDir, "api-key-account-only"); + + const result = run("node", [SCRIPT, "setup", "--json"], { + cwd: ROOT, + env: buildEnv(binDir) + }); + + assert.equal(result.status, 0, result.stderr); + const payload = JSON.parse(result.stdout); + assert.equal(payload.ready, true); + assert.equal(payload.auth.loggedIn, true); + assert.equal(payload.auth.authMethod, "apiKey"); + assert.equal(payload.auth.source, "app-server"); + assert.match(payload.auth.detail, /API key configured \(unverified\)/); +}); + +test("setup is ready when the active provider does not require OpenAI login", () => { + const binDir = makeTempDir(); + installFakeCodex(binDir, "provider-no-auth"); + + const result = run("node", [SCRIPT, "setup", "--json"], { + cwd: ROOT, + env: buildEnv(binDir) + }); + + assert.equal(result.status, 0, result.stderr); + const payload = JSON.parse(result.stdout); + assert.equal(payload.ready, true); + assert.equal(payload.auth.loggedIn, true); + assert.equal(payload.auth.authMethod, null); + assert.equal(payload.auth.source, "app-server"); + assert.match(payload.auth.detail, /configured and does not require OpenAI authentication/i); +}); + +test("setup treats custom providers with app-server-ready config as ready", () => { + const binDir = makeTempDir(); + installFakeCodex(binDir, "env-key-provider"); + + const result = run("node", [SCRIPT, "setup", "--json"], { + cwd: ROOT, + env: buildEnv(binDir) + }); + + assert.equal(result.status, 0, result.stderr); + const payload = JSON.parse(result.stdout); + assert.equal(payload.ready, true); + assert.equal(payload.auth.loggedIn, true); + assert.equal(payload.auth.authMethod, null); + assert.equal(payload.auth.source, "app-server"); + assert.match(payload.auth.detail, /configured and does not require OpenAI authentication/i); +}); + +test("setup reports not ready when app-server config read fails", () => { + const binDir = makeTempDir(); + installFakeCodex(binDir, "config-read-fails"); + + const result = run("node", [SCRIPT, "setup", "--json"], { + cwd: ROOT, + env: buildEnv(binDir) + }); + + assert.equal(result.status, 0, result.stderr); + const payload = JSON.parse(result.stdout); + assert.equal(payload.ready, false); + assert.equal(payload.auth.loggedIn, false); + assert.equal(payload.auth.source, "app-server"); + assert.match(payload.auth.detail, /config\/read failed for cwd/); +}); + test("review renders a no-findings result from app-server review/start", () => { const repo = makeTempDir(); const binDir = makeTempDir(); @@ -86,6 +157,60 @@ test("review renders a no-findings result from app-server review/start", () => { assert.match(result.stdout, /No material issues found/); }); +test("task runs when the active provider does not require OpenAI login", () => { + const repo = makeTempDir(); + const binDir = makeTempDir(); + installFakeCodex(binDir, "provider-no-auth"); + initGitRepo(repo); + fs.writeFileSync(path.join(repo, "README.md"), "hello\n"); + run("git", ["add", "README.md"], { cwd: repo }); + run("git", ["commit", "-m", "init"], { cwd: repo }); + + const result = run("node", [SCRIPT, "task", "check auth preflight"], { + cwd: repo, + env: buildEnv(binDir) + }); + + assert.equal(result.status, 0, result.stderr); + assert.match(result.stdout, /Handled the requested task/); +}); + +test("task runs without auth preflight so Codex can refresh an expired session", () => { + const repo = makeTempDir(); + const binDir = makeTempDir(); + installFakeCodex(binDir, "refreshable-auth"); + initGitRepo(repo); + fs.writeFileSync(path.join(repo, "README.md"), "hello\n"); + run("git", ["add", "README.md"], { cwd: repo }); + run("git", ["commit", "-m", "init"], { cwd: repo }); + + const result = run("node", [SCRIPT, "task", "check refreshable auth"], { + cwd: repo, + env: buildEnv(binDir) + }); + + assert.equal(result.status, 0, result.stderr); + assert.match(result.stdout, /Handled the requested task/); +}); + +test("task reports the actual Codex auth error when the run is rejected", () => { + const repo = makeTempDir(); + const binDir = makeTempDir(); + installFakeCodex(binDir, "auth-run-fails"); + initGitRepo(repo); + fs.writeFileSync(path.join(repo, "README.md"), "hello\n"); + run("git", ["add", "README.md"], { cwd: repo }); + run("git", ["commit", "-m", "init"], { cwd: repo }); + + const result = run("node", [SCRIPT, "task", "check failed auth"], { + cwd: repo, + env: buildEnv(binDir) + }); + + assert.notEqual(result.status, 0); + assert.match(result.stderr, /authentication expired; run codex login/); +}); + test("review accepts the quoted raw argument style for built-in base-branch review", () => { const repo = makeTempDir(); const binDir = makeTempDir(); @@ -1598,10 +1723,10 @@ test("stop hook does not block when Codex is unavailable even if the review gate assert.match(allowed.stderr, /Run \/codex:setup/i); }); -test("stop hook does not block when Codex is not authenticated even if the review gate is enabled", () => { +test("stop hook runs the actual task when auth status looks stale", () => { const repo = makeTempDir(); const binDir = makeTempDir(); - installFakeCodex(binDir, "logged-out"); + installFakeCodex(binDir, "refreshable-auth"); initGitRepo(repo); fs.writeFileSync(path.join(repo, "README.md"), "hello\n"); run("git", ["add", "README.md"], { cwd: repo }); @@ -1620,10 +1745,10 @@ test("stop hook does not block when Codex is not authenticated even if the revie }); assert.equal(allowed.status, 0, allowed.stderr); - assert.equal(allowed.stdout.trim(), ""); - assert.match(allowed.stderr, /Codex is not set up for the review gate/i); - assert.match(allowed.stderr, /not authenticated/i); - assert.match(allowed.stderr, /!codex login/i); + assert.doesNotMatch(allowed.stderr, /Codex is not set up for the review gate/i); + const payload = JSON.parse(allowed.stdout); + assert.equal(payload.decision, "block"); + assert.match(payload.reason, /Missing empty-state guard/i); }); test("commands lazily start and reuse one shared app-server after first use", async () => { @@ -1671,6 +1796,51 @@ test("commands lazily start and reuse one shared app-server after first use", as assert.equal(cleanup.status, 0, cleanup.stderr); }); +test("setup reuses an existing shared app-server without starting another one", () => { + const repo = makeTempDir(); + const binDir = makeTempDir(); + const fakeStatePath = path.join(binDir, "fake-codex-state.json"); + + installFakeCodex(binDir); + initGitRepo(repo); + fs.writeFileSync(path.join(repo, "README.md"), "hello\n"); + run("git", ["add", "README.md"], { cwd: repo }); + run("git", ["commit", "-m", "init"], { cwd: repo }); + fs.writeFileSync(path.join(repo, "README.md"), "hello again\n"); + + const env = buildEnv(binDir); + + const review = run("node", [SCRIPT, "review"], { + cwd: repo, + env + }); + assert.equal(review.status, 0, review.stderr); + + const brokerSession = loadBrokerSession(repo); + if (!brokerSession) { + return; + } + + const setup = run("node", [SCRIPT, "setup", "--json"], { + cwd: repo, + env + }); + assert.equal(setup.status, 0, setup.stderr); + + const fakeState = JSON.parse(fs.readFileSync(fakeStatePath, "utf8")); + assert.equal(fakeState.appServerStarts, 1); + + const cleanup = run("node", [SESSION_HOOK, "SessionEnd"], { + cwd: repo, + env, + input: JSON.stringify({ + hook_event_name: "SessionEnd", + cwd: repo + }) + }); + assert.equal(cleanup.status, 0, cleanup.stderr); +}); + test("status reports shared session runtime when a lazy broker is active", () => { const repo = makeTempDir(); const binDir = makeTempDir();