diff --git a/scripts/hooks/ambient-prompt.sh b/scripts/hooks/ambient-prompt.sh index f8791d1..d3fc33e 100755 --- a/scripts/hooks/ambient-prompt.sh +++ b/scripts/hooks/ambient-prompt.sh @@ -5,7 +5,7 @@ # relevant skill loading via the ambient-router skill. # Zero file I/O beyond stdin — static injection only. -set -euo pipefail +set -e # jq is required to parse hook input JSON — silently no-op if missing if ! command -v jq &>/dev/null; then exit 0; fi diff --git a/scripts/hooks/background-memory-update.sh b/scripts/hooks/background-memory-update.sh index 15c9a36..0dd5877 100755 --- a/scripts/hooks/background-memory-update.sh +++ b/scripts/hooks/background-memory-update.sh @@ -5,7 +5,7 @@ # Resumes the parent session headlessly to update .memory/WORKING-MEMORY.md. # On failure: logs error, does nothing (no fallback). -set -euo pipefail +set -e CWD="$1" SESSION_ID="$2" diff --git a/scripts/hooks/pre-compact-memory.sh b/scripts/hooks/pre-compact-memory.sh index 7653b85..3d10980 100644 --- a/scripts/hooks/pre-compact-memory.sh +++ b/scripts/hooks/pre-compact-memory.sh @@ -6,7 +6,7 @@ # has something to inject after compaction. # PreCompact hooks cannot block compaction — this is informational only. -set -euo pipefail +set -e # jq is required to parse hook input JSON — silently no-op if missing if ! command -v jq &>/dev/null; then exit 0; fi diff --git a/scripts/hooks/run-hook b/scripts/hooks/run-hook new file mode 100755 index 0000000..054d5d4 --- /dev/null +++ b/scripts/hooks/run-hook @@ -0,0 +1,15 @@ +: << 'CMDBLOCK' +@echo off +setlocal enabledelayedexpansion +set "SCRIPT_DIR=%~dp0" +set "HOOK_NAME=%~1" +shift +where bash >nul 2>&1 && ( + bash "%SCRIPT_DIR%%HOOK_NAME%.sh" %* & exit /b !errorlevel! +) +exit /b 0 +CMDBLOCK +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +HOOK_NAME="$1" +shift +exec bash "$SCRIPT_DIR/${HOOK_NAME}.sh" "$@" diff --git a/scripts/hooks/session-start-memory.sh b/scripts/hooks/session-start-memory.sh index 50529ad..6bc09c7 100644 --- a/scripts/hooks/session-start-memory.sh +++ b/scripts/hooks/session-start-memory.sh @@ -5,7 +5,7 @@ # Also captures fresh git state so Claude knows what's changed since the memory was written. # Adds staleness warning if memory is >1 hour old. -set -euo pipefail +set -e # jq is required to parse hook input JSON — silently no-op if missing if ! command -v jq &>/dev/null; then exit 0; fi diff --git a/scripts/hooks/stop-update-memory.sh b/scripts/hooks/stop-update-memory.sh index d54298d..411a2be 100755 --- a/scripts/hooks/stop-update-memory.sh +++ b/scripts/hooks/stop-update-memory.sh @@ -5,7 +5,7 @@ # The session ends immediately — no visible edit in the TUI. # On failure: does nothing (stale memory is better than fake data). -set -euo pipefail +set -e # Break feedback loop: background updater's headless session triggers stop hook on exit. # DEVFLOW_BG_UPDATER is set by background-memory-update.sh before invoking claude. diff --git a/src/cli/commands/ambient.ts b/src/cli/commands/ambient.ts index 492ae27..b6153d4 100644 --- a/src/cli/commands/ambient.ts +++ b/src/cli/commands/ambient.ts @@ -23,7 +23,7 @@ interface Settings { [key: string]: unknown; } -const AMBIENT_HOOK_MARKER = 'ambient-prompt.sh'; +const AMBIENT_HOOK_MARKER = 'ambient-prompt'; /** * Add the ambient UserPromptSubmit hook to settings JSON. @@ -40,7 +40,7 @@ export function addAmbientHook(settingsJson: string, devflowDir: string): string settings.hooks = {}; } - const hookCommand = path.join(devflowDir, 'scripts', 'hooks', AMBIENT_HOOK_MARKER); + const hookCommand = path.join(devflowDir, 'scripts', 'hooks', 'run-hook') + ' ambient-prompt'; const newEntry: HookMatcher = { hooks: [ diff --git a/src/cli/commands/memory.ts b/src/cli/commands/memory.ts index b4c186d..d6e2e41 100644 --- a/src/cli/commands/memory.ts +++ b/src/cli/commands/memory.ts @@ -28,9 +28,9 @@ interface Settings { * Map of hook event type → filename marker for the 3 memory hooks. */ const MEMORY_HOOK_CONFIG: Record = { - Stop: 'stop-update-memory.sh', - SessionStart: 'session-start-memory.sh', - PreCompact: 'pre-compact-memory.sh', + Stop: 'stop-update-memory', + SessionStart: 'session-start-memory', + PreCompact: 'pre-compact-memory', }; /** @@ -57,7 +57,7 @@ export function addMemoryHooks(settingsJson: string, devflowDir: string): string ); if (!alreadyPresent) { - const hookCommand = path.join(devflowDir, 'scripts', 'hooks', marker); + const hookCommand = path.join(devflowDir, 'scripts', 'hooks', 'run-hook') + ` ${marker}`; const newEntry: HookMatcher = { hooks: [ { diff --git a/src/cli/utils/installer.ts b/src/cli/utils/installer.ts index f267330..ed10d42 100644 --- a/src/cli/utils/installer.ts +++ b/src/cli/utils/installer.ts @@ -224,7 +224,9 @@ export async function installViaFileCopy(options: FileCopyOptions): Promise { const settings = JSON.parse(result); expect(settings.hooks.UserPromptSubmit).toHaveLength(1); - expect(settings.hooks.UserPromptSubmit[0].hooks[0].command).toContain('ambient-prompt.sh'); + expect(settings.hooks.UserPromptSubmit[0].hooks[0].command).toContain('ambient-prompt'); expect(settings.hooks.UserPromptSubmit[0].hooks[0].timeout).toBe(5); }); @@ -35,7 +35,7 @@ describe('addAmbientHook', () => { expect(settings.hooks.UserPromptSubmit).toHaveLength(2); expect(settings.hooks.UserPromptSubmit[0].hooks[0].command).toBe('other-hook.sh'); - expect(settings.hooks.UserPromptSubmit[1].hooks[0].command).toContain('ambient-prompt.sh'); + expect(settings.hooks.UserPromptSubmit[1].hooks[0].command).toContain('ambient-prompt'); }); it('is idempotent — does not add duplicate hooks', () => { @@ -58,12 +58,13 @@ describe('addAmbientHook', () => { expect(settings.hooks.UserPromptSubmit).toHaveLength(1); }); - it('uses correct devflowDir path in command', () => { + it('uses correct devflowDir path in command via run-hook wrapper', () => { const result = addAmbientHook('{}', '/custom/path/.devflow'); const settings = JSON.parse(result); const command = settings.hooks.UserPromptSubmit[0].hooks[0].command; - expect(command).toContain('/custom/path/.devflow/scripts/hooks/ambient-prompt.sh'); + expect(command).toContain('/custom/path/.devflow/scripts/hooks/run-hook'); + expect(command).toContain('ambient-prompt'); }); }); @@ -81,7 +82,7 @@ describe('removeAmbientHook', () => { hooks: { UserPromptSubmit: [ { hooks: [{ type: 'command', command: 'other-hook.sh' }] }, - { hooks: [{ type: 'command', command: '/path/to/ambient-prompt.sh' }] }, + { hooks: [{ type: 'command', command: '/path/to/ambient-prompt' }] }, ], }, }); @@ -96,7 +97,7 @@ describe('removeAmbientHook', () => { const input = JSON.stringify({ hooks: { UserPromptSubmit: [ - { hooks: [{ type: 'command', command: '/path/to/ambient-prompt.sh' }] }, + { hooks: [{ type: 'command', command: '/path/to/ambient-prompt' }] }, ], }, }); @@ -111,7 +112,7 @@ describe('removeAmbientHook', () => { hooks: { Stop: [{ hooks: [{ type: 'command', command: 'stop.sh' }] }], UserPromptSubmit: [ - { hooks: [{ type: 'command', command: '/path/to/ambient-prompt.sh' }] }, + { hooks: [{ type: 'command', command: '/path/to/ambient-prompt' }] }, ], }, }); @@ -134,7 +135,7 @@ describe('removeAmbientHook', () => { statusLine: { type: 'command' }, hooks: { UserPromptSubmit: [ - { hooks: [{ type: 'command', command: '/path/to/ambient-prompt.sh' }] }, + { hooks: [{ type: 'command', command: '/path/to/ambient-prompt' }] }, ], }, }); @@ -171,7 +172,7 @@ describe('hasAmbientHook', () => { hooks: { UserPromptSubmit: [ { hooks: [{ type: 'command', command: 'other-hook.sh' }] }, - { hooks: [{ type: 'command', command: '/path/to/ambient-prompt.sh' }] }, + { hooks: [{ type: 'command', command: '/path/to/ambient-prompt' }] }, ], }, }); diff --git a/tests/memory.test.ts b/tests/memory.test.ts index d537eaa..76a8bea 100644 --- a/tests/memory.test.ts +++ b/tests/memory.test.ts @@ -13,9 +13,9 @@ describe('addMemoryHooks', () => { expect(settings.hooks.Stop).toHaveLength(1); expect(settings.hooks.SessionStart).toHaveLength(1); expect(settings.hooks.PreCompact).toHaveLength(1); - expect(settings.hooks.Stop[0].hooks[0].command).toContain('stop-update-memory.sh'); - expect(settings.hooks.SessionStart[0].hooks[0].command).toContain('session-start-memory.sh'); - expect(settings.hooks.PreCompact[0].hooks[0].command).toContain('pre-compact-memory.sh'); + expect(settings.hooks.Stop[0].hooks[0].command).toContain('stop-update-memory'); + expect(settings.hooks.SessionStart[0].hooks[0].command).toContain('session-start-memory'); + expect(settings.hooks.PreCompact[0].hooks[0].command).toContain('pre-compact-memory'); }); it('preserves existing hooks (UserPromptSubmit/ambient untouched)', () => { @@ -44,8 +44,8 @@ describe('addMemoryHooks', () => { it('adds only missing hooks when partial state (1 hook missing)', () => { const input = JSON.stringify({ hooks: { - Stop: [{ hooks: [{ type: 'command', command: '/path/stop-update-memory.sh', timeout: 10 }] }], - SessionStart: [{ hooks: [{ type: 'command', command: '/path/session-start-memory.sh', timeout: 10 }] }], + Stop: [{ hooks: [{ type: 'command', command: '/path/stop-update-memory', timeout: 10 }] }], + SessionStart: [{ hooks: [{ type: 'command', command: '/path/session-start-memory', timeout: 10 }] }], }, }); const result = addMemoryHooks(input, '/home/user/.devflow'); @@ -56,7 +56,7 @@ describe('addMemoryHooks', () => { expect(settings.hooks.SessionStart).toHaveLength(1); // Missing hook added expect(settings.hooks.PreCompact).toHaveLength(1); - expect(settings.hooks.PreCompact[0].hooks[0].command).toContain('pre-compact-memory.sh'); + expect(settings.hooks.PreCompact[0].hooks[0].command).toContain('pre-compact-memory'); }); it('creates hooks object if missing', () => { @@ -68,13 +68,16 @@ describe('addMemoryHooks', () => { expect(settings.hooks.Stop).toHaveLength(1); }); - it('uses correct devflowDir path in command', () => { + it('uses correct devflowDir path in command via run-hook wrapper', () => { const result = addMemoryHooks('{}', '/custom/path/.devflow'); const settings = JSON.parse(result); - expect(settings.hooks.Stop[0].hooks[0].command).toContain('/custom/path/.devflow/scripts/hooks/stop-update-memory.sh'); - expect(settings.hooks.SessionStart[0].hooks[0].command).toContain('/custom/path/.devflow/scripts/hooks/session-start-memory.sh'); - expect(settings.hooks.PreCompact[0].hooks[0].command).toContain('/custom/path/.devflow/scripts/hooks/pre-compact-memory.sh'); + expect(settings.hooks.Stop[0].hooks[0].command).toContain('/custom/path/.devflow/scripts/hooks/run-hook'); + expect(settings.hooks.Stop[0].hooks[0].command).toContain('stop-update-memory'); + expect(settings.hooks.SessionStart[0].hooks[0].command).toContain('run-hook'); + expect(settings.hooks.SessionStart[0].hooks[0].command).toContain('session-start-memory'); + expect(settings.hooks.PreCompact[0].hooks[0].command).toContain('run-hook'); + expect(settings.hooks.PreCompact[0].hooks[0].command).toContain('pre-compact-memory'); }); it('preserves other settings (statusLine, env)', () => { @@ -113,9 +116,9 @@ describe('removeMemoryHooks', () => { const input = JSON.stringify({ hooks: { UserPromptSubmit: [{ hooks: [{ type: 'command', command: 'ambient-prompt.sh' }] }], - Stop: [{ hooks: [{ type: 'command', command: '/path/stop-update-memory.sh' }] }], - SessionStart: [{ hooks: [{ type: 'command', command: '/path/session-start-memory.sh' }] }], - PreCompact: [{ hooks: [{ type: 'command', command: '/path/pre-compact-memory.sh' }] }], + Stop: [{ hooks: [{ type: 'command', command: '/path/stop-update-memory' }] }], + SessionStart: [{ hooks: [{ type: 'command', command: '/path/session-start-memory' }] }], + PreCompact: [{ hooks: [{ type: 'command', command: '/path/pre-compact-memory' }] }], }, }); const result = removeMemoryHooks(input); @@ -139,7 +142,7 @@ describe('removeMemoryHooks', () => { it('cleans empty hook type arrays', () => { const input = JSON.stringify({ hooks: { - Stop: [{ hooks: [{ type: 'command', command: '/path/stop-update-memory.sh' }] }], + Stop: [{ hooks: [{ type: 'command', command: '/path/stop-update-memory' }] }], }, }); const result = removeMemoryHooks(input); @@ -159,7 +162,7 @@ describe('removeMemoryHooks', () => { it('removes only the hooks that exist (partial)', () => { const input = JSON.stringify({ hooks: { - Stop: [{ hooks: [{ type: 'command', command: '/path/stop-update-memory.sh' }] }], + Stop: [{ hooks: [{ type: 'command', command: '/path/stop-update-memory' }] }], // SessionStart and PreCompact already missing }, }); @@ -173,9 +176,9 @@ describe('removeMemoryHooks', () => { const input = JSON.stringify({ statusLine: { type: 'command' }, hooks: { - Stop: [{ hooks: [{ type: 'command', command: '/path/stop-update-memory.sh' }] }], - SessionStart: [{ hooks: [{ type: 'command', command: '/path/session-start-memory.sh' }] }], - PreCompact: [{ hooks: [{ type: 'command', command: '/path/pre-compact-memory.sh' }] }], + Stop: [{ hooks: [{ type: 'command', command: '/path/stop-update-memory' }] }], + SessionStart: [{ hooks: [{ type: 'command', command: '/path/session-start-memory' }] }], + PreCompact: [{ hooks: [{ type: 'command', command: '/path/pre-compact-memory' }] }], }, }); const result = removeMemoryHooks(input); @@ -198,7 +201,7 @@ describe('hasMemoryHooks', () => { it('returns false when partial (1 or 2 of 3)', () => { const input = JSON.stringify({ hooks: { - Stop: [{ hooks: [{ type: 'command', command: '/path/stop-update-memory.sh' }] }], + Stop: [{ hooks: [{ type: 'command', command: '/path/stop-update-memory' }] }], }, }); expect(hasMemoryHooks(input)).toBe(false); @@ -227,8 +230,8 @@ describe('countMemoryHooks', () => { it('returns correct partial count', () => { const input = JSON.stringify({ hooks: { - Stop: [{ hooks: [{ type: 'command', command: '/path/stop-update-memory.sh' }] }], - SessionStart: [{ hooks: [{ type: 'command', command: '/path/session-start-memory.sh' }] }], + Stop: [{ hooks: [{ type: 'command', command: '/path/stop-update-memory' }] }], + SessionStart: [{ hooks: [{ type: 'command', command: '/path/session-start-memory' }] }], }, }); expect(countMemoryHooks(input)).toBe(2);