diff --git a/internal/server/manager.go b/internal/server/manager.go index 45ac851a..c1497907 100644 --- a/internal/server/manager.go +++ b/internal/server/manager.go @@ -541,6 +541,7 @@ func (i *Investigation) Start() error { LinkedRepos: i.LinkedRepos, LaunchCwd: i.LaunchCwd, KubeconfigPath: i.KubeconfigPath, + Cluster: i.ActiveContext, Profile: i.Profile, }) if err != nil { diff --git a/internal/server/rehydrate.go b/internal/server/rehydrate.go index 7e7f2c96..895203a1 100644 --- a/internal/server/rehydrate.go +++ b/internal/server/rehydrate.go @@ -141,6 +141,7 @@ func (a *apiHandlers) rehydrate(inv *Investigation) error { LinkedRepos: linked, LaunchCwd: inv.LaunchCwd, KubeconfigPath: inv.KubeconfigPath, + Cluster: inv.ActiveContext, Profile: a.prof, } priorID := inv.ClaudeSessionID diff --git a/internal/sessions/session.go b/internal/sessions/session.go index 8d9e5925..beb7cab4 100644 --- a/internal/sessions/session.go +++ b/internal/sessions/session.go @@ -23,13 +23,13 @@ import ( // Options describes how to launch one session. They mirror the bits of // preflight.Result and program-level Deps that affect what claude sees. type Options struct { - Namespace string - UserNotes string - IncidentURL string // operator-supplied incident.io incident URL; empty when not set - SlackChannelURL string // operator-supplied slack channel/thread URL; empty when not set - SlackChannelID string - SlackChannelName string - MCPConfigPath string + Namespace string + UserNotes string + IncidentURL string // operator-supplied incident.io incident URL; empty when not set + SlackChannelURL string // operator-supplied slack channel/thread URL; empty when not set + SlackChannelID string + SlackChannelName string + MCPConfigPath string SlackMCPAvailable bool // triagent-slack MCP wired (slack token linked) IncidentioMCPAvailable bool // triagent-incidentio MCP wired (incidentio token linked) LinkedRepos []repos.LinkedRepo @@ -51,7 +51,9 @@ type Options struct { AlertKind string // ProjectID is the deployment / tenant identifier (when known). ProjectID string - // Cluster is the kube-context / cluster name (when known). + // Cluster is the kubeconfig context name the launcher seeded at + // preflight (when known). Rendered as kubernetes-context in the + // opening prompt and exposed to namespace_derivation as "cluster". Cluster string } @@ -184,9 +186,17 @@ func clusterIDFromNamespace(ns string) string { // from Options. Events are delivered on the returned channel; the channel // closes when claude exits or ctx is cancelled. func (s *Session) Start(ctx context.Context) (<-chan claude.Event, error) { + return s.inner.Start(ctx, s.startPrompt()) +} + +// startPrompt renders the opening prompt from Options. Cluster feeds the +// Environment block's kubernetes-context line so the agent sees the +// context the launcher already seeded instead of "". +func (s *Session) startPrompt() string { clusterID := clusterIDFromNamespace(s.opts.Namespace) - prompt := prompts.Build(prompts.Env{ - UserNotes: s.opts.UserNotes, + return prompts.Build(prompts.Env{ + Context: s.opts.Cluster, + UserNotes: s.opts.UserNotes, InputValues: map[string]map[string]any{ "cluster_id": {"value": clusterID}, "incident_url": {"value": s.opts.IncidentURL}, @@ -197,7 +207,6 @@ func (s *Session) Start(ctx context.Context) (<-chan claude.Event, error) { IncidentioMCPAvailable: s.opts.IncidentioMCPAvailable, LinkedRepos: s.opts.LinkedRepos, }, s.opts.Profile) - return s.inner.Start(ctx, prompt) } // Resume continues the most-recent conversation with a follow-up prompt. diff --git a/internal/sessions/session_test.go b/internal/sessions/session_test.go index 7fab1f2b..75a48deb 100644 --- a/internal/sessions/session_test.go +++ b/internal/sessions/session_test.go @@ -139,3 +139,20 @@ func TestNew_ForwardsProfileInvestigationModel(t *testing.T) { require.NoError(t, err) assert.Equal(t, "claude-opus-4-7", sess.inner.Model()) } + +func TestStartPrompt_EmitsSeededKubeContext(t *testing.T) { + t.Parallel() + s := &Session{opts: Options{ + Cluster: "camunda.teleport.sh-saas-int-worker-3", + Profile: &profile.Profile{}, + }} + got := s.startPrompt() + assert.Contains(t, got, "kubernetes-context: camunda.teleport.sh-saas-int-worker-3\n") + assert.NotContains(t, got, "kubernetes-context: ") +} + +func TestStartPrompt_UnsetWhenNoContextSeeded(t *testing.T) { + t.Parallel() + s := &Session{opts: Options{Profile: &profile.Profile{}}} + assert.Contains(t, s.startPrompt(), "kubernetes-context: \n") +}