From 4ba9cabff15ab641a026ec9a74eb543d1bd6054c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=86gir=20M=C3=A1ni=20Hauksson?= <54936225+sourcehawk@users.noreply.github.com> Date: Wed, 26 Aug 2026 01:00:11 +0200 Subject: [PATCH] fix(sessions): render the seeded kube context in the opening prompt Preflight seeds /active-context from the operator's cluster pick and the k8s MCP hydrates from it, but the Investigation's ActiveContext was never copied into sessions.Options, so prompts.Env.Context stayed empty and every opening prompt read "kubernetes-context: ". The agent then believed no context was bound while the MCP was already pointed at the cluster. Plumb Investigation.ActiveContext into Options.Cluster on both the fresh-start and rehydrate paths, and extract the prompt build into startPrompt() so the rendered line is testable without spawning claude. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01RWH96oteD5VB4R9togLq5V --- internal/server/manager.go | 1 + internal/server/rehydrate.go | 1 + internal/sessions/session.go | 31 ++++++++++++++++++++----------- internal/sessions/session_test.go | 17 +++++++++++++++++ 4 files changed, 39 insertions(+), 11 deletions(-) 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") +}