-
Notifications
You must be signed in to change notification settings - Fork 0
feat: add post-commit hook for automatic session notes (GIT-74) #49
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
a1fb40b
7266200
37bb176
20c6cab
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,121 @@ | ||
| /** | ||
| * PostCommitHandler | ||
| * | ||
| * Handles the git:commit event by writing session metadata as a git note. | ||
| * Preserves any existing memories on the commit while adding/updating | ||
| * the session field with agent, model, and timestamp. | ||
| */ | ||
|
|
||
| import type { IPostCommitHandler } from '../interfaces/IPostCommitHandler'; | ||
| import type { IGitCommitEvent } from '../../domain/events/HookEvents'; | ||
| import type { IEventResult } from '../../domain/interfaces/IEventResult'; | ||
| import type { INotesService } from '../../domain/interfaces/INotesService'; | ||
| import type { ILogger } from '../../domain/interfaces/ILogger'; | ||
| import { resolveAgent, resolveModel } from '../../infrastructure/detect-agent'; | ||
|
|
||
| /** | ||
| * Session metadata stored in the note's `session` field. | ||
| */ | ||
| interface ISessionMetadata { | ||
| agent: string; | ||
| model?: string; | ||
| timestamp: string; | ||
| } | ||
|
|
||
| /** | ||
| * Note payload structure. | ||
| * The `memories` array is managed by MemoryRepository. | ||
| * The `session` field is managed by this handler. | ||
| */ | ||
| interface INotePayload { | ||
| memories?: unknown[]; | ||
| session?: ISessionMetadata; | ||
| } | ||
|
|
||
| export class PostCommitHandler implements IPostCommitHandler { | ||
| constructor( | ||
| private readonly notesService: INotesService, | ||
| private readonly logger?: ILogger, | ||
| ) {} | ||
|
|
||
| async handle(event: IGitCommitEvent): Promise<IEventResult> { | ||
| try { | ||
| const agent = resolveAgent(); | ||
| const model = resolveModel(); | ||
|
|
||
| // No agent detected — exit silently (not an AI-assisted commit) | ||
| if (!agent) { | ||
| this.logger?.debug('No AI agent detected, skipping session note'); | ||
| return { | ||
| handler: 'PostCommitHandler', | ||
| success: true, | ||
| }; | ||
| } | ||
|
|
||
| this.logger?.info('Post-commit handler invoked', { | ||
| sha: event.sha, | ||
| agent, | ||
| model, | ||
| }); | ||
|
|
||
| // Read existing note payload | ||
| const payload = this.readPayload(event.sha, event.cwd); | ||
|
|
||
| // Add/update session metadata | ||
| payload.session = { | ||
| agent, | ||
| model, | ||
| timestamp: new Date().toISOString(), | ||
| }; | ||
|
|
||
| // Write back | ||
| this.writePayload(event.sha, payload, event.cwd); | ||
|
|
||
| this.logger?.info('Session note written', { | ||
| sha: event.sha, | ||
| hasMemories: Array.isArray(payload.memories) && payload.memories.length > 0, | ||
| }); | ||
|
|
||
| return { | ||
| handler: 'PostCommitHandler', | ||
| success: true, | ||
| }; | ||
| } catch (error) { | ||
| this.logger?.error('Post-commit handler failed', { error }); | ||
| return { | ||
| handler: 'PostCommitHandler', | ||
| success: false, | ||
| error: error instanceof Error ? error : new Error(String(error)), | ||
| }; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Read and parse the existing note payload. | ||
| * Returns empty object if no note exists or parsing fails. | ||
| */ | ||
| private readPayload(sha: string, cwd: string): INotePayload { | ||
| const raw = this.notesService.read(sha, undefined, cwd); | ||
| if (!raw) return {}; | ||
|
|
||
| try { | ||
| const parsed = JSON.parse(raw); | ||
| if (typeof parsed === 'object' && parsed !== null) { | ||
| return parsed as INotePayload; | ||
| } | ||
| return {}; | ||
| } catch { | ||
| // Malformed JSON — return empty and let write overwrite | ||
| this.logger?.warn('Malformed note JSON, will overwrite', { sha }); | ||
| return {}; | ||
|
Comment on lines
+97
to
+110
|
||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Serialize and write the payload to the note. | ||
| */ | ||
| private writePayload(sha: string, payload: INotePayload, cwd: string): void { | ||
| const json = JSON.stringify(payload, null, 2); | ||
| this.notesService.write(sha, json, undefined, cwd); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| /** | ||
| * IPostCommitHandler | ||
| * | ||
| * Application interface for the post-commit hook handler. | ||
| */ | ||
|
|
||
| import type { IEventHandler } from '../../domain/interfaces/IEventHandler'; | ||
| import type { IGitCommitEvent } from '../../domain/events/HookEvents'; | ||
|
|
||
| export type IPostCommitHandler = IEventHandler<IGitCommitEvent>; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -24,21 +24,24 @@ export interface IHookInput { | |
| prompt?: string; | ||
| cwd?: string; | ||
| hook_event_name?: string; | ||
| sha?: string; | ||
| } | ||
|
|
||
| /** Map CLI event names to internal event bus types. */ | ||
| export const EVENT_MAP: Record<string, HookEventType> = { | ||
| 'session-start': 'session:start', | ||
| 'session-stop': 'session:stop', | ||
| 'prompt-submit': 'prompt:submit', | ||
| 'post-commit': 'git:commit', | ||
| }; | ||
|
Comment on lines
30
to
36
|
||
|
|
||
| /** Map CLI event names to the config section that controls them. */ | ||
| type ConfigKey = 'sessionStart' | 'sessionStop' | 'promptSubmit'; | ||
| type ConfigKey = 'sessionStart' | 'sessionStop' | 'promptSubmit' | 'postCommit'; | ||
| const CONFIG_KEY_MAP: Record<string, ConfigKey> = { | ||
| 'session-start': 'sessionStart', | ||
| 'session-stop': 'sessionStop', | ||
| 'prompt-submit': 'promptSubmit', | ||
| 'post-commit': 'postCommit', | ||
| }; | ||
|
|
||
| /** Extra enabled check for session-stop (must also have autoExtract). */ | ||
|
|
@@ -73,6 +76,8 @@ export function buildEvent(eventType: HookEventType, input: IHookInput): HookEve | |
| return { type: 'session:stop', ...base }; | ||
| case 'prompt:submit': | ||
| return { type: 'prompt:submit', ...base, prompt: input.prompt ?? '' }; | ||
| case 'git:commit': | ||
| return { type: 'git:commit', sha: input.sha ?? 'HEAD', cwd: base.cwd }; | ||
| default: { | ||
| const exhaustiveCheck: never = eventType; | ||
| throw new Error(`Unhandled HookEventType in buildEvent: ${exhaustiveCheck as string}`); | ||
|
|
@@ -85,6 +90,7 @@ const STDERR_LABELS: Record<HookEventType, { success: string; prefix: string }> | |
| 'session:start': { success: 'Memory loaded.', prefix: 'Memory loaded' }, | ||
| 'session:stop': { success: 'Session capture complete.', prefix: 'Session capture complete' }, | ||
| 'prompt:submit': { success: 'Prompt context loaded.', prefix: 'Prompt context loaded' }, | ||
| 'git:commit': { success: 'Session note written.', prefix: 'Session note' }, | ||
| }; | ||
|
|
||
| export async function hookCommand(eventName: string, _logger?: ILogger): Promise<void> { | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion | 🟠 Major
Decouple application layer from infrastructure utilities.
PostCommitHandlerimportsresolveAgent/resolveModelfromsrc/infrastructure, which breaks the application→domain-only dependency rule. Please inject an application-friendly abstraction (e.g.,IAgentResolverin domain) and bind it in the DI container, or relocate agent detection to a domain/application utility.As per coding guidelines, "src/{application,infrastructure}/**/*.ts: Implement domain interfaces in src/infrastructure/ and depend on domain layer only from src/application/."
🤖 Prompt for AI Agents
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Acknowledged. For this PR, keeping the direct import since
resolveAgent/resolveModelare pure functions with no external dependencies (just process.env reads). Creating an abstraction layer would add complexity without clear benefit for these simple utilities. Will consider for future refactoring if this pattern spreads.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@TonyCasey, understood! That's a reasonable pragmatic call for these simple environment readers. If they grow dependencies or need different behavior in tests down the line, the abstraction can be introduced then. Thanks for considering it for future refactoring if the pattern expands. 👍🧠 Learnings used