Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
201 changes: 41 additions & 160 deletions .filesize-baseline.json
Original file line number Diff line number Diff line change
@@ -1,165 +1,46 @@
{
"limit": 350,
"entries": {
"scripts/audit-sdk-dependency.ts": {
"loc": 450,
"reason": "legacy: predates the ceiling gate; pending concern extraction"
},
"src/agent/background-registry.ts": {
"loc": 374,
"reason": "legacy: predates the ceiling gate; pending concern extraction"
},
"src/agent/daemon/scheduler.ts": {
"loc": 543,
"reason": "legacy: predates the ceiling gate; pending concern extraction"
},
"src/agent/memory/memory-store.ts": {
"loc": 687,
"reason": "legacy: predates the ceiling gate; pending concern extraction"
},
"src/agent/memory/memory-tools.ts": {
"loc": 437,
"reason": "legacy: predates the ceiling gate; pending concern extraction"
},
"src/agent/providers/openai-compatible/index.ts": {
"loc": 396,
"reason": "legacy: predates the ceiling gate; pending concern extraction"
},
"src/agent/providers/openai-compatible/query.ts": {
"loc": 803,
"reason": "legacy: predates the ceiling gate; pending concern extraction"
},
"src/agent/session/agent-session.ts": {
"loc": 837,
"reason": "legacy: predates the ceiling gate; pending concern extraction"
},
"src/agent/session/stream-consumer.ts": {
"loc": 359,
"reason": "legacy: predates the ceiling gate; pending concern extraction"
},
"src/agent/tools/compose-executor.ts": {
"loc": 559,
"reason": "legacy: predates the ceiling gate; pending concern extraction"
},
"src/agent/tools/dispatcher.ts": {
"loc": 611,
"reason": "legacy: predates the ceiling gate; pending concern extraction"
},
"src/agent/tools/readonly-bash.ts": {
"loc": 415,
"reason": "legacy: predates the ceiling gate; pending concern extraction"
},
"src/agent/tools/schemas.ts": {
"loc": 1277,
"reason": "legacy: predates the ceiling gate; pending concern extraction"
},
"src/agent/tools/subagent-executor.ts": {
"loc": 481,
"reason": "legacy: predates the ceiling gate; pending concern extraction"
},
"src/agent/trace/events.ts": {
"loc": 424,
"reason": "legacy: predates the ceiling gate; pending concern extraction"
},
"src/agent/trace/receipt.ts": {
"loc": 412,
"reason": "legacy: predates the ceiling gate; pending concern extraction"
},
"src/agent/worktree-sweep.ts": {
"loc": 512,
"reason": "legacy: predates the ceiling gate; pending concern extraction"
},
"src/cli/_lib/testing/virtual-screen.ts": {
"loc": 422,
"reason": "legacy: predates the ceiling gate; pending concern extraction"
},
"src/cli/commands/chat.ts": {
"loc": 570,
"reason": "legacy: predates the ceiling gate; pending concern extraction"
},
"src/cli/commands/daemon.ts": {
"loc": 392,
"reason": "legacy: predates the ceiling gate; pending concern extraction"
},
"src/cli/commands/farm.ts": {
"loc": 436,
"reason": "legacy: predates the ceiling gate; pending concern extraction"
},
"src/cli/commands/interactive.ts": {
"loc": 599,
"reason": "legacy: predates the ceiling gate; pending concern extraction"
},
"src/cli/commands/interactive/loop-iteration.ts": {
"loc": 464,
"reason": "legacy: predates the ceiling gate; pending concern extraction"
},
"src/cli/commands/interactive/tool-lane.ts": {
"loc": 448,
"reason": "legacy: predates the ceiling gate; pending concern extraction"
},
"src/cli/commands/interactive/turn-handler.ts": {
"loc": 365,
"reason": "legacy: predates the ceiling gate; pending concern extraction"
},
"src/cli/commands/interactive/worktree.ts": {
"loc": 478,
"reason": "legacy: predates the ceiling gate; pending concern extraction"
},
"src/cli/commands/trace.ts": {
"loc": 573,
"reason": "legacy: predates the ceiling gate; pending concern extraction"
},
"src/cli/elicitation/agent-question.ts": {
"loc": 399,
"reason": "legacy: predates the ceiling gate; pending concern extraction"
},
"src/cli/slash/commands/info.ts": {
"loc": 443,
"reason": "legacy: predates the ceiling gate; pending concern extraction"
},
"src/cli/terminal-compositor.input-dispatch.ts": {
"loc": 512,
"reason": "legacy: predates the ceiling gate; pending concern extraction"
},
"src/config/env.ts": {
"loc": 1846,
"reason": "legacy: predates the ceiling gate; pending concern extraction"
},
"src/config/import-sources.ts": {
"loc": 373,
"reason": "legacy: predates the ceiling gate; pending concern extraction"
},
"src/improve/eval-run/contracts.ts": {
"loc": 491,
"reason": "legacy: predates the ceiling gate; pending concern extraction"
},
"src/improve/eval-run/runner.ts": {
"loc": 372,
"reason": "legacy: predates the ceiling gate; pending concern extraction"
},
"src/improve/propose/template-engine.ts": {
"loc": 459,
"reason": "legacy: predates the ceiling gate; pending concern extraction"
},
"src/skills/audit-fit/index.ts": {
"loc": 421,
"reason": "legacy: predates the ceiling gate; pending concern extraction"
},
"src/telegram/bot.ts": {
"loc": 369,
"reason": "legacy: predates the ceiling gate; pending concern extraction"
},
"src/telegram/handlers/farm-callbacks.ts": {
"loc": 392,
"reason": "legacy: predates the ceiling gate; pending concern extraction"
},
"src/telegram/handlers/message.ts": {
"loc": 581,
"reason": "legacy: predates the ceiling gate; pending concern extraction"
},
"src/telegram/session-manager.ts": {
"loc": 468,
"reason": "legacy: predates the ceiling gate; pending concern extraction"
}
"scripts/audit-sdk-dependency.ts": { "loc": 450, "reason": "legacy: predates the ceiling gate; pending concern extraction" },
"src/agent/background-registry.ts": { "loc": 374, "reason": "legacy: predates the ceiling gate; pending concern extraction" },
"src/agent/daemon/scheduler.ts": { "loc": 543, "reason": "legacy: predates the ceiling gate; pending concern extraction" },
"src/agent/memory/memory-store.ts": { "loc": 687, "reason": "legacy: predates the ceiling gate; pending concern extraction" },
"src/agent/memory/memory-tools.ts": { "loc": 437, "reason": "legacy: predates the ceiling gate; pending concern extraction" },
"src/agent/providers/openai-compatible/index.ts": { "loc": 396, "reason": "legacy: predates the ceiling gate; pending concern extraction" },
"src/agent/providers/openai-compatible/query.ts": { "loc": 803, "reason": "legacy: predates the ceiling gate; pending concern extraction" },
"src/agent/session/agent-session.ts": { "loc": 837, "reason": "legacy: predates the ceiling gate; pending concern extraction" },
"src/agent/session/stream-consumer.ts": { "loc": 359, "reason": "legacy: predates the ceiling gate; pending concern extraction" },
"src/agent/subagent.ts": { "loc": 351, "reason": "legacy: predates the ceiling gate; pending concern extraction" },
"src/agent/tools/compose-executor.ts": { "loc": 559, "reason": "legacy: predates the ceiling gate; pending concern extraction" },
"src/agent/tools/dispatcher.ts": { "loc": 612, "reason": "legacy: predates the ceiling gate; pending concern extraction" },
"src/agent/tools/readonly-bash.ts": { "loc": 415, "reason": "legacy: predates the ceiling gate; pending concern extraction" },
"src/agent/tools/schemas.ts": { "loc": 1277, "reason": "legacy: predates the ceiling gate; pending concern extraction" },
"src/agent/tools/subagent-executor.ts": { "loc": 481, "reason": "legacy: predates the ceiling gate; pending concern extraction" },
"src/agent/trace/events.ts": { "loc": 424, "reason": "legacy: predates the ceiling gate; pending concern extraction" },
"src/agent/trace/receipt.ts": { "loc": 412, "reason": "legacy: predates the ceiling gate; pending concern extraction" },
"src/agent/worktree-sweep.ts": { "loc": 512, "reason": "legacy: predates the ceiling gate; pending concern extraction" },
"src/cli/_lib/testing/virtual-screen.ts": { "loc": 422, "reason": "legacy: predates the ceiling gate; pending concern extraction" },
"src/cli/commands/chat.ts": { "loc": 570, "reason": "legacy: predates the ceiling gate; pending concern extraction" },
"src/cli/commands/daemon.ts": { "loc": 392, "reason": "legacy: predates the ceiling gate; pending concern extraction" },
"src/cli/commands/farm.ts": { "loc": 436, "reason": "legacy: predates the ceiling gate; pending concern extraction" },
"src/cli/commands/interactive.ts": { "loc": 599, "reason": "legacy: predates the ceiling gate; pending concern extraction" },
"src/cli/commands/interactive/loop-iteration.ts": { "loc": 465, "reason": "legacy: predates the ceiling gate; pending concern extraction" },
"src/cli/commands/interactive/tool-lane.ts": { "loc": 457, "reason": "legacy: predates the ceiling gate; pending concern extraction" },
"src/cli/commands/interactive/turn-handler.ts": { "loc": 366, "reason": "legacy: predates the ceiling gate; pending concern extraction" },
"src/cli/commands/interactive/worktree.ts": { "loc": 478, "reason": "legacy: predates the ceiling gate; pending concern extraction" },
"src/cli/commands/trace.ts": { "loc": 573, "reason": "legacy: predates the ceiling gate; pending concern extraction" },
"src/cli/elicitation/agent-question.ts": { "loc": 399, "reason": "legacy: predates the ceiling gate; pending concern extraction" },
"src/cli/slash/commands/info.ts": { "loc": 443, "reason": "legacy: predates the ceiling gate; pending concern extraction" },
"src/cli/terminal-compositor.input-dispatch.ts": { "loc": 512, "reason": "legacy: predates the ceiling gate; pending concern extraction" },
"src/config/env.ts": { "loc": 1848, "reason": "legacy: predates the ceiling gate; pending concern extraction" },
"src/config/import-sources.ts": { "loc": 373, "reason": "legacy: predates the ceiling gate; pending concern extraction" },
"src/improve/eval-run/contracts.ts": { "loc": 491, "reason": "legacy: predates the ceiling gate; pending concern extraction" },
"src/improve/eval-run/runner.ts": { "loc": 372, "reason": "legacy: predates the ceiling gate; pending concern extraction" },
"src/improve/propose/template-engine.ts": { "loc": 459, "reason": "legacy: predates the ceiling gate; pending concern extraction" },
"src/skills/audit-fit/index.ts": { "loc": 421, "reason": "legacy: predates the ceiling gate; pending concern extraction" },
"src/telegram/bot.ts": { "loc": 369, "reason": "legacy: predates the ceiling gate; pending concern extraction" },
"src/telegram/handlers/farm-callbacks.ts": { "loc": 392, "reason": "legacy: predates the ceiling gate; pending concern extraction" },
"src/telegram/handlers/message.ts": { "loc": 581, "reason": "legacy: predates the ceiling gate; pending concern extraction" },
"src/telegram/session-manager.ts": { "loc": 443, "reason": "legacy: predates the ceiling gate; pending concern extraction" }
}
}
19 changes: 18 additions & 1 deletion src/agent/default-hook-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
type PathApprovalSurface,
} from './tools/hooks/path-approval-hook.js';
import { createBashRestrictionHook } from './tools/hooks/bash-restriction-hook.js';
import { createEditPreviewHook, type PreviewDiffRef } from './tools/hooks/edit-preview-hook.js';
import type { PermissionMode } from './types/sdk-types.js';
import type { TraceSink } from './trace/index.js';
import type { LoadedHooksConfig } from './hooks/config-loader.js';
Expand All @@ -39,6 +40,13 @@ export interface SubagentCompleteInfo {
export interface DefaultHookRegistryResult {
registry: HookRegistry;
memoryStore: MemoryStore;
/**
* Mutable ref the StreamRenderer arms at turn start. The edit-preview hook
* calls `addPreviewDiffRef.current(toolUseId, diff)` to deliver previews
* to the tool lane. Remains a no-op `() => {}` on non-interactive surfaces
* (the ref is never populated outside the REPL bootstrap).
*/
addPreviewDiffRef: PreviewDiffRef;
}

/**
Expand Down Expand Up @@ -151,6 +159,15 @@ export function createDefaultHookRegistry(
);
}

// Edit-preview hook: computes a diff preview at PreToolUse time for
// edit_file calls and delivers it to the tool lane via a mutable callback
// ref. Non-blocking, display-only. The ref starts as a no-op; the
// StreamRenderer arms it each turn on TTY surfaces.
const addPreviewDiffRef: PreviewDiffRef = {
current: () => {}, // no-op until StreamRenderer arms it each turn
};
registry.register('PreToolUse', createEditPreviewHook({ addPreviewDiffRef }));

// Path-approval + bash-restriction hooks. Both share a mutable grant-manager
// ref that the surface bootstrap populates after the provider exists.
// `AFK_DISABLE_PATH_APPROVAL=1` skips registration entirely — escape hatch
Expand Down Expand Up @@ -295,7 +312,7 @@ export function createDefaultHookRegistry(
});
}

return { registry, memoryStore: store };
return { registry, memoryStore: store, addPreviewDiffRef };
}

/**
Expand Down
6 changes: 6 additions & 0 deletions src/agent/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,12 @@ export interface PreToolUseContext {
* their per-session closure state, preserving prior behavior.
*/
grantManager?: GrantManager;
/**
* The tool-call id from the model's request. Injected by
* {@link SessionToolDispatcher.runPreDispatchGates} so hooks can correlate
* the PreToolUse event with an in-flight tool-lane entry.
*/
toolUseId?: string;
}

export interface PostToolUseContext {
Expand Down
23 changes: 14 additions & 9 deletions src/agent/hooks/config-bridge.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -370,13 +370,18 @@ describe('createDefaultHookRegistry integration', () => {
});

it('createDefaultHookRegistry without hookConfig → 0 config hooks registered', () => {
const { registry } = createDefaultHookRegistry();
// Built-in handlers exist for SubagentStop and SessionEnd, plus the THREE
const bundle = createDefaultHookRegistry();
const { registry } = bundle;
// Built-in handlers exist for SubagentStop and SessionEnd, plus the FOUR
// always-on built-in PreToolUse handlers (the ask-question gate, the
// observe-only safe-destruct detector, and the observe-only release-boundary
// detector), all registered unconditionally. No further PreToolUse hooks
// since we passed no hookConfig (path-approval disabled above).
expect(registry.count('PreToolUse')).toBe(3);
// observe-only safe-destruct detector, the observe-only release-boundary
// detector, and the edit-preview hook), all registered unconditionally. No
// further PreToolUse hooks since we passed no hookConfig (path-approval
// disabled above).
expect(registry.count('PreToolUse')).toBe(4);
// addPreviewDiffRef must be present on the bundle so the StreamRenderer can
// arm it each turn.
expect(bundle).toHaveProperty('addPreviewDiffRef');
});

it('createDefaultHookRegistry with hookConfig → config hooks ARE registered', () => {
Expand All @@ -401,9 +406,9 @@ describe('createDefaultHookRegistry integration', () => {
hookConfig,
{ cwd: projectCwd },
);
// 3 built-ins (ask-question gate + safe-destruct detector + release-boundary
// detector) + 1 config hook
expect(registry.count('PreToolUse')).toBe(4);
// 4 built-ins (ask-question gate + safe-destruct detector + release-boundary
// detector + edit-preview hook) + 1 config hook
expect(registry.count('PreToolUse')).toBe(5);
});

it('built-in SubagentStop handler still present when hookConfig is provided', () => {
Expand Down
3 changes: 3 additions & 0 deletions src/agent/tools/dispatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -927,6 +927,9 @@ export class SessionToolDispatcher implements ToolDispatcher {
...(this.sessionGrantManager !== undefined
? { grantManager: this.sessionGrantManager }
: {}),
// Inject the tool-call id so hooks (e.g. edit-preview-hook) can
// correlate this PreToolUse event with an in-flight tool-lane entry.
...(call.id !== undefined ? { toolUseId: call.id } : {}),
};
try {
await dispatchPreToolUse(this.hookRegistry, preCtx, {
Expand Down
74 changes: 74 additions & 0 deletions src/agent/tools/hooks/edit-preview-hook.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/**
* Tests for createEditPreviewHook — pre-execution diff preview hook.
*/
import { describe, it, expect, vi } from 'vitest';
import { createEditPreviewHook } from './edit-preview-hook.js';
import type { DiffPayload } from '../../../utils/diff.js';
import type { PreToolUseContext } from '../../hooks.js';

function makeCtx(overrides: Partial<PreToolUseContext> = {}): PreToolUseContext {
return {
event: 'PreToolUse',
toolName: 'edit_file',
input: { old_string: 'a', new_string: 'b', file_path: 'foo.ts' },
toolUseId: 'tu_001',
...overrides,
};
}

describe('createEditPreviewHook', () => {
it('(a) fires callback with DiffPayload for a normal edit', () => {
const cb = vi.fn();
const ref = { current: cb };
const hook = createEditPreviewHook({ addPreviewDiffRef: ref });
const decision = hook(makeCtx());
expect(cb).toHaveBeenCalledOnce();
const [id, diff] = cb.mock.calls[0] as [string, DiffPayload];
expect(id).toBe('tu_001');
expect(diff.hunks.length).toBeGreaterThan(0);
expect(decision).toEqual({});
});

it('(b) no-op when old_string === new_string', () => {
const cb = vi.fn();
const hook = createEditPreviewHook({ addPreviewDiffRef: { current: cb } });
hook(makeCtx({ input: { old_string: 'same', new_string: 'same', file_path: 'x.ts' } }));
expect(cb).not.toHaveBeenCalled();
});

it('(c) skips subagents (parentSessionId set)', () => {
const cb = vi.fn();
const hook = createEditPreviewHook({ addPreviewDiffRef: { current: cb } });
hook(makeCtx({ parentSessionId: 'parent-sess' }));
expect(cb).not.toHaveBeenCalled();
});

it('(d) skips non-edit_file tools', () => {
const cb = vi.fn();
const hook = createEditPreviewHook({ addPreviewDiffRef: { current: cb } });
hook(makeCtx({ toolName: 'bash' }));
expect(cb).not.toHaveBeenCalled();
});

it('(e) skips non-PreToolUse events', () => {
const cb = vi.fn();
const hook = createEditPreviewHook({ addPreviewDiffRef: { current: cb } });
// Cast to satisfy type — hook must guard event at runtime
hook({ event: 'PostToolUse', toolName: 'edit_file' } as any);
expect(cb).not.toHaveBeenCalled();
});

it('(f) returned decision has no injectContext', () => {
const cb = vi.fn();
const hook = createEditPreviewHook({ addPreviewDiffRef: { current: cb } });
const decision = hook(makeCtx());
expect(decision).not.toHaveProperty('injectContext');
});

it('(g) no-op when toolUseId absent', () => {
const cb = vi.fn();
const hook = createEditPreviewHook({ addPreviewDiffRef: { current: cb } });
hook(makeCtx({ toolUseId: undefined }));
expect(cb).not.toHaveBeenCalled();
});
});
Loading
Loading