diff --git a/src/agents/core/BaseAgentAdapter.ts b/src/agents/core/BaseAgentAdapter.ts index 59ac3ec3..773910eb 100644 --- a/src/agents/core/BaseAgentAdapter.ts +++ b/src/agents/core/BaseAgentAdapter.ts @@ -1,7 +1,7 @@ import { AgentMetadata, AgentAdapter, AgentConfig, MCPConfigSummary, ExtensionsScanSummary, VersionCompatibilityResult } from './types.js'; import * as npm from '../../utils/processes.js'; import { NpmError, createErrorContext } from '../../utils/errors.js'; -import { exec, detectGitBranch } from '../../utils/processes.js'; +import { exec, detectGitBranch, detectGitRemoteRepo } from '../../utils/processes.js'; import { compareVersions } from '../../utils/version-utils.js'; import { logger } from '../../utils/logger.js'; import { spawn } from 'child_process'; @@ -461,12 +461,19 @@ export abstract class BaseAgentAdapter implements AgentAdapter { // Detect repository and branch once at session start so all downstream // components (proxy config, metrics sender, etc.) can reuse without re-computing const workingDir = process.cwd(); - const sessionBranch = await detectGitBranch(workingDir); const repoParts = workingDir.split(/[/\\]/).filter((p: string) => p.length > 0); - const sessionRepository = repoParts.length >= 2 + const filesystemRepository = repoParts.length >= 2 ? `${repoParts[repoParts.length - 2]}/${repoParts[repoParts.length - 1]}` : repoParts[repoParts.length - 1] || 'unknown'; + const [sessionBranch, remoteRepository] = await Promise.all([ + detectGitBranch(workingDir), + detectGitRemoteRepo(workingDir), + ]); + + // Use canonical owner/repo from git remote as primary; fall back to filesystem path + const sessionRepository = remoteRepository ?? filesystemRepository; + // Merge environment variables let env: NodeJS.ProcessEnv = { ...process.env, diff --git a/src/agents/core/session/ensure-session.ts b/src/agents/core/session/ensure-session.ts index 1d79949d..e849ff9f 100644 --- a/src/agents/core/session/ensure-session.ts +++ b/src/agents/core/session/ensure-session.ts @@ -29,9 +29,13 @@ export async function ensureSessionFile( const workingDirectory = process.cwd(); let gitBranch: string | undefined; + let remoteRepository: string | undefined; try { - const { detectGitBranch } = await import('../../../utils/processes.js'); - gitBranch = await detectGitBranch(workingDirectory); + const { detectGitBranch, detectGitRemoteRepo } = await import('../../../utils/processes.js'); + [gitBranch, remoteRepository] = await Promise.all([ + detectGitBranch(workingDirectory), + detectGitRemoteRepo(workingDirectory), + ]); } catch { // Git detection optional } @@ -47,6 +51,7 @@ export async function ensureSessionFile( ...(project && { project }), startTime: estimatedStartTime, workingDirectory, + ...(remoteRepository && { repository: remoteRepository }), ...(gitBranch && { gitBranch }), status: 'completed' as const, activeDurationMs: 0, diff --git a/src/agents/core/session/types.ts b/src/agents/core/session/types.ts index 4fc525c3..7226d45d 100644 --- a/src/agents/core/session/types.ts +++ b/src/agents/core/session/types.ts @@ -103,6 +103,7 @@ export interface Session { endTime?: number; // Unix timestamp (ms) workingDirectory: string; // CWD where agent was launched gitBranch?: string; // Git branch at session start (optional, detected from workingDirectory) + repository?: string; // Resolved repository identifier: owner/repo from git remote, or parent/current fallback correlation: CorrelationResult; status: SessionStatus; diff --git a/src/cli/commands/hook.ts b/src/cli/commands/hook.ts index 3345fd2b..4bf264ec 100644 --- a/src/cli/commands/hook.ts +++ b/src/cli/commands/hook.ts @@ -637,13 +637,17 @@ async function createSessionRecord(event: SessionStartEvent, sessionId: string, // Determine working directory const workingDirectory = event.cwd || process.cwd(); - // Detect git branch + // Detect git branch and remote repository in parallel let gitBranch: string | undefined; + let remoteRepository: string | undefined; try { - const { detectGitBranch } = await import('../../utils/processes.js'); - gitBranch = await detectGitBranch(workingDirectory); + const { detectGitBranch, detectGitRemoteRepo } = await import('../../utils/processes.js'); + [gitBranch, remoteRepository] = await Promise.all([ + detectGitBranch(workingDirectory), + detectGitRemoteRepo(workingDirectory), + ]); } catch (error) { - logger.debug('[hook:SessionStart] Could not detect git branch:', error); + logger.debug('[hook:SessionStart] Could not detect git info:', error); } // Import session types and store @@ -658,6 +662,7 @@ async function createSessionRecord(event: SessionStartEvent, sessionId: string, ...(project && { project }), startTime: Date.now(), workingDirectory, + ...(remoteRepository && { repository: remoteRepository }), ...(gitBranch && { gitBranch }), status: 'active' as const, activeDurationMs: 0, // Initialize active duration tracking diff --git a/src/providers/plugins/sso/session/processors/metrics/metrics-aggregator.ts b/src/providers/plugins/sso/session/processors/metrics/metrics-aggregator.ts index 5c5caad5..9932d637 100644 --- a/src/providers/plugins/sso/session/processors/metrics/metrics-aggregator.ts +++ b/src/providers/plugins/sso/session/processors/metrics/metrics-aggregator.ts @@ -208,7 +208,7 @@ function buildSessionAttributes( agent: session.agentName, agent_version: version, llm_model: primaryModel || 'unknown', - repository: extractRepository(session.workingDirectory), + repository: session.repository ?? extractRepository(session.workingDirectory), session_id: agentSessionId, // Use agent session ID for API correlation branch: branch, ...(session.project && { project: session.project }), diff --git a/src/providers/plugins/sso/session/processors/metrics/metrics-api-client.ts b/src/providers/plugins/sso/session/processors/metrics/metrics-api-client.ts index a557091d..89720df1 100644 --- a/src/providers/plugins/sso/session/processors/metrics/metrics-api-client.ts +++ b/src/providers/plugins/sso/session/processors/metrics/metrics-api-client.ts @@ -250,7 +250,7 @@ export class MetricsSender { * @param extensionsSummary - Optional extensions scan summary (project + global scopes) */ async sendSessionStart( - session: Pick & { model?: string }, + session: Pick & { model?: string }, workingDirectory: string, status: SessionStartStatus = { status: 'started' }, error?: SessionError, @@ -260,8 +260,8 @@ export class MetricsSender { // Detect git branch const branch = await detectGitBranch(workingDirectory); - // Extract repository from working directory - const repository = this.extractRepository(workingDirectory); + // Use canonical owner/repo from session if available; fall back to filesystem derivation + const repository = session.repository ?? this.extractRepository(workingDirectory); // Build session start metric with status const attributes: any = { @@ -379,7 +379,7 @@ export class MetricsSender { * @param activeDurationMs - Optional active duration excluding idle time */ async sendSessionEnd( - session: Pick & { model?: string }, + session: Pick & { model?: string }, workingDirectory: string, status: SessionEndStatus, durationMs: number, @@ -389,8 +389,8 @@ export class MetricsSender { // Detect git branch const branch = await detectGitBranch(workingDirectory); - // Extract repository from working directory - const repository = this.extractRepository(workingDirectory); + // Use canonical owner/repo from session if available; fall back to filesystem derivation + const repository = session.repository ?? this.extractRepository(workingDirectory); // Build session end metric with status const attributes: any = { diff --git a/src/utils/processes.ts b/src/utils/processes.ts index 4ed947ce..5c401ba0 100644 --- a/src/utils/processes.ts +++ b/src/utils/processes.ts @@ -365,6 +365,26 @@ export async function npxRun( // Git Operations // ============================================================================ +/** + * Detect canonical repository identifier from git remote origin URL. + * Supports both HTTPS (https://github.com/owner/repo.git) and SSH (git@github.com:owner/repo.git) formats. + * Works with GitHub, GitLab, Bitbucket, and self-hosted instances. + * + * @param cwd - Working directory path + * @returns Repository in "owner/repo" format, or undefined if no remote is configured + */ +export async function detectGitRemoteRepo(cwd: string): Promise { + try { + const { stdout } = await execAsync('git remote get-url origin', { cwd, timeout: 5000 }); + const remoteUrl = stdout.trim(); + const match = remoteUrl.match(/[:/]([^/]+)\/([^/.]+?)(?:\.git)?$/); + if (match) return `${match[1]}/${match[2]}`; + return undefined; + } catch { + return undefined; + } +} + /** * Detect current git branch from working directory *