mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
refactor(hooks): tighten the hook-protocol contract surface
Implement the tighten-hook-protocol-contract RFC (moved to implemented/): - HookDialect narrows to 'claude' | 'codex': the 'native' variant had zero producers (native plugins on the seams write no hook/* provenance), and the dialect is defined as the bridge that ran the hook. - HookOutput.suppressOutput is gone: the codec parsed it and every path discarded it with no warn and no deferral — hook stdout never enters a transcript, so there is nothing to suppress. - hook/result.durationMs is gone: durable timing telemetry with no reader that the snapshot normalizer had to scrub as replay noise. With no duration to measure, runHook loses its injected now clock and the single-field RunHookResult wrapper — it returns the HookOutput directly. The committed hook fixtures had the field stripped mechanically (field-only diff); the stdout goldens never carried it. - The bridges' double-defaulted defaultTimeoutMs config knob is replaced by one reference-default constant, DEFAULT_HOOK_TIMEOUT_MS, exported from the lib's runner and applied inside runHook; per-hook timeoutSec stays the override surface. - The hook/result semantics move into the lib that declares the event: HookResultRecord now carries the decoded HookOutput and appendHookResult derives the decision string (decision ?? stop-on-continue:false ?? pass) and the 500-char stderrSummary truncation; both bridges delete their byte-identical private copies. The snapshot suite passes against the existing goldens, proving the derived values are unchanged. - Rider: BLOCKING_EXIT_CODE is codec-internal again (zero importers). Amend the hook-protocol-lib and hook-snapshot-matrix RFCs to the new facts, update the lib/bridge READMEs and the session.md event tables, and retarget the affected unit tests (including new lib-level coverage of the derivation rules).
This commit is contained in:
@@ -47,14 +47,11 @@ export interface Config {
|
||||
configPath: string
|
||||
/** The model name stamped on every payload (Codex includes `model` on each event). */
|
||||
model?: string
|
||||
/** Default per-hook timeout in ms when a hook sets none (Codex default: 600000). */
|
||||
defaultTimeoutMs?: number
|
||||
}
|
||||
|
||||
export const Config: z<Config> = z.object({
|
||||
configPath: z.string().required(),
|
||||
model: z.string().default(''),
|
||||
defaultTimeoutMs: z.number().default(600_000),
|
||||
})
|
||||
|
||||
let handlerCounter = 0
|
||||
@@ -64,12 +61,6 @@ function nextHandlerId(point: string): string {
|
||||
|
||||
const PLUGIN_SOURCE: MessageSource = { kind: 'plugin', plugin: 'hooks-codex' }
|
||||
|
||||
function summarize(stderr: string): string | undefined {
|
||||
const t = stderr.trim()
|
||||
if (t.length === 0) return undefined
|
||||
return t.length > 500 ? t.slice(0, 500) + '…' : t
|
||||
}
|
||||
|
||||
export function apply(ctx: Context, config: Config): void {
|
||||
let parsed: CodexHookConfig = {}
|
||||
try {
|
||||
@@ -84,7 +75,6 @@ export function apply(ctx: Context, config: Config): void {
|
||||
return
|
||||
}
|
||||
|
||||
const defaultTimeoutMs = config.defaultTimeoutMs ?? 600_000
|
||||
const model = config.model ?? ''
|
||||
|
||||
async function runPoint(
|
||||
@@ -111,15 +101,14 @@ export function apply(ctx: Context, config: Config): void {
|
||||
...group.matcher !== undefined ? { matcher: group.matcher } : {},
|
||||
})
|
||||
}
|
||||
const { output, durationMs } = await runHook(ctx.bash, hook, {
|
||||
const output = await runHook(ctx.bash, hook, {
|
||||
payload,
|
||||
...workdir !== undefined ? { cwd: workdir } : {},
|
||||
...opts.signal ? { signal: opts.signal } : {},
|
||||
defaultTimeoutMs,
|
||||
trailingNewline: false, // Codex writes stdin WITHOUT a trailing newline.
|
||||
// Discard a `hookSpecificOutput` block naming a different event.
|
||||
expectedEventName: point,
|
||||
}, () => performance.now())
|
||||
})
|
||||
// Codex's SessionStart/UserPromptSubmit treat a CLEAN hook's PLAIN
|
||||
// (non-JSON) stdout as additionalContext. The codec keeps that raw text on
|
||||
// `output.stdout` but only sets `additionalContext` from a JSON
|
||||
@@ -140,14 +129,7 @@ export function apply(ctx: Context, config: Config): void {
|
||||
ctx.logger.warn(`hooks-codex: ${point} hook emitted a systemMessage, which is not yet surfaced (ignored)`)
|
||||
}
|
||||
if (session && opts.turn !== undefined) {
|
||||
const stderrSummary = summarize(output.stderr)
|
||||
appendHookResult(session, {
|
||||
turn: opts.turn, point, handlerId,
|
||||
decision: output.decision ?? (output.continue === false ? 'stop' : 'pass'),
|
||||
...output.exitCode !== undefined ? { exitCode: output.exitCode } : {},
|
||||
...stderrSummary !== undefined ? { stderrSummary } : {},
|
||||
durationMs,
|
||||
})
|
||||
appendHookResult(session, { turn: opts.turn, point, handlerId, output })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user