@@ -740,6 +740,9 @@ pub struct PromptProfile {
740740 /// i.e. it is an owned agent rather than a human. Used to gate reply-anchor
741741 /// flattening (UX routing heuristic, not a security boundary).
742742 pub is_agent : bool ,
743+ /// Git email from the NIP-01 kind:0 `email` field. Injected into `[Context]`
744+ /// as `Requester-Git-Email:` so agents can use it for commit trailers.
745+ pub git_email : Option < String > ,
743746}
744747
745748/// Pubkey-keyed profile lookup used while formatting ACP prompts.
@@ -966,12 +969,30 @@ fn format_context_hints(
966969 is_dm : bool ,
967970 has_conversation_context : bool ,
968971 reply_anchor : Option < & str > ,
972+ sender_pubkey : & str ,
973+ profile_lookup : Option < & PromptProfileLookup > ,
969974) -> String {
970975 let channel_display = match channel_info {
971976 Some ( ci) => format ! ( "{} (#{channel_id})" , ci. name) ,
972977 None => channel_id. to_string ( ) ,
973978 } ;
974979
980+ // Emit `Requester-Git-Email: Name <email>` when the triggering event's author
981+ // has a git_email set in their profile. Agents use this for commit trailers
982+ // instead of falling back to `git config user.email`.
983+ let requester_git_email_line = profile_lookup
984+ . and_then ( |lookup| lookup. get ( & normalize_lookup_key ( sender_pubkey) ) )
985+ . and_then ( |profile| {
986+ let email = profile. git_email . as_deref ( ) ?;
987+ let name = profile
988+ . display_name
989+ . as_deref ( )
990+ . or ( profile. nip05_handle . as_deref ( ) )
991+ . unwrap_or ( "Unknown" ) ;
992+ Some ( format ! ( "\n Requester-Git-Email: {name} <{email}>" ) )
993+ } )
994+ . unwrap_or_default ( ) ;
995+
975996 // DM check comes first — a DM reply has both thread tags AND is_dm=true,
976997 // and the scope should be "dm" (not "thread") because the agent is in a DM.
977998 if is_dm {
@@ -1005,6 +1026,7 @@ fn format_context_hints(
10051026 append_reply_instruction ( & mut s, event_id) ;
10061027 }
10071028 }
1029+ s. push_str ( & requester_git_email_line) ;
10081030 s
10091031 } else if let Some ( ref root) = thread_tags. root_event_id {
10101032 let ctx_hint = if has_conversation_context {
@@ -1027,6 +1049,7 @@ fn format_context_hints(
10271049 if let Some ( event_id) = reply_anchor {
10281050 append_reply_instruction ( & mut s, event_id) ;
10291051 }
1052+ s. push_str ( & requester_git_email_line) ;
10301053 s
10311054 } else {
10321055 let mut s = format ! (
@@ -1038,6 +1061,7 @@ fn format_context_hints(
10381061 if let Some ( event_id) = reply_anchor {
10391062 append_new_thread_reply_instruction ( & mut s, event_id) ;
10401063 }
1064+ s. push_str ( & requester_git_email_line) ;
10411065 s
10421066 }
10431067}
@@ -1194,6 +1218,8 @@ pub fn format_prompt(batch: &FlushBatch, args: &FormatPromptArgs<'_>) -> Vec<Str
11941218 is_dm,
11951219 args. conversation_context . is_some ( ) ,
11961220 reply_anchor. as_deref ( ) ,
1221+ & sender_pubkey,
1222+ args. profile_lookup ,
11971223 ) ) ;
11981224
11991225 // 3. Conversation context (thread or DM).
@@ -3506,4 +3532,97 @@ mod tests {
35063532 None
35073533 ) ;
35083534 }
3535+
3536+ // ── Requester-Git-Email injection ────────────────────────────────────────
3537+
3538+ /// When the triggering event's author has a git_email in their profile,
3539+ /// `[Context]` should include a `Requester-Git-Email:` line.
3540+ #[ test]
3541+ fn test_format_context_hints_emits_requester_git_email_when_present ( ) {
3542+ let batch = make_single_batch ( "hello" ) ;
3543+ let sender_hex = batch. events [ 0 ] . event . pubkey . to_hex ( ) ;
3544+ let profiles = HashMap :: from ( [ (
3545+ sender_hex. clone ( ) ,
3546+ PromptProfile {
3547+ display_name : Some ( "Will Pfleger" . into ( ) ) ,
3548+ git_email : Some ( "will@example.com" . into ( ) ) ,
3549+ ..Default :: default ( )
3550+ } ,
3551+ ) ] ) ;
3552+
3553+ let prompt = format_prompt (
3554+ & batch,
3555+ & FormatPromptArgs {
3556+ profile_lookup : Some ( & profiles) ,
3557+ ..Default :: default ( )
3558+ } ,
3559+ )
3560+ . join ( "\n \n " ) ;
3561+
3562+ assert ! (
3563+ prompt. contains( "Requester-Git-Email: Will Pfleger <will@example.com>" ) ,
3564+ "context block should include Requester-Git-Email when profile has git_email"
3565+ ) ;
3566+ }
3567+
3568+ /// When the triggering event's author has no git_email, `[Context]` must
3569+ /// NOT include a `Requester-Git-Email:` line.
3570+ #[ test]
3571+ fn test_format_context_hints_omits_requester_git_email_when_absent ( ) {
3572+ let batch = make_single_batch ( "hello" ) ;
3573+ let sender_hex = batch. events [ 0 ] . event . pubkey . to_hex ( ) ;
3574+ let profiles = HashMap :: from ( [ (
3575+ sender_hex. clone ( ) ,
3576+ PromptProfile {
3577+ display_name : Some ( "Will Pfleger" . into ( ) ) ,
3578+ git_email : None ,
3579+ ..Default :: default ( )
3580+ } ,
3581+ ) ] ) ;
3582+
3583+ let prompt = format_prompt (
3584+ & batch,
3585+ & FormatPromptArgs {
3586+ profile_lookup : Some ( & profiles) ,
3587+ ..Default :: default ( )
3588+ } ,
3589+ )
3590+ . join ( "\n \n " ) ;
3591+
3592+ assert ! (
3593+ !prompt. contains( "Requester-Git-Email" ) ,
3594+ "context block should not include Requester-Git-Email when git_email is absent"
3595+ ) ;
3596+ }
3597+
3598+ /// Falls back to display_name when constructing the Requester-Git-Email line.
3599+ /// When display_name is absent, nip05_handle is used as the name part.
3600+ #[ test]
3601+ fn test_format_context_hints_requester_git_email_uses_nip05_as_name_fallback ( ) {
3602+ let batch = make_single_batch ( "hello" ) ;
3603+ let sender_hex = batch. events [ 0 ] . event . pubkey . to_hex ( ) ;
3604+ let profiles = HashMap :: from ( [ (
3605+ sender_hex. clone ( ) ,
3606+ PromptProfile {
3607+ display_name : None ,
3608+ nip05_handle : Some ( "will@relay.example.com" . into ( ) ) ,
3609+ git_email : Some ( "will@example.com" . into ( ) ) ,
3610+ ..Default :: default ( )
3611+ } ,
3612+ ) ] ) ;
3613+
3614+ let prompt = format_prompt (
3615+ & batch,
3616+ & FormatPromptArgs {
3617+ profile_lookup : Some ( & profiles) ,
3618+ ..Default :: default ( )
3619+ } ,
3620+ )
3621+ . join ( "\n \n " ) ;
3622+
3623+ assert ! (
3624+ prompt. contains( "Requester-Git-Email: will@relay.example.com <will@example.com>" ) ,
3625+ "name part should fall back to nip05_handle when display_name is absent"
3626+ ) ;
3627+ }
35093628}
0 commit comments