diff --git a/.changeset/goal-tick-provider-env.md b/.changeset/goal-tick-provider-env.md new file mode 100644 index 000000000..681c6129e --- /dev/null +++ b/.changeset/goal-tick-provider-env.md @@ -0,0 +1,11 @@ +--- +id: goal-tick-provider-env +tasks: [T13884] +kind: feat +summary: the Stop dispatcher passes CLEO_HOOK_PROVIDER (claude-code or codex) to the goal tick +--- + +`cleo hook stop --provider
` now spawns the goal tick with +`CLEO_HOOK_PROVIDER=
` added to the inherited environment, so `cleo-goal tick`
+can tell which harness's Stop it is answering. The name is exported as
+`GOAL_TICK_PROVIDER_ENV` from `@cleocode/contracts/goal-runtime.js`.
diff --git a/packages/cleo/src/cli/__tests__/stop-dispatch-entry.test.ts b/packages/cleo/src/cli/__tests__/stop-dispatch-entry.test.ts
index 4a650d028..60d59fbf3 100644
--- a/packages/cleo/src/cli/__tests__/stop-dispatch-entry.test.ts
+++ b/packages/cleo/src/cli/__tests__/stop-dispatch-entry.test.ts
@@ -7,6 +7,7 @@
* @task T13796
*/
+import { GOAL_TICK_PROVIDER_ENV } from '@cleocode/contracts/goal-runtime.js';
import { describe, expect, it, vi } from 'vitest';
import {
askEnforceOptedOut,
@@ -216,6 +217,15 @@ describe('stopDispatch (T13796)', () => {
expect(r.stdout).not.toContain('pwned');
});
+ it('the tick child sees CLEO_HOOK_PROVIDER set to the dispatching harness (T13884)', async () => {
+ const echo = `node -e "process.stdout.write(JSON.stringify({p: process.env.${GOAL_TICK_PROVIDER_ENV}, h: process.env.HOME ?? null}))"`;
+ for (const provider of ['claude-code', 'codex'] as const) {
+ const run = await runTickKillable(echo, STOP, 5000, provider);
+ expect(run.status).toBe(0);
+ expect(JSON.parse(run.stdout)).toEqual({ p: provider, h: process.env.HOME ?? null });
+ }
+ });
+
it('a tick that traps SIGTERM is SIGKILLed with its group at the deadline, and fails open', async () => {
const marker = `cleo-t13796-${process.pid}-${Date.now()}`;
const trap = `node -e "process.on('SIGTERM',()=>{});setInterval(()=>{},1000)" ${marker}-trap`;
diff --git a/packages/cleo/src/cli/stop-dispatch-entry.ts b/packages/cleo/src/cli/stop-dispatch-entry.ts
index 9e8c0d6a0..19eb843f6 100644
--- a/packages/cleo/src/cli/stop-dispatch-entry.ts
+++ b/packages/cleo/src/cli/stop-dispatch-entry.ts
@@ -43,6 +43,7 @@ import { spawn, spawnSync } from 'node:child_process';
import { existsSync } from 'node:fs';
import { dirname, join, resolve } from 'node:path';
import type { HookJsonValue } from '@cleocode/contracts';
+import { GOAL_TICK_PROVIDER_ENV } from '@cleocode/contracts/goal-runtime.js';
/** Harnesses `cleo hook stop` dispatches for. */
export const STOP_DISPATCH_PROVIDERS = ['claude-code', 'codex'] as const;
@@ -231,7 +232,7 @@ export function processStopDispatchDeps(
return goalTickCommand({ ...process.env, ...env });
},
hasGoalWorkspace: (input) => hasGoalWorkspace(input, env),
- runTick: runTickKillable,
+ runTick: (command, input, timeoutMs) => runTickKillable(command, input, timeoutMs, provider),
endSession: () => {
const cli = process.argv[1];
spawnSync(
@@ -285,12 +286,15 @@ export function hasGoalWorkspace(
* @param command - The tick command line.
* @param input - The Stop JSON for its stdin.
* @param timeoutMs - Deadline.
+ * @param provider - The harness whose Stop this is, passed to the tick as
+ * {@link GOAL_TICK_PROVIDER_ENV}; the rest of the environment is inherited.
* @returns What the tick did.
*/
export function runTickKillable(
command: string,
input: string,
timeoutMs: number,
+ provider?: StopDispatchProvider,
): Promise