From 808be17185ddf932a343a865890b5ecc335865e0 Mon Sep 17 00:00:00 2001 From: Tony Casey Date: Sun, 15 Feb 2026 23:03:46 +0000 Subject: [PATCH 1/9] chore: smoke test codex trailers AI-Agent: OpenAI-Codex AI-Model: gpt-5-codex From 9369b9168bd7c82dea94859dcf0d2db8ec6800fb Mon Sep 17 00:00:00 2001 From: Tony Casey Date: Sun, 15 Feb 2026 23:05:03 +0000 Subject: [PATCH 2/9] feat(auth): decide to use JWT over sessions for stateless API decision: JWT keeps the API stateless and scales horizontally. AI-Agent: OpenAI-Codex AI-Model: gpt-5-codex From 1b0284d84e6beff92c48f36eff6d9bc5f2826f85 Mon Sep 17 00:00:00 2001 From: Tony Casey Date: Sun, 15 Feb 2026 23:05:15 +0000 Subject: [PATCH 3/9] fix: debug commit-msg hook AI-Agent: OpenAI-Codex AI-Model: gpt-5-codex From 5cb9b63088ce1933e15e672cde4c61ee7e38e7d2 Mon Sep 17 00:00:00 2001 From: Tony Casey Date: Sun, 15 Feb 2026 23:06:31 +0000 Subject: [PATCH 4/9] chore: hook invocation probe AI-Agent: OpenAI-Codex AI-Model: gpt-5-codex From bc5d407e76891f64fc613bbd8b96ea6bff3108a3 Mon Sep 17 00:00:00 2001 From: Tony Casey Date: Sun, 15 Feb 2026 23:07:16 +0000 Subject: [PATCH 5/9] chore: probe shared commit-msg hook 2 AI-Agent: OpenAI-Codex AI-Model: gpt-5-codex From b56e0907714de5c794d96ee82237ae36930d6459 Mon Sep 17 00:00:00 2001 From: Tony Casey Date: Sun, 15 Feb 2026 23:08:11 +0000 Subject: [PATCH 6/9] feat(mem): decision to prefer local CLI in hooks decision: local CLI avoids global version drift during repo development. AI-Agent: OpenAI-Codex AI-Model: gpt-5-codex From 07a1aff38699e6febe0afb519a4c44cc0b81b315 Mon Sep 17 00:00:00 2001 From: Tony Casey Date: Sun, 15 Feb 2026 23:09:16 +0000 Subject: [PATCH 7/9] feat(hooks): verify full AI trailers decision: removing msys cwd from hook payload lets analyzer run. AI-Agent: OpenAI-Codex AI-Model: gpt-5-codex AI-Decision: verify full AI trailers. decision: removing msys cwd from hook payload lets analyzer run. AI-Confidence: medium AI-Tags: hooks AI-Lifecycle: project AI-Memory-Id: ab376da3 AI-Source: heuristic From 588aad9d7b1929527409aa8dbba7d8fbd710ba58 Mon Sep 17 00:00:00 2001 From: Tony Casey Date: Sun, 15 Feb 2026 23:11:58 +0000 Subject: [PATCH 8/9] fix(hooks): avoid passing cwd in commit-msg hook payload decision: omit cwd from hook JSON because git hook shells on Windows can provide MSYS-style paths (/c/...) that break execFileSync git cwd resolution. AI-Agent: OpenAI-Codex AI-Model: gpt-5-codex AI-Decision: git hook shells on Windows can provide MSYS-style paths (/c/ AI-Confidence: medium AI-Tags: hooks, tests, unit, pattern:because-clause, pattern:avoid AI-Lifecycle: project AI-Memory-Id: cfb0615c AI-Source: heuristic --- src/hooks/commit-msg.ts | 7 +++---- tests/unit/hooks/commit-msg.test.ts | 7 ++++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/hooks/commit-msg.ts b/src/hooks/commit-msg.ts index 6ca86942..dd6928b4 100644 --- a/src/hooks/commit-msg.ts +++ b/src/hooks/commit-msg.ts @@ -17,7 +17,7 @@ import { execFileSync } from 'child_process'; const HOOK_FINGERPRINT_PREFIX = '# git-mem:commit-msg'; /** Full fingerprint with version — used for upgrade detection. */ -const HOOK_FINGERPRINT = `${HOOK_FINGERPRINT_PREFIX} v4`; +const HOOK_FINGERPRINT = `${HOOK_FINGERPRINT_PREFIX} v5`; /** * The shell hook script. @@ -44,13 +44,12 @@ head -1 "$COMMIT_MSG_FILE" | grep -qiE "^(fixup|squash|amend)! " && exit 0 # Skip revert commits (auto-generated) head -1 "$COMMIT_MSG_FILE" | grep -qiE '^Revert "' && exit 0 -# Escape values for safe JSON inclusion (handles quotes, backslashes) +# Escape value for safe JSON inclusion (handles quotes, backslashes) COMMIT_MSG_FILE_ESC=$(printf '%s' "$COMMIT_MSG_FILE" | sed 's/\\\\/\\\\\\\\/g; s/"/\\\\"/g') -CWD_ESC=$(pwd | sed 's/\\\\/\\\\\\\\/g; s/"/\\\\"/g') # Run git-mem commit-msg analyzer # Pass commit message file path via JSON stdin -echo "{\\"commit_msg_path\\": \\"$COMMIT_MSG_FILE_ESC\\", \\"cwd\\": \\"$CWD_ESC\\"}" | \\ +echo "{\\"commit_msg_path\\": \\"$COMMIT_MSG_FILE_ESC\\"}" | \\ git-mem hook commit-msg 2>/dev/null || true exit 0 diff --git a/tests/unit/hooks/commit-msg.test.ts b/tests/unit/hooks/commit-msg.test.ts index b9bda7c1..ce4f6475 100644 --- a/tests/unit/hooks/commit-msg.test.ts +++ b/tests/unit/hooks/commit-msg.test.ts @@ -39,8 +39,9 @@ describe('installCommitMsgHook', () => { const content = readFileSync(result.hookPath, 'utf8'); assert.ok(content.includes('#!/bin/sh')); - assert.ok(content.includes('git-mem:commit-msg v4')); + assert.ok(content.includes('git-mem:commit-msg v5')); assert.ok(content.includes('git-mem hook commit-msg')); + assert.ok(!content.includes('"cwd"')); }); it('should be idempotent — second install is a no-op', () => { @@ -74,7 +75,7 @@ describe('installCommitMsgHook', () => { // Installed hook should contain both fingerprint and wrapper reference const content = readFileSync(hookPath, 'utf8'); - assert.ok(content.includes('git-mem:commit-msg v4')); + assert.ok(content.includes('git-mem:commit-msg v5')); assert.ok(content.includes('user-backup')); } finally { rmSync(freshRepo, { recursive: true, force: true }); @@ -102,7 +103,7 @@ describe('installCommitMsgHook', () => { assert.equal(result.wrapped, false); const content = readFileSync(hookPath, 'utf8'); - assert.ok(content.includes('git-mem:commit-msg v4'), 'Should be upgraded to v4'); + assert.ok(content.includes('git-mem:commit-msg v5'), 'Should be upgraded to v5'); assert.ok(content.includes('git-mem hook commit-msg'), 'Should include git-mem command'); // Second install should be idempotent From e53be8d0fd27e5bc150f4f94436b6cc789578cb6 Mon Sep 17 00:00:00 2001 From: Tony Casey Date: Sun, 15 Feb 2026 23:24:09 +0000 Subject: [PATCH 9/9] fix(hooks): pass repo cwd and normalize msys paths decision: include explicit repo root cwd in commit-msg hook payload for stable config/git context, while normalizing /c/... paths on Windows to avoid spawnSync git ENOENT. AI-Agent: OpenAI-Codex AI-Model: gpt-5-codex AI-Convention: spawnSync git ENOENT AI-Confidence: low AI-Tags: hooks, commands, tests, unit, pattern:avoid AI-Lifecycle: project AI-Memory-Id: 1dc9b177 AI-Source: heuristic --- src/commands/hook.ts | 21 +++++++++++++++++++-- src/hooks/commit-msg.ts | 10 +++++++--- tests/unit/commands/hook.test.ts | 26 +++++++++++++++++++++++++- tests/unit/hooks/commit-msg.test.ts | 9 +++++---- 4 files changed, 56 insertions(+), 10 deletions(-) diff --git a/src/commands/hook.ts b/src/commands/hook.ts index 9f28ed2c..a3ec3e93 100644 --- a/src/commands/hook.ts +++ b/src/commands/hook.ts @@ -111,6 +111,22 @@ function findGitRoot(cwd: string): string { return resolveGitRoot(cwd); } +/** + * Normalize hook cwd values across shell environments. + * On Windows Git Bash/MSYS may pass "/c/path" which Node cannot use as cwd. + */ +export function normalizeHookCwd(cwd: string, platform: NodeJS.Platform = process.platform): string { + if (platform !== 'win32') return cwd; + + const msysDrivePath = /^\/([a-zA-Z])(?:\/(.*))?$/; + const match = cwd.match(msysDrivePath); + if (!match) return cwd; + + const drive = match[1].toUpperCase(); + const rest = match[2] ?? ''; + return rest.length > 0 ? `${drive}:/${rest}` : `${drive}:/`; +} + /** Stderr labels per event for user-facing messages. */ const STDERR_LABELS: Record = { 'session:start': { success: 'Memory loaded.', prefix: 'Memory loaded' }, @@ -131,7 +147,8 @@ export async function hookCommand(eventName: string, _logger?: ILogger): Promise try { const input = await readStdin(); - const cwd = input.cwd ?? process.cwd(); + const cwd = normalizeHookCwd(input.cwd ?? process.cwd()); + const normalizedInput: IHookInput = { ...input, cwd }; const repoRoot = findGitRoot(cwd); // Load .env from repository root for API keys (e.g., ANTHROPIC_API_KEY) @@ -147,7 +164,7 @@ export async function hookCommand(eventName: string, _logger?: ILogger): Promise const container = createContainer({ scope: `hook:${eventName}` }); const { eventBus } = container.cradle; - const event = buildEvent(eventType, input); + const event = buildEvent(eventType, normalizedInput); const results = await eventBus.emit(event); // Successful handler output → stdout (Claude context) diff --git a/src/hooks/commit-msg.ts b/src/hooks/commit-msg.ts index dd6928b4..1fc38d56 100644 --- a/src/hooks/commit-msg.ts +++ b/src/hooks/commit-msg.ts @@ -17,7 +17,7 @@ import { execFileSync } from 'child_process'; const HOOK_FINGERPRINT_PREFIX = '# git-mem:commit-msg'; /** Full fingerprint with version — used for upgrade detection. */ -const HOOK_FINGERPRINT = `${HOOK_FINGERPRINT_PREFIX} v5`; +const HOOK_FINGERPRINT = `${HOOK_FINGERPRINT_PREFIX} v6`; /** * The shell hook script. @@ -44,12 +44,16 @@ head -1 "$COMMIT_MSG_FILE" | grep -qiE "^(fixup|squash|amend)! " && exit 0 # Skip revert commits (auto-generated) head -1 "$COMMIT_MSG_FILE" | grep -qiE '^Revert "' && exit 0 -# Escape value for safe JSON inclusion (handles quotes, backslashes) +# Resolve repository root for hook context. +REPO_ROOT=$(git rev-parse --show-toplevel 2>/dev/null || pwd) + +# Escape values for safe JSON inclusion (handles quotes, backslashes) COMMIT_MSG_FILE_ESC=$(printf '%s' "$COMMIT_MSG_FILE" | sed 's/\\\\/\\\\\\\\/g; s/"/\\\\"/g') +REPO_ROOT_ESC=$(printf '%s' "$REPO_ROOT" | sed 's/\\\\/\\\\\\\\/g; s/"/\\\\"/g') # Run git-mem commit-msg analyzer # Pass commit message file path via JSON stdin -echo "{\\"commit_msg_path\\": \\"$COMMIT_MSG_FILE_ESC\\"}" | \\ +echo "{\\"commit_msg_path\\": \\"$COMMIT_MSG_FILE_ESC\\", \\"cwd\\": \\"$REPO_ROOT_ESC\\"}" | \\ git-mem hook commit-msg 2>/dev/null || true exit 0 diff --git a/tests/unit/commands/hook.test.ts b/tests/unit/commands/hook.test.ts index 0e63b344..b5459ebd 100644 --- a/tests/unit/commands/hook.test.ts +++ b/tests/unit/commands/hook.test.ts @@ -8,7 +8,7 @@ import { describe, it } from 'node:test'; import assert from 'node:assert/strict'; -import { EVENT_MAP, isEventEnabled, buildEvent } from '../../../src/commands/hook'; +import { EVENT_MAP, isEventEnabled, buildEvent, normalizeHookCwd } from '../../../src/commands/hook'; import type { IHooksConfig } from '../../../src/domain/interfaces/IHookConfig'; // ── Fixture helpers ────────────────────────────────────────────────── @@ -146,3 +146,27 @@ describe('buildEvent', () => { assert.equal('prompt' in event && event.prompt, ''); }); }); + +// ── normalizeHookCwd ──────────────────────────────────────────────── + +describe('normalizeHookCwd', () => { + it('should normalize MSYS drive path on win32', () => { + const normalized = normalizeHookCwd('/c/dev/git-mem', 'win32'); + assert.equal(normalized, 'C:/dev/git-mem'); + }); + + it('should normalize MSYS drive root on win32', () => { + const normalized = normalizeHookCwd('/d', 'win32'); + assert.equal(normalized, 'D:/'); + }); + + it('should leave non-MSYS path unchanged on win32', () => { + const normalized = normalizeHookCwd('C:/dev/git-mem', 'win32'); + assert.equal(normalized, 'C:/dev/git-mem'); + }); + + it('should leave path unchanged on non-win32', () => { + const normalized = normalizeHookCwd('/c/dev/git-mem', 'linux'); + assert.equal(normalized, '/c/dev/git-mem'); + }); +}); diff --git a/tests/unit/hooks/commit-msg.test.ts b/tests/unit/hooks/commit-msg.test.ts index ce4f6475..1732e03e 100644 --- a/tests/unit/hooks/commit-msg.test.ts +++ b/tests/unit/hooks/commit-msg.test.ts @@ -39,9 +39,10 @@ describe('installCommitMsgHook', () => { const content = readFileSync(result.hookPath, 'utf8'); assert.ok(content.includes('#!/bin/sh')); - assert.ok(content.includes('git-mem:commit-msg v5')); + assert.ok(content.includes('git-mem:commit-msg v6')); assert.ok(content.includes('git-mem hook commit-msg')); - assert.ok(!content.includes('"cwd"')); + assert.ok(content.includes('\\"cwd\\"')); + assert.ok(content.includes('git rev-parse --show-toplevel')); }); it('should be idempotent — second install is a no-op', () => { @@ -75,7 +76,7 @@ describe('installCommitMsgHook', () => { // Installed hook should contain both fingerprint and wrapper reference const content = readFileSync(hookPath, 'utf8'); - assert.ok(content.includes('git-mem:commit-msg v5')); + assert.ok(content.includes('git-mem:commit-msg v6')); assert.ok(content.includes('user-backup')); } finally { rmSync(freshRepo, { recursive: true, force: true }); @@ -103,7 +104,7 @@ describe('installCommitMsgHook', () => { assert.equal(result.wrapped, false); const content = readFileSync(hookPath, 'utf8'); - assert.ok(content.includes('git-mem:commit-msg v5'), 'Should be upgraded to v5'); + assert.ok(content.includes('git-mem:commit-msg v6'), 'Should be upgraded to v6'); assert.ok(content.includes('git-mem hook commit-msg'), 'Should include git-mem command'); // Second install should be idempotent