	labels := []string{"televibe", "teleport-access"}
	if in.Tier != "" {
		labels = append(labels, "tier-"+strings.ToLower(in.Tier))
	}
	if in.Escalation {
		labels = append(labels, "televibe-escalation")
	}

	summary := fmt.Sprintf("[televibe] Access request %s - %s (%s)", in.RequestID, in.Tier, in.Slang)
	if in.Escalation {
		summary = fmt.Sprintf("[televibe][escalation] Session escalation %s - %s (%s)", in.RequestID, in.Tier, in.Slang)
	}

package jira

import (
	"context"
	"encoding/json"
	"fmt"
	"io"
	"regexp"
	"strings"
	"time"

	gojira "github.com/andygrunwald/go-jira/v2/cloud"
)

type Config struct {
	URL            string        `toml:"url"`
	Username       string        `toml:"username"`
	APIToken       string        `toml:"api_token"`
	Project        string        `toml:"project"`
	IssueType      string        `toml:"issue_type"`
	PollInterval   time.Duration `toml:"poll_interval"`
	ApprovedStatus string        `toml:"approved_status"`
	RejectedStatus string        `toml:"rejected_status"`
}

func (c *Config) ApplyDefaults() {
	if c.IssueType == "" {
		c.IssueType = "Task"
	}
	if c.PollInterval == 0 {
		c.PollInterval = 30 * time.Second
	}
	if c.ApprovedStatus == "" {
		c.ApprovedStatus = "Approved"
	}
	if c.RejectedStatus == "" {
		c.RejectedStatus = "Rejected"
	}
}

func (c *Config) Validate() error {
	if c.URL == "" {
		return fmt.Errorf("url is required")
	}
	if c.Project == "" {
		return fmt.Errorf("project is required")
	}
	if c.Username == "" || c.APIToken == "" {
		return fmt.Errorf("username and api_token are required")
	}
	return nil
}

type Status int

const (
	StatusOpen Status = iota
	StatusApproved
	StatusRejected
)

func (s Status) String() string {
	switch s {
	case StatusApproved:
		return "approved"
	case StatusRejected:
		return "rejected"
	}
	return "open"
}

type TicketInput struct {
	RequestID   string
	User        string
	Roles       []string
	Reason      string
	Score       int
	Tier        string
	Slang       string
	Components  map[string]int
	AIReasoning string
	Escalation  bool
}

type Client struct {
	cfg     Config
	jira    *gojira.Client
	reqIDRE *regexp.Regexp
}

func New(cfg Config) (*Client, error) {
	tp := gojira.BasicAuthTransport{Username: cfg.Username, APIToken: cfg.APIToken}
	jc, err := gojira.NewClient(cfg.URL, tp.Client())
	if err != nil {
		return nil, err
	}
	return &Client{
		cfg:     cfg,
		jira:    jc,
		reqIDRE: regexp.MustCompile(`Teleport Request ID:\s*([A-Za-z0-9-]+)`),
	}, nil
}

/*
	func (c *Client) CreateITSMTicket(ctx context.Context, in TicketInput) (string, error) {
		summary := fmt.Sprintf("[televibe] Access request %s — %s (%s)", in.RequestID, in.Tier, in.Slang)
		issue := &gojira.Issue{
			Fields: &gojira.IssueFields{
				Project:     gojira.Project{Key: c.cfg.Project},
				Type:        gojira.IssueType{Name: c.cfg.IssueType},
				Summary:     summary,
				Description: buildDescription(in),
				Labels:      []string{"televibe", "teleport-access", "tier-" + in.Tier},
			},
		}
		b, err := json.MarshalIndent(issue, "", "  ")
		if err != nil {
			return "", fmt.Errorf("marshal jira issue: %w", err)
		}
		fmt.Println("Jira request body:")
		fmt.Println(string(b))

		created, _, err := c.jira.Issue.Create(ctx, issue)
		if err != nil {
			return "", fmt.Errorf("create jira issue: %w", err)
		}
		if created == nil {
			return "", fmt.Errorf("jira returned nil issue without error")
		}

		return created.Key, nil
	}
*/
func (c *Client) CreateITSMTicket(ctx context.Context, in TicketInput) (string, error) {
	if c.cfg.Project == "" {
		return "", fmt.Errorf("jira project key is empty")
	}
	if c.cfg.IssueType == "" {
		return "", fmt.Errorf("jira issue type is empty")
	}
	if in.RequestID == "" {
		return "", fmt.Errorf("request id is empty")
	}

	labels := []string{"televibe", "teleport-access"}
	if in.Tier != "" {
		labels = append(labels, "tier-"+strings.ToLower(in.Tier))
	}

	summary := fmt.Sprintf("[televibe] Access request %s - %s (%s)", in.RequestID, in.Tier, in.Slang)

	issue := &gojira.Issue{
		Fields: &gojira.IssueFields{
			Project:     gojira.Project{Key: c.cfg.Project},
			Type:        gojira.IssueType{Name: c.cfg.IssueType},
			Summary:     summary,
			Description: buildDescription(in),
			Labels:      labels,
		},
	}

	reqBody, err := json.MarshalIndent(issue, "", "  ")
	if err != nil {
		return "", fmt.Errorf("marshal jira issue: %w", err)
	}
	fmt.Println("Jira request body:")
	fmt.Println(string(reqBody))

	created, resp, err := c.jira.Issue.Create(ctx, issue)
	if err != nil {
		if resp != nil && resp.Body != nil {
			body, _ := io.ReadAll(resp.Body)
			fmt.Println("Jira error response:")
			fmt.Println(string(body))
		}
		return "", fmt.Errorf("create jira issue: %w", err)
	}

	if created == nil {
		return "", fmt.Errorf("jira returned nil issue without error")
	}

	return created.Key, nil
}

func (c *Client) TicketStatus(ctx context.Context, key string) (Status, error) {
	issue, _, err := c.jira.Issue.Get(ctx, key, nil)
	if err != nil {
		return StatusOpen, err
	}
	if issue.Fields == nil || issue.Fields.Status == nil {
		return StatusOpen, nil
	}
	switch issue.Fields.Status.Name {
	case c.cfg.ApprovedStatus:
		return StatusApproved, nil
	case c.cfg.RejectedStatus:
		return StatusRejected, nil
	}
	return StatusOpen, nil
}

func (c *Client) ListOpenTeleportTickets(ctx context.Context) (map[string]string, error) {
	jql := fmt.Sprintf(`project = %q AND labels = televibe AND status not in (%q, %q)`,
		c.cfg.Project, c.cfg.ApprovedStatus, c.cfg.RejectedStatus)
	issues, _, err := c.jira.Issue.Search(ctx, jql, nil)
	if err != nil {
		return nil, err
	}
	out := map[string]string{}
	for _, iss := range issues {
		if iss.Fields == nil {
			continue
		}
		m := c.reqIDRE.FindStringSubmatch(iss.Fields.Description)
		if len(m) < 2 {
			continue
		}
		out[m[1]] = iss.Key
	}
	return out, nil
}

func buildDescription(in TicketInput) string {
	var sb strings.Builder
	sb.WriteString("h2. Teleport Access Request Review\n\n")
	fmt.Fprintf(&sb, "* *Teleport Request ID:* %s\n", in.RequestID)
	fmt.Fprintf(&sb, "* *Requester:* %s\n", in.User)
	fmt.Fprintf(&sb, "* *Roles requested:* %s\n", strings.Join(in.Roles, ", "))
	fmt.Fprintf(&sb, "* *Risk score:* %d\n", in.Score)
	fmt.Fprintf(&sb, "* *Risk tier:* %s (%q)\n\n", in.Tier, in.Slang)

	sb.WriteString("h3. Request reason\n")
	sb.WriteString("{code}\n")
	sb.WriteString(in.Reason)
	sb.WriteString("\n{code}\n\n")

	if len(in.Components) > 0 {
		sb.WriteString("h3. Risk components\n")
		for k, v := range in.Components {
			fmt.Fprintf(&sb, "* %s: +%d\n", k, v)
		}
		sb.WriteString("\n")
	}

	if in.AIReasoning != "" {
		sb.WriteString("h3. AI reasoning\n")
		sb.WriteString("{quote}\n")
		sb.WriteString(in.AIReasoning)
		sb.WriteString("\n{quote}\n\n")
	}

	sb.WriteString("h3. SRE action required\n")
	sb.WriteString("Transition this ticket to the configured approved or rejected status to resolve the Teleport access request.\n")

	return sb.String()
}

+    labels = append(labels, "televibe-escalation")

+    summary = fmt.Sprintf("[televibe][escalation] Session escalation %s - %s (%s)", in.RequestID, in.Tier, in.Slang)


Edit

func buildDescription(in TicketInput) string {
	var sb strings.Builder
	sb.WriteString("h2. Teleport Access Request Review\n\n")
	fmt.Fprintf(&sb, "* *Teleport Request ID:* %s\n", in.RequestID)
	fmt.Fprintf(&sb, "* *Requester:* %s\n", in.User)
	fmt.Fprintf(&sb, "* *Roles requested:* %s\n", strings.Join(in.Roles, ", "))
	fmt.Fprintf(&sb, "* *Risk score:* %d\n", in.Score)
	fmt.Fprintf(&sb, "* *Risk tier:* %s (%q)\n\n", in.Tier, in.Slang)

func buildDescription(in TicketInput) string {
	var sb strings.Builder
	if in.Escalation {
		sb.WriteString("h2. Teleport Session Escalation Review\n\n")
		sb.WriteString("{panel:title=Session escalation}This request is a mid-session privilege bump — please prioritise review.{panel}\n\n")
	} else {
		sb.WriteString("h2. Teleport Access Request Review\n\n")
	}
	fmt.Fprintf(&sb, "* *Teleport Request ID:* %s\n", in.RequestID)
	fmt.Fprintf(&sb, "* *Requester:* %s\n", in.User)
	fmt.Fprintf(&sb, "* *Roles requested:* %s\n", strings.Join(in.Roles, ", "))
	fmt.Fprintf(&sb, "* *Risk score:* %d\n", in.Score)
	fmt.Fprintf(&sb, "* *Risk tier:* %s (%q)\n\n", in.Tier, in.Slang)


The file /Users/steve.briggs/projects/operations/apps/televibe/jira/itsm.go has been updated successfully.

func buildDescription(in TicketInput) string {
	var sb strings.Builder
	if in.Escalation {
		sb.WriteString("h2. Teleport Session Escalation Review\n\n")
		sb.WriteString("{panel:title=Session escalation}This request is a mid-session privilege bump — please prioritise review.{panel}\n\n")
	} else {
		sb.WriteString("h2. Teleport Access Request Review\n\n")
	}
	fmt.Fprintf(&sb, "* *Teleport Request ID:* %s\n", in.RequestID)
	fmt.Fprintf(&sb, "* *Requester:* %s\n", in.User)
	fmt.Fprintf(&sb, "* *Roles requested:* %s\n", strings.Join(in.Roles, ", "))
	fmt.Fprintf(&sb, "* *Risk score:* %d\n", in.Score)
	fmt.Fprintf(&sb, "* *Risk tier:* %s (%q)\n\n", in.Tier, in.Slang)

package jira

import (
	"context"
	"encoding/json"
	"fmt"
	"io"
	"regexp"
	"strings"
	"time"

	gojira "github.com/andygrunwald/go-jira/v2/cloud"
)

type Config struct {
	URL            string        `toml:"url"`
	Username       string        `toml:"username"`
	APIToken       string        `toml:"api_token"`
	Project        string        `toml:"project"`
	IssueType      string        `toml:"issue_type"`
	PollInterval   time.Duration `toml:"poll_interval"`
	ApprovedStatus string        `toml:"approved_status"`
	RejectedStatus string        `toml:"rejected_status"`
}

func (c *Config) ApplyDefaults() {
	if c.IssueType == "" {
		c.IssueType = "Task"
	}
	if c.PollInterval == 0 {
		c.PollInterval = 30 * time.Second
	}
	if c.ApprovedStatus == "" {
		c.ApprovedStatus = "Approved"
	}
	if c.RejectedStatus == "" {
		c.RejectedStatus = "Rejected"
	}
}

func (c *Config) Validate() error {
	if c.URL == "" {
		return fmt.Errorf("url is required")
	}
	if c.Project == "" {
		return fmt.Errorf("project is required")
	}
	if c.Username == "" || c.APIToken == "" {
		return fmt.Errorf("username and api_token are required")
	}
	return nil
}

type Status int

const (
	StatusOpen Status = iota
	StatusApproved
	StatusRejected
)

func (s Status) String() string {
	switch s {
	case StatusApproved:
		return "approved"
	case StatusRejected:
		return "rejected"
	}
	return "open"
}

type TicketInput struct {
	RequestID   string
	User        string
	Roles       []string
	Reason      string
	Score       int
	Tier        string
	Slang       string
	Components  map[string]int
	AIReasoning string
	Escalation  bool
}

type Client struct {
	cfg     Config
	jira    *gojira.Client
	reqIDRE *regexp.Regexp
}

func New(cfg Config) (*Client, error) {
	tp := gojira.BasicAuthTransport{Username: cfg.Username, APIToken: cfg.APIToken}
	jc, err := gojira.NewClient(cfg.URL, tp.Client())
	if err != nil {
		return nil, err
	}
	return &Client{
		cfg:     cfg,
		jira:    jc,
		reqIDRE: regexp.MustCompile(`Teleport Request ID:\s*([A-Za-z0-9-]+)`),
	}, nil
}

/*
	func (c *Client) CreateITSMTicket(ctx context.Context, in TicketInput) (string, error) {
		summary := fmt.Sprintf("[televibe] Access request %s — %s (%s)", in.RequestID, in.Tier, in.Slang)
		issue := &gojira.Issue{
			Fields: &gojira.IssueFields{
				Project:     gojira.Project{Key: c.cfg.Project},
				Type:        gojira.IssueType{Name: c.cfg.IssueType},
				Summary:     summary,
				Description: buildDescription(in),
				Labels:      []string{"televibe", "teleport-access", "tier-" + in.Tier},
			},
		}
		b, err := json.MarshalIndent(issue, "", "  ")
		if err != nil {
			return "", fmt.Errorf("marshal jira issue: %w", err)
		}
		fmt.Println("Jira request body:")
		fmt.Println(string(b))

		created, _, err := c.jira.Issue.Create(ctx, issue)
		if err != nil {
			return "", fmt.Errorf("create jira issue: %w", err)
		}
		if created == nil {
			return "", fmt.Errorf("jira returned nil issue without error")
		}

		return created.Key, nil
	}
*/
func (c *Client) CreateITSMTicket(ctx context.Context, in TicketInput) (string, error) {
	if c.cfg.Project == "" {
		return "", fmt.Errorf("jira project key is empty")
	}
	if c.cfg.IssueType == "" {
		return "", fmt.Errorf("jira issue type is empty")
	}
	if in.RequestID == "" {
		return "", fmt.Errorf("request id is empty")
	}

	labels := []string{"televibe", "teleport-access"}
	if in.Tier != "" {
		labels = append(labels, "tier-"+strings.ToLower(in.Tier))
	}
	if in.Escalation {
		labels = append(labels, "televibe-escalation")
	}

	summary := fmt.Sprintf("[televibe] Access request %s - %s (%s)", in.RequestID, in.Tier, in.Slang)
	if in.Escalation {
		summary = fmt.Sprintf("[televibe][escalation] Session escalation %s - %s (%s)", in.RequestID, in.Tier, in.Slang)
	}

	issue := &gojira.Issue{
		Fields: &gojira.IssueFields{
			Project:     gojira.Project{Key: c.cfg.Project},
			Type:        gojira.IssueType{Name: c.cfg.IssueType},
			Summary:     summary,
			Description: buildDescription(in),
			Labels:      labels,
		},
	}

	reqBody, err := json.MarshalIndent(issue, "", "  ")
	if err != nil {
		return "", fmt.Errorf("marshal jira issue: %w", err)
	}
	fmt.Println("Jira request body:")
	fmt.Println(string(reqBody))

	created, resp, err := c.jira.Issue.Create(ctx, issue)
	if err != nil {
		if resp != nil && resp.Body != nil {
			body, _ := io.ReadAll(resp.Body)
			fmt.Println("Jira error response:")
			fmt.Println(string(body))
		}
		return "", fmt.Errorf("create jira issue: %w", err)
	}

	if created == nil {
		return "", fmt.Errorf("jira returned nil issue without error")
	}

	return created.Key, nil
}

func (c *Client) TicketStatus(ctx context.Context, key string) (Status, error) {
	issue, _, err := c.jira.Issue.Get(ctx, key, nil)
	if err != nil {
		return StatusOpen, err
	}
	if issue.Fields == nil || issue.Fields.Status == nil {
		return StatusOpen, nil
	}
	switch issue.Fields.Status.Name {
	case c.cfg.ApprovedStatus:
		return StatusApproved, nil
	case c.cfg.RejectedStatus:
		return StatusRejected, nil
	}
	return StatusOpen, nil
}

func (c *Client) ListOpenTeleportTickets(ctx context.Context) (map[string]string, error) {
	jql := fmt.Sprintf(`project = %q AND labels = televibe AND status not in (%q, %q)`,
		c.cfg.Project, c.cfg.ApprovedStatus, c.cfg.RejectedStatus)
	issues, _, err := c.jira.Issue.Search(ctx, jql, nil)
	if err != nil {
		return nil, err
	}
	out := map[string]string{}
	for _, iss := range issues {
		if iss.Fields == nil {
			continue
		}
		m := c.reqIDRE.FindStringSubmatch(iss.Fields.Description)
		if len(m) < 2 {
			continue
		}
		out[m[1]] = iss.Key
	}
	return out, nil
}

func buildDescription(in TicketInput) string {
	var sb strings.Builder
	sb.WriteString("h2. Teleport Access Request Review\n\n")
	fmt.Fprintf(&sb, "* *Teleport Request ID:* %s\n", in.RequestID)
	fmt.Fprintf(&sb, "* *Requester:* %s\n", in.User)
	fmt.Fprintf(&sb, "* *Roles requested:* %s\n", strings.Join(in.Roles, ", "))
	fmt.Fprintf(&sb, "* *Risk score:* %d\n", in.Score)
	fmt.Fprintf(&sb, "* *Risk tier:* %s (%q)\n\n", in.Tier, in.Slang)

	sb.WriteString("h3. Request reason\n")
	sb.WriteString("{code}\n")
	sb.WriteString(in.Reason)
	sb.WriteString("\n{code}\n\n")

	if len(in.Components) > 0 {
		sb.WriteString("h3. Risk components\n")
		for k, v := range in.Components {
			fmt.Fprintf(&sb, "* %s: +%d\n", k, v)
		}
		sb.WriteString("\n")
	}

	if in.AIReasoning != "" {
		sb.WriteString("h3. AI reasoning\n")
		sb.WriteString("{quote}\n")
		sb.WriteString(in.AIReasoning)
		sb.WriteString("\n{quote}\n\n")
	}

	sb.WriteString("h3. SRE action required\n")
	sb.WriteString("Transition this ticket to the configured approved or rejected status to resolve the Teleport access request.\n")

	return sb.String()
}

-  sb.WriteString("h2. Teleport Access Request Review\n\n")

+    sb.WriteString("h2. Teleport Session Escalation Review\n\n")

+    sb.WriteString("{panel:title=Session escalation}This request is a mid-session privilege bump — please prioritise review.{panel}\n\n")

+    sb.WriteString("h2. Teleport Access Request Review\n\n")


Now the config example and a small test file for the new helpers.


[risk]
# Requests scoring >= ticket_threshold get an ITSM ticket (and stay PENDING).
# Anything below is auto-approved as "no cap".
ticket_threshold = 25
custx_pattern = 'CUSTX-\d+'
# Off-hours window applies a +10 risk bump. Wraps midnight when start > end.
off_hours_start = 18
off_hours_end = 8
timezone = "America/New_York"

[risk]
# Requests scoring >= ticket_threshold get an ITSM ticket (and stay PENDING).
# Anything below is auto-approved as "no cap".
ticket_threshold = 25
custx_pattern = 'CUSTX-\d+'
# Off-hours window applies a +10 risk bump. Wraps midnight when start > end.
off_hours_start = 18
off_hours_end = 8
timezone = "America/New_York"

# Session escalations — access requests that Teleport tags with the
# escalation_annotation (set via the role's allow.request.annotations).
# These use a tighter fallback threshold. Per-role overrides in
# [risk.escalation_thresholds] take precedence; the strictest (lowest)
# value wins when a request names multiple roles.
escalation_annotation = "televibe.escalation"
escalation_threshold  = 15

[risk.escalation_thresholds]
# "sre-admin"    = 5
# "db-admin"     = 10
# "read-only"    = 25


The file /Users/steve.briggs/projects/operations/apps/televibe/televibe.toml.example has been updated successfully.

[risk]
# Requests scoring >= ticket_threshold get an ITSM ticket (and stay PENDING).
# Anything below is auto-approved as "no cap".
ticket_threshold = 25
custx_pattern = 'CUSTX-\d+'
# Off-hours window applies a +10 risk bump. Wraps midnight when start > end.
off_hours_start = 18
off_hours_end = 8
timezone = "America/New_York"

# Session escalations — access requests that Teleport tags with the
# escalation_annotation (set via the role's allow.request.annotations).
# These use a tighter fallback threshold. Per-role overrides in
# [risk.escalation_thresholds] take precedence; the strictest (lowest)
# value wins when a request names multiple roles.
escalation_annotation = "televibe.escalation"
escalation_threshold  = 15

[risk.escalation_thresholds]
# "sre-admin"    = 5
# "db-admin"     = 10
# "read-only"    = 25

# televibe — Teleport access plugin with AI risk scoring
# Copy to /etc/televibe/televibe.toml and fill in.

[teleport]
addr = "proxy.example.com:443"
identity_file = "/var/lib/teleport/televibe.pem"

[ai]
# Exactly one of: "anthropic", "bedrock", "webhook"
provider = "anthropic"

[ai.anthropic]
api_key = "sk-ant-..."
model = "claude-sonnet-4-6"

[ai.bedrock]
region = "us-west-2"
model_id = "us.anthropic.claude-sonnet-4-6-v1:0"

[ai.webhook]
url = "https://chat.internal/score"
auth_header = "Bearer replace-me"
# [ai.webhook.headers]
# X-Tenant = "televibe"

[jira]
url = "https://example.atlassian.net"
username = "bot@example.com"
api_token = "replace-me"
project = "ITSM"
issue_type = "Task"
poll_interval = "30s"
approved_status = "Approved"
rejected_status = "Rejected"

[risk]
# Requests scoring >= ticket_threshold get an ITSM ticket (and stay PENDING).
# Anything below is auto-approved as "no cap".
ticket_threshold = 25
custx_pattern = 'CUSTX-\d+'
# Off-hours window applies a +10 risk bump. Wraps midnight when start > end.
off_hours_start = 18
off_hours_end = 8
timezone = "America/New_York"

[reputation]
# Per-user reputation, persisted as JSON on local disk. Every user starts
# at starting_score; points are deducted for bad requests and clamped to
# [floor_score, ceiling_score]. Reputation only goes down — the point is
# to shame, not to forgive.
persist_path              = "/var/lib/televibe/reputation.json"
starting_score            = 100
floor_score               = 0
ceiling_score             = 100
penalty_missing_custx     = 10
penalty_thin_description  = 5

[telemetry]
# Emit per-user reputation as an OTLP gauge for the collector to ship to
# Datadog. Disable to keep reputation state local only.
enabled       = true
endpoint      = "otel-collector:4317"
insecure      = true
service_name  = "televibe"
export_period = "15s"

[log]
level = "info"

+# Session escalations — access requests that Teleport tags with the

+# escalation_annotation (set via the role's allow.request.annotations).

+# These use a tighter fallback threshold. Per-role overrides in

+# [risk.escalation_thresholds] take precedence; the strictest (lowest)

+# value wins when a request names multiple roles.


lets set the default escalation threshold of 15 but it should be configurable, and preferrably configurable for different requesting roles


Write

package risk

import (
	"testing"

	"github.com/gravitational/teleport/api/types"
)

func newReqWithAnnotations(t *testing.T, roles []string, annotations map[string][]string) types.AccessRequest {
	t.Helper()
	req, err := types.NewAccessRequest("test-req", "alice", roles...)
	if err != nil {
		t.Fatalf("new access request: %v", err)
	}
	if annotations != nil {
		req.SetSystemAnnotations(annotations)
	}
	return req
}

func TestIsEscalation(t *testing.T) {
	cases := []struct {
		name        string
		key         string
		annotations map[string][]string
		want        bool
	}{
		{name: "empty key", key: "", annotations: map[string][]string{"televibe.escalation": {"true"}}, want: false},
		{name: "no annotation", key: "televibe.escalation", annotations: nil, want: false},
		{name: "key present, empty value", key: "televibe.escalation", annotations: map[string][]string{"televibe.escalation": {""}}, want: false},
		{name: "key present with true", key: "televibe.escalation", annotations: map[string][]string{"televibe.escalation": {"true"}}, want: true},
		{name: "key present with arbitrary non-empty", key: "televibe.escalation", annotations: map[string][]string{"televibe.escalation": {"yes-please"}}, want: true},
		{name: "unrelated key set", key: "televibe.escalation", annotations: map[string][]string{"other": {"true"}}, want: false},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			req := newReqWithAnnotations(t, []string{"role-a"}, tc.annotations)
			if got := IsEscalation(req, tc.key); got != tc.want {
				t.Errorf("IsEscalation = %v, want %v", got, tc.want)
			}
		})
	}
}

func TestEscalationThresholdFor(t *testing.T) {
	cfg := Config{
		EscalationThreshold: 15,
		EscalationThresholds: map[string]int{
			"sre-admin": 5,
			"db-admin":  10,
			"loose":     25,
		},
	}

	cases := []struct {
		name  string
		roles []string
		want  int
	}{
		{name: "no roles uses global", roles: nil, want: 15},
		{name: "unknown role uses global", roles: []string{"unknown"}, want: 15},
		{name: "single known role", roles: []string{"sre-admin"}, want: 5},
		{name: "looser-than-global override is honoured in isolation", roles: []string{"loose"}, want: 25},
		{name: "strictest among mixed wins", roles: []string{"loose", "db-admin", "sre-admin"}, want: 5},
		{name: "known plus unknown picks min of override and global", roles: []string{"loose", "unknown"}, want: 15},
	}

	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			if got := cfg.EscalationThresholdFor(tc.roles); got != tc.want {
				t.Errorf("EscalationThresholdFor(%v) = %d, want %d", tc.roles, got, tc.want)
			}
		})
	}
}


File created successfully at: /Users/steve.briggs/projects/operations/apps/televibe/risk/score_test.go

package risk

import (
	"testing"

	"github.com/gravitational/teleport/api/types"
)

func newReqWithAnnotations(t *testing.T, roles []string, annotations map[string][]string) types.AccessRequest {
	t.Helper()
	req, err := types.NewAccessRequest("test-req", "alice", roles...)
	if err != nil {
		t.Fatalf("new access request: %v", err)
	}
	if annotations != nil {
		req.SetSystemAnnotations(annotations)
	}
	return req
}

func TestIsEscalation(t *testing.T) {
	cases := []struct {
		name        string
		key         string
		annotations map[string][]string
		want        bool
	}{
		{name: "empty key", key: "", annotations: map[string][]string{"televibe.escalation": {"true"}}, want: false},
		{name: "no annotation", key: "televibe.escalation", annotations: nil, want: false},
		{name: "key present, empty value", key: "televibe.escalation", annotations: map[string][]string{"televibe.escalation": {""}}, want: false},
		{name: "key present with true", key: "televibe.escalation", annotations: map[string][]string{"televibe.escalation": {"true"}}, want: true},
		{name: "key present with arbitrary non-empty", key: "televibe.escalation", annotations: map[string][]string{"televibe.escalation": {"yes-please"}}, want: true},
		{name: "unrelated key set", key: "televibe.escalation", annotations: map[string][]string{"other": {"true"}}, want: false},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			req := newReqWithAnnotations(t, []string{"role-a"}, tc.annotations)
			if got := IsEscalation(req, tc.key); got != tc.want {
				t.Errorf("IsEscalation = %v, want %v", got, tc.want)
			}
		})
	}
}

func TestEscalationThresholdFor(t *testing.T) {
	cfg := Config{
		EscalationThreshold: 15,
		EscalationThresholds: map[string]int{
			"sre-admin": 5,
			"db-admin":  10,
			"loose":     25,
		},
	}

	cases := []struct {
		name  string
		roles []string
		want  int
	}{
		{name: "no roles uses global", roles: nil, want: 15},
		{name: "unknown role uses global", roles: []string{"unknown"}, want: 15},
		{name: "single known role", roles: []string{"sre-admin"}, want: 5},
		{name: "looser-than-global override is honoured in isolation", roles: []string{"loose"}, want: 25},
		{name: "strictest among mixed wins", roles: []string{"loose", "db-admin", "sre-admin"}, want: 5},
		{name: "known plus unknown picks min of override and global", roles: []string{"loose", "unknown"}, want: 15},
	}

	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			if got := cfg.EscalationThresholdFor(tc.roles); got != tc.want {
				t.Errorf("EscalationThresholdFor(%v) = %d, want %d", tc.roles, got, tc.want)
			}
		})
	}
}


Now let me build and run the tests with the race detector.


Bash

Build televibe to catch compile errors


(Bash completed with no output)


go test -race ./risk/... -v 2>&1 | tail -40

Run risk package tests with race detector


=== RUN   TestIsEscalation
=== RUN   TestIsEscalation/empty_key
=== RUN   TestIsEscalation/no_annotation
=== RUN   TestIsEscalation/key_present,_empty_value
=== RUN   TestIsEscalation/key_present_with_true
=== RUN   TestIsEscalation/key_present_with_arbitrary_non-empty
=== RUN   TestIsEscalation/unrelated_key_set
--- PASS: TestIsEscalation (0.00s)
    --- PASS: TestIsEscalation/empty_key (0.00s)
    --- PASS: TestIsEscalation/no_annotation (0.00s)
    --- PASS: TestIsEscalation/key_present,_empty_value (0.00s)
    --- PASS: TestIsEscalation/key_present_with_true (0.00s)
    --- PASS: TestIsEscalation/key_present_with_arbitrary_non-empty (0.00s)
    --- PASS: TestIsEscalation/unrelated_key_set (0.00s)
=== RUN   TestEscalationThresholdFor
=== RUN   TestEscalationThresholdFor/no_roles_uses_global
=== RUN   TestEscalationThresholdFor/unknown_role_uses_global
=== RUN   TestEscalationThresholdFor/single_known_role
=== RUN   TestEscalationThresholdFor/looser-than-global_override_is_honoured_in_isolation
=== RUN   TestEscalationThresholdFor/strictest_among_mixed_wins
=== RUN   TestEscalationThresholdFor/known_plus_unknown_picks_min_of_override_and_global
--- PASS: TestEscalationThresholdFor (0.00s)
    --- PASS: TestEscalationThresholdFor/no_roles_uses_global (0.00s)
    --- PASS: TestEscalationThresholdFor/unknown_role_uses_global (0.00s)
    --- PASS: TestEscalationThresholdFor/single_known_role (0.00s)
    --- PASS: TestEscalationThresholdFor/looser-than-global_override_is_honoured_in_isolation (0.00s)
    --- PASS: TestEscalationThresholdFor/strictest_among_mixed_wins (0.00s)
    --- PASS: TestEscalationThresholdFor/known_plus_unknown_picks_min_of_override_and_global (0.00s)
PASS
ok  	televibe/risk	2.063s


Clean build, all tests green under `-race`, Sir Charles. Here's what landed:

**Detection** — `risk.IsEscalation(req, key)` at `risk/score.go`; reads `req.GetSystemAnnotations()[key]`, presence-of-non-empty-value wins. No SDK bump needed.

**Config** (`risk/risk.go`)
- `escalation_annotation` — default `televibe.escalation`
- `escalation_threshold` — fallback, default `15`
- `escalation_thresholds` — `map[string]int` of per-role overrides
- `Config.EscalationThresholdFor(roles)` picks the strictest match; unknown roles fall back to the global; explicit looser overrides are honored in isolation.

**Wiring** — `app.go:handleRequest` flips to the escalation threshold when the annotation is present and logs the fact. Escalation flag flows into `jira.TicketInput`; ticket summary becomes `[televibe][escalation] Session escalation …`, picks up a `televibe-escalation` label, and the description opens with a priority panel.

**Example TOML** updated with commented-out role overrides so it reads as documentation.

**Tests** — `risk/score_test.go` covers `IsEscalation` (empty key, missing key, empty value, set value, unrelated key) and `EscalationThresholdFor` (empty, unknown, single known, looser-than-global, strictest-wins, known+unknown mix).

One thing to be aware of: for this to actually fire, a SRE needs to add `request.annotations["televibe.escalation"] = ["true"]` to the escalation-flavored Teleport role spec. That's outside this repo — happy to sketch the role YAML if you want, but I didn't go poking at your Teleport configs uninvited.


can we remove the slang (i.e. sus, mad sus fr) from the code? and also don't use `televibe` in JIRA comments, just use teleport instead


grep -rn "Slang\|slang\|\"no cap\"\|\"sus\"\|mad sus\|straight L" --include="*.go" 2>&1; echo '---'; grep -rn 'televibe' --include="*.go" . 2>&1 | grep -v "_test.go\|package televibe\|\"televibe/\|televibe\.toml" | head -40

Find all slang and televibe-in-Jira references


app.go:172:		"slang", result.Tier.Slang,
app.go:198:			result.Tier.Name, result.Score, result.Tier.Slang)
app.go:212:		Slang:       result.Tier.Slang,
app.go:227:		"ticket", ticket, "tier", result.Tier.Name, "slang", result.Tier.Slang)
jira/itsm.go:79:	Slang       string
jira/itsm.go:106:		summary := fmt.Sprintf("[televibe] Access request %s — %s (%s)", in.RequestID, in.Tier, in.Slang)
jira/itsm.go:153:	summary := fmt.Sprintf("[televibe] Access request %s - %s (%s)", in.RequestID, in.Tier, in.Slang)
jira/itsm.go:155:		summary = fmt.Sprintf("[televibe][escalation] Session escalation %s - %s (%s)", in.RequestID, in.Tier, in.Slang)
jira/itsm.go:242:	fmt.Fprintf(&sb, "* *Risk tier:* %s (%q)\n\n", in.Tier, in.Slang)
risk/risk.go:63:	Slang    string
risk/risk.go:68:	{Name: "low", Slang: "no cap", MinScore: 0},
risk/risk.go:69:	{Name: "medium", Slang: "sus", MinScore: 25},
risk/risk.go:70:	{Name: "high", Slang: "mad sus fr", MinScore: 50},
risk/risk.go:71:	{Name: "critical", Slang: "straight L, touch grass", MinScore: 75},
---
app.go:92:	slog.Info("televibe starting",
app.go:197:		reason := fmt.Sprintf("televibe: %s (score %d, %q) — auto-approved",
app.go:266:		reason := fmt.Sprintf("televibe: ITSM %s resolved (%s)", ticket, status)
app.go:281:			Author:        "televibe",
cmd/submit/main.go:2:// access request as a given user, so you can watch televibe score it.
cmd/televibe/main.go:13:	"televibe"
cmd/televibe/main.go:20:	cfg, err := televibe.LoadConfig(*configPath)
cmd/televibe/main.go:31:	app, err := televibe.NewApp(ctx, cfg)
jira/itsm.go:106:		summary := fmt.Sprintf("[televibe] Access request %s — %s (%s)", in.RequestID, in.Tier, in.Slang)
jira/itsm.go:113:				Labels:      []string{"televibe", "teleport-access", "tier-" + in.Tier},
jira/itsm.go:145:	labels := []string{"televibe", "teleport-access"}
jira/itsm.go:150:		labels = append(labels, "televibe-escalation")
jira/itsm.go:153:	summary := fmt.Sprintf("[televibe] Access request %s - %s (%s)", in.RequestID, in.Tier, in.Slang)
jira/itsm.go:155:		summary = fmt.Sprintf("[televibe][escalation] Session escalation %s - %s (%s)", in.RequestID, in.Tier, in.Slang)
jira/itsm.go:210:	jql := fmt.Sprintf(`project = %q AND labels = televibe AND status not in (%q, %q)`,
telemetry/telemetry.go:31:		c.ServiceName = "televibe"
telemetry/telemetry.go:75:		"televibe.user.reputation",
risk/risk.go:34:		c.EscalationAnnotation = "televibe.escalation"


Making the changes now. The slang lives in `risk/risk.go` via `Tier.Slang`; I'll drop the field and every consumer. For Jira I'll swap `[televibe]` → `[teleport]`, labels `televibe`/`televibe-escalation` → `teleport`/`teleport-escalation`, and fix the JQL. Teleport review reasons and plugin logs still say "televibe" — that's the plugin identifying itself, not Jira content.


Edit

type Tier struct {
	Name     string
	Slang    string
	MinScore int
}

var Tiers = []Tier{
	{Name: "low", Slang: "no cap", MinScore: 0},
	{Name: "medium", Slang: "sus", MinScore: 25},
	{Name: "high", Slang: "mad sus fr", MinScore: 50},
	{Name: "critical", Slang: "straight L, touch grass", MinScore: 75},
}

type Tier struct {
	Name     string
	MinScore int
}

var Tiers = []Tier{
	{Name: "low", MinScore: 0},
	{Name: "medium", MinScore: 25},
	{Name: "high", MinScore: 50},
	{Name: "critical", MinScore: 75},
}


The file /Users/steve.briggs/projects/operations/apps/televibe/risk/risk.go has been updated successfully.

type Tier struct {
	Name     string
	MinScore int
}

var Tiers = []Tier{
	{Name: "low", MinScore: 0},
	{Name: "medium", MinScore: 25},
	{Name: "high", MinScore: 50},
	{Name: "critical", MinScore: 75},
}

package risk

type Config struct {
	TicketThreshold int    `toml:"ticket_threshold"`
	CUSTXPattern    string `toml:"custx_pattern"`
	OffHoursStart   int    `toml:"off_hours_start"`
	OffHoursEnd     int    `toml:"off_hours_end"`
	Timezone        string `toml:"timezone"`

	// EscalationAnnotation is the Teleport system annotation key that marks
	// an access request as a session escalation. A request is treated as an
	// escalation when this key is present with any non-empty value.
	EscalationAnnotation string `toml:"escalation_annotation"`
	// EscalationThreshold is the fallback ticket threshold applied to
	// escalation-flavored requests when no role-specific override matches.
	EscalationThreshold int `toml:"escalation_threshold"`
	// EscalationThresholds overrides EscalationThreshold for specific
	// requested roles. The strictest (lowest) matching value wins when a
	// request names multiple roles.
	EscalationThresholds map[string]int `toml:"escalation_thresholds"`
}

func (c *Config) ApplyDefaults() {
	if c.TicketThreshold == 0 {
		c.TicketThreshold = 25
	}
	if c.CUSTXPattern == "" {
		c.CUSTXPattern = `CUSTX-\d+`
	}
	if c.Timezone == "" {
		c.Timezone = "UTC"
	}
	if c.EscalationAnnotation == "" {
		c.EscalationAnnotation = "televibe.escalation"
	}
	if c.EscalationThreshold == 0 {
		c.EscalationThreshold = 15
	}
}

// EscalationThresholdFor returns the strictest ticket threshold among the
// requested roles. A role without an explicit override uses the global
// EscalationThreshold. An empty role list also yields the global default.
func (c Config) EscalationThresholdFor(roles []string) int {
	if len(roles) == 0 {
		return c.EscalationThreshold
	}
	chosen := -1
	for _, role := range roles {
		t, ok := c.EscalationThresholds[role]
		if !ok {
			t = c.EscalationThreshold
		}
		if chosen == -1 || t < chosen {
			chosen = t
		}
	}
	return chosen
}

type Tier struct {
	Name     string
	Slang    string
	MinScore int
}

var Tiers = []Tier{
	{Name: "low", Slang: "no cap", MinScore: 0},
	{Name: "medium", Slang: "sus", MinScore: 25},
	{Name: "high", Slang: "mad sus fr", MinScore: 50},
	{Name: "critical", Slang: "straight L, touch grass", MinScore: 75},
}

func TierForScore(score int) Tier {
	chosen := Tiers[0]
	for _, t := range Tiers {
		if score >= t.MinScore {
			chosen = t
		}
	}
	return chosen
}

-  {Name: "low", Slang: "no cap", MinScore: 0},

-  {Name: "medium", Slang: "sus", MinScore: 25},

-  {Name: "high", Slang: "mad sus fr", MinScore: 50},

-  {Name: "critical", Slang: "straight L, touch grass", MinScore: 75},


	log.Info("risk scored",
		"score", result.Score,
		"tier", result.Tier.Name,
		"slang", result.Tier.Slang,
		"components", result.Components)

	log.Info("risk scored",
		"score", result.Score,
		"tier", result.Tier.Name,
		"components", result.Components)


The file /Users/steve.briggs/projects/operations/apps/televibe/app.go has been updated successfully.

	log.Info("risk scored",
		"score", result.Score,
		"tier", result.Tier.Name,
		"components", result.Components)

package televibe

import (
	"context"
	"fmt"
	"log/slog"
	"sync"
	"time"

	"github.com/gravitational/teleport/api/client"
	"github.com/gravitational/teleport/api/types"
	"golang.org/x/sync/errgroup"

	"televibe/ai"
	"televibe/jira"
	"televibe/reputation"
	"televibe/risk"
	"televibe/telemetry"
)

type App struct {
	cfg    *Config
	tele   *client.Client
	ai     ai.Provider
	jira   *jira.Client
	scorer *risk.Scorer
	rep    *reputation.Store
	meter  *telemetry.Meter

	pending   map[string]string
	pendingMu sync.Mutex
}

func NewApp(ctx context.Context, cfg *Config) (*App, error) {
	tele, err := client.New(ctx, client.Config{
		Addrs: []string{cfg.Teleport.Addr},
		Credentials: []client.Credentials{
			client.LoadIdentityFile(cfg.Teleport.IdentityFile),
		},
	})
	if err != nil {
		return nil, fmt.Errorf("teleport client: %w", err)
	}

	prov, err := ai.New(cfg.AI)
	if err != nil {
		return nil, fmt.Errorf("ai provider: %w", err)
	}

	jc, err := jira.New(cfg.Jira)
	if err != nil {
		return nil, fmt.Errorf("jira client: %w", err)
	}

	scorer, err := risk.NewScorer(cfg.Risk)
	if err != nil {
		return nil, fmt.Errorf("scorer: %w", err)
	}

	store, err := reputation.NewStore(cfg.Reputation)
	if err != nil {
		return nil, fmt.Errorf("reputation store: %w", err)
	}

	var meter *telemetry.Meter
	if cfg.Telemetry.Enabled {
		meter, err = telemetry.New(ctx, cfg.Telemetry, store)
		if err != nil {
			return nil, fmt.Errorf("telemetry: %w", err)
		}
	}

	return &App{
		cfg:     cfg,
		tele:    tele,
		ai:      prov,
		jira:    jc,
		scorer:  scorer,
		rep:     store,
		meter:   meter,
		pending: map[string]string{},
	}, nil
}

// Close flushes outstanding telemetry. Safe to call even if telemetry
// is disabled.
func (a *App) Close(ctx context.Context) error {
	return a.meter.Shutdown(ctx)
}

func (a *App) Run(ctx context.Context) error {
	slog.Info("televibe starting",
		"provider", a.cfg.AI.Provider,
		"threshold", a.cfg.Risk.TicketThreshold,
		"itsm_project", a.cfg.Jira.Project)

	if err := a.rehydrate(ctx); err != nil {
		return fmt.Errorf("rehydrate open ITSM tickets: %w", err)
	}

	g, gctx := errgroup.WithContext(ctx)
	g.Go(func() error { return a.watchRequests(gctx) })
	g.Go(func() error { return a.pollITSM(gctx) })
	return g.Wait()
}

func (a *App) rehydrate(ctx context.Context) error {
	open, err := a.jira.ListOpenTeleportTickets(ctx)
	if err != nil {
		return err
	}
	a.pendingMu.Lock()
	for reqID, ticket := range open {
		a.pending[reqID] = ticket
	}
	a.pendingMu.Unlock()
	slog.Info("rehydrated pending tickets", "count", len(open))
	return nil
}

func (a *App) watchRequests(ctx context.Context) error {
	watcher, err := a.tele.NewWatcher(ctx, types.Watch{
		Kinds: []types.WatchKind{{Kind: types.KindAccessRequest}},
	})
	if err != nil {
		return fmt.Errorf("start watcher: %w", err)
	}
	defer watcher.Close()

	for {
		select {
		case <-ctx.Done():
			return ctx.Err()
		case <-watcher.Done():
			return watcher.Error()
		case ev := <-watcher.Events():
			if ev.Type != types.OpPut {
				continue
			}
			req, ok := ev.Resource.(types.AccessRequest)
			if !ok {
				continue
			}
			if req.GetState() != types.RequestState_PENDING {
				continue
			}
			a.pendingMu.Lock()
			_, held := a.pending[req.GetName()]
			a.pendingMu.Unlock()
			if held {
				continue
			}
			go a.handleRequest(ctx, req)
		}
	}
}

func (a *App) handleRequest(ctx context.Context, req types.AccessRequest) {
	id := req.GetName()
	user := req.GetUser()
	log := slog.With("request_id", id, "user", user)

	log.Info("scoring access request")
	result, err := a.scorer.Score(ctx, a.ai, req)
	if err != nil {
		log.Error("scoring failed", "err", err)
		return
	}
	log.Info("risk scored",
		"score", result.Score,
		"tier", result.Tier.Name,
		"slang", result.Tier.Slang,
		"components", result.Components)

	assessment, err := a.rep.Assess(ctx, user, result.Components)
	if err != nil {
		log.Warn("reputation update failed", "err", err)
	}
	if len(assessment.Penalties) > 0 {
		reasons := make([]string, 0, len(assessment.Penalties))
		for _, p := range assessment.Penalties {
			reasons = append(reasons, p.Reason)
		}
		log.Info("reputation docked",
			"previous", assessment.Previous,
			"current", assessment.Score,
			"reasons", reasons)
	}

	threshold := a.cfg.Risk.TicketThreshold
	if result.Escalation {
		threshold = a.cfg.Risk.EscalationThresholdFor(req.GetRoles())
		log.Info("session escalation detected", "threshold", threshold)
	}

	if result.Score < threshold {
		reason := fmt.Sprintf("televibe: %s (score %d, %q) — auto-approved",
			result.Tier.Name, result.Score, result.Tier.Slang)
		if err := a.submitReview(ctx, id, types.RequestState_APPROVED, reason); err != nil {
			log.Error("auto-approve failed", "err", err)
		}
		return
	}

	ticket, err := a.jira.CreateITSMTicket(ctx, jira.TicketInput{
		RequestID:   id,
		User:        req.GetUser(),
		Roles:       req.GetRoles(),
		Reason:      req.GetRequestReason(),
		Score:       result.Score,
		Tier:        result.Tier.Name,
		Slang:       result.Tier.Slang,
		Components:  result.Components,
		AIReasoning: result.Reasoning,
		Escalation:  result.Escalation,
	})
	if err != nil {
		log.Error("create ITSM ticket failed", "err", err)

		return
	}

	a.pendingMu.Lock()
	a.pending[id] = ticket
	a.pendingMu.Unlock()
	log.Info("ITSM ticket created, request held for SRE review",
		"ticket", ticket, "tier", result.Tier.Name, "slang", result.Tier.Slang)
}

func (a *App) pollITSM(ctx context.Context) error {
	t := time.NewTicker(a.cfg.Jira.PollInterval)
	defer t.Stop()
	for {
		select {
		case <-ctx.Done():
			return ctx.Err()
		case <-t.C:
			a.reconcileITSM(ctx)
		}
	}
}

func (a *App) reconcileITSM(ctx context.Context) {
	a.pendingMu.Lock()
	snap := make(map[string]string, len(a.pending))
	for k, v := range a.pending {
		snap[k] = v
	}
	a.pendingMu.Unlock()

	for requestID, ticket := range snap {
		status, err := a.jira.TicketStatus(ctx, ticket)
		if err != nil {
			slog.Warn("ITSM status check failed", "ticket", ticket, "err", err)
			continue
		}
		var next types.RequestState
		switch status {
		case jira.StatusApproved:
			next = types.RequestState_APPROVED
		case jira.StatusRejected:
			next = types.RequestState_DENIED
		default:
			continue
		}
		reason := fmt.Sprintf("televibe: ITSM %s resolved (%s)", ticket, status)
		if err := a.submitReview(ctx, requestID, next, reason); err != nil {
			slog.Error("submit review failed", "request_id", requestID, "err", err)
			continue
		}
		a.pendingMu.Lock()
		delete(a.pending, requestID)
		a.pendingMu.Unlock()
	}
}

func (a *App) submitReview(ctx context.Context, requestID string, state types.RequestState, reason string) error {
	_, err := a.tele.SubmitAccessReview(ctx, types.AccessReviewSubmission{
		RequestID: requestID,
		Review: types.AccessReview{
			Author:        "televibe",
			ProposedState: state,
			Reason:        reason,
		},
	})
	return err
}


	if result.Score < threshold {
		reason := fmt.Sprintf("televibe: %s (score %d, %q) — auto-approved",
			result.Tier.Name, result.Score, result.Tier.Slang)
		if err := a.submitReview(ctx, id, types.RequestState_APPROVED, reason); err != nil {
			log.Error("auto-approve failed", "err", err)
		}
		return
	}

	ticket, err := a.jira.CreateITSMTicket(ctx, jira.TicketInput{
		RequestID:   id,
		User:        req.GetUser(),
		Roles:       req.GetRoles(),
		Reason:      req.GetRequestReason(),
		Score:       result.Score,
		Tier:        result.Tier.Name,
		Slang:       result.Tier.Slang,
		Components:  result.Components,
		AIReasoning: result.Reasoning,
		Escalation:  result.Escalation,
	})

	if result.Score < threshold {
		reason := fmt.Sprintf("televibe: %s (score %d) — auto-approved",
			result.Tier.Name, result.Score)
		if err := a.submitReview(ctx, id, types.RequestState_APPROVED, reason); err != nil {
			log.Error("auto-approve failed", "err", err)
		}
		return
	}

	ticket, err := a.jira.CreateITSMTicket(ctx, jira.TicketInput{
		RequestID:   id,
		User:        req.GetUser(),
		Roles:       req.GetRoles(),
		Reason:      req.GetRequestReason(),
		Score:       result.Score,
		Tier:        result.Tier.Name,
		Components:  result.Components,
		AIReasoning: result.Reasoning,
		Escalation:  result.Escalation,
	})


	if result.Score < threshold {
		reason := fmt.Sprintf("televibe: %s (score %d) — auto-approved",
			result.Tier.Name, result.Score)
		if err := a.submitReview(ctx, id, types.RequestState_APPROVED, reason); err != nil {
			log.Error("auto-approve failed", "err", err)
		}
		return
	}

	ticket, err := a.jira.CreateITSMTicket(ctx, jira.TicketInput{
		RequestID:   id,
		User:        req.GetUser(),
		Roles:       req.GetRoles(),
		Reason:      req.GetRequestReason(),
		Score:       result.Score,
		Tier:        result.Tier.Name,
		Components:  result.Components,
		AIReasoning: result.Reasoning,
		Escalation:  result.Escalation,
	})

package televibe

import (
	"context"
	"fmt"
	"log/slog"
	"sync"
	"time"

	"github.com/gravitational/teleport/api/client"
	"github.com/gravitational/teleport/api/types"
	"golang.org/x/sync/errgroup"

	"televibe/ai"
	"televibe/jira"
	"televibe/reputation"
	"televibe/risk"
	"televibe/telemetry"
)

type App struct {
	cfg    *Config
	tele   *client.Client
	ai     ai.Provider
	jira   *jira.Client
	scorer *risk.Scorer
	rep    *reputation.Store
	meter  *telemetry.Meter

	pending   map[string]string
	pendingMu sync.Mutex
}

func NewApp(ctx context.Context, cfg *Config) (*App, error) {
	tele, err := client.New(ctx, client.Config{
		Addrs: []string{cfg.Teleport.Addr},
		Credentials: []client.Credentials{
			client.LoadIdentityFile(cfg.Teleport.IdentityFile),
		},
	})
	if err != nil {
		return nil, fmt.Errorf("teleport client: %w", err)
	}

	prov, err := ai.New(cfg.AI)
	if err != nil {
		return nil, fmt.Errorf("ai provider: %w", err)
	}

	jc, err := jira.New(cfg.Jira)
	if err != nil {
		return nil, fmt.Errorf("jira client: %w", err)
	}

	scorer, err := risk.NewScorer(cfg.Risk)
	if err != nil {
		return nil, fmt.Errorf("scorer: %w", err)
	}

	store, err := reputation.NewStore(cfg.Reputation)
	if err != nil {
		return nil, fmt.Errorf("reputation store: %w", err)
	}

	var meter *telemetry.Meter
	if cfg.Telemetry.Enabled {
		meter, err = telemetry.New(ctx, cfg.Telemetry, store)
		if err != nil {
			return nil, fmt.Errorf("telemetry: %w", err)
		}
	}

	return &App{
		cfg:     cfg,
		tele:    tele,
		ai:      prov,
		jira:    jc,
		scorer:  scorer,
		rep:     store,
		meter:   meter,
		pending: map[string]string{},
	}, nil
}

// Close flushes outstanding telemetry. Safe to call even if telemetry
// is disabled.
func (a *App) Close(ctx context.Context) error {
	return a.meter.Shutdown(ctx)
}

func (a *App) Run(ctx context.Context) error {
	slog.Info("televibe starting",
		"provider", a.cfg.AI.Provider,
		"threshold", a.cfg.Risk.TicketThreshold,
		"itsm_project", a.cfg.Jira.Project)

	if err := a.rehydrate(ctx); err != nil {
		return fmt.Errorf("rehydrate open ITSM tickets: %w", err)
	}

	g, gctx := errgroup.WithContext(ctx)
	g.Go(func() error { return a.watchRequests(gctx) })
	g.Go(func() error { return a.pollITSM(gctx) })
	return g.Wait()
}

func (a *App) rehydrate(ctx context.Context) error {
	open, err := a.jira.ListOpenTeleportTickets(ctx)
	if err != nil {
		return err
	}
	a.pendingMu.Lock()
	for reqID, ticket := range open {
		a.pending[reqID] = ticket
	}
	a.pendingMu.Unlock()
	slog.Info("rehydrated pending tickets", "count", len(open))
	return nil
}

func (a *App) watchRequests(ctx context.Context) error {
	watcher, err := a.tele.NewWatcher(ctx, types.Watch{
		Kinds: []types.WatchKind{{Kind: types.KindAccessRequest}},
	})
	if err != nil {
		return fmt.Errorf("start watcher: %w", err)
	}
	defer watcher.Close()

	for {
		select {
		case <-ctx.Done():
			return ctx.Err()
		case <-watcher.Done():
			return watcher.Error()
		case ev := <-watcher.Events():
			if ev.Type != types.OpPut {
				continue
			}
			req, ok := ev.Resource.(types.AccessRequest)
			if !ok {
				continue
			}
			if req.GetState() != types.RequestState_PENDING {
				continue
			}
			a.pendingMu.Lock()
			_, held := a.pending[req.GetName()]
			a.pendingMu.Unlock()
			if held {
				continue
			}
			go a.handleRequest(ctx, req)
		}
	}
}

func (a *App) handleRequest(ctx context.Context, req types.AccessRequest) {
	id := req.GetName()
	user := req.GetUser()
	log := slog.With("request_id", id, "user", user)

	log.Info("scoring access request")
	result, err := a.scorer.Score(ctx, a.ai, req)
	if err != nil {
		log.Error("scoring failed", "err", err)
		return
	}
	log.Info("risk scored",
		"score", result.Score,
		"tier", result.Tier.Name,
		"components", result.Components)

	assessment, err := a.rep.Assess(ctx, user, result.Components)
	if err != nil {
		log.Warn("reputation update failed", "err", err)
	}
	if len(assessment.Penalties) > 0 {
		reasons := make([]string, 0, len(assessment.Penalties))
		for _, p := range assessment.Penalties {
			reasons = append(reasons, p.Reason)
		}
		log.Info("reputation docked",
			"previous", assessment.Previous,
			"current", assessment.Score,
			"reasons", reasons)
	}

	threshold := a.cfg.Risk.TicketThreshold
	if result.Escalation {
		threshold = a.cfg.Risk.EscalationThresholdFor(req.GetRoles())
		log.Info("session escalation detected", "threshold", threshold)
	}

	if result.Score < threshold {
		reason := fmt.Sprintf("televibe: %s (score %d, %q) — auto-approved",
			result.Tier.Name, result.Score, result.Tier.Slang)
		if err := a.submitReview(ctx, id, types.RequestState_APPROVED, reason); err != nil {
			log.Error("auto-approve failed", "err", err)
		}
		return
	}

	ticket, err := a.jira.CreateITSMTicket(ctx, jira.TicketInput{
		RequestID:   id,
		User:        req.GetUser(),
		Roles:       req.GetRoles(),
		Reason:      req.GetRequestReason(),
		Score:       result.Score,
		Tier:        result.Tier.Name,
		Slang:       result.Tier.Slang,
		Components:  result.Components,
		AIReasoning: result.Reasoning,
		Escalation:  result.Escalation,
	})
	if err != nil {
		log.Error("create ITSM ticket failed", "err", err)

		return
	}

	a.pendingMu.Lock()
	a.pending[id] = ticket
	a.pendingMu.Unlock()
	log.Info("ITSM ticket created, request held for SRE review",
		"ticket", ticket, "tier", result.Tier.Name, "slang", result.Tier.Slang)
}

func (a *App) pollITSM(ctx context.Context) error {
	t := time.NewTicker(a.cfg.Jira.PollInterval)
	defer t.Stop()
	for {
		select {
		case <-ctx.Done():
			return ctx.Err()
		case <-t.C:
			a.reconcileITSM(ctx)
		}
	}
}

func (a *App) reconcileITSM(ctx context.Context) {
	a.pendingMu.Lock()
	snap := make(map[string]string, len(a.pending))
	for k, v := range a.pending {
		snap[k] = v
	}
	a.pendingMu.Unlock()

	for requestID, ticket := range snap {
		status, err := a.jira.TicketStatus(ctx, ticket)
		if err != nil {
			slog.Warn("ITSM status check failed", "ticket", ticket, "err", err)
			continue
		}
		var next types.RequestState
		switch status {
		case jira.StatusApproved:
			next = types.RequestState_APPROVED
		case jira.StatusRejected:
			next = types.RequestState_DENIED
		default:
			continue
		}
		reason := fmt.Sprintf("televibe: ITSM %s resolved (%s)", ticket, status)
		if err := a.submitReview(ctx, requestID, next, reason); err != nil {
			slog.Error("submit review failed", "request_id", requestID, "err", err)
			continue
		}
		a.pendingMu.Lock()
		delete(a.pending, requestID)
		a.pendingMu.Unlock()
	}
}

func (a *App) submitReview(ctx context.Context, requestID string, state types.RequestState, reason string) error {
	_, err := a.tele.SubmitAccessReview(ctx, types.AccessReviewSubmission{
		RequestID: requestID,
		Review: types.AccessReview{
			Author:        "televibe",
			ProposedState: state,
			Reason:        reason,
		},
	})
	return err
}

-    reason := fmt.Sprintf("televibe: %s (score %d, %q) — auto-approved",

-      result.Tier.Name, result.Score, result.Tier.Slang)

+    reason := fmt.Sprintf("televibe: %s (score %d) — auto-approved",


	log.Info("ITSM ticket created, request held for SRE review",
		"ticket", ticket, "tier", result.Tier.Name, "slang", result.Tier.Slang)
}

	log.Info("ITSM ticket created, request held for SRE review",
		"ticket", ticket, "tier", result.Tier.Name)
}


	log.Info("ITSM ticket created, request held for SRE review",
		"ticket", ticket, "tier", result.Tier.Name)
}

package televibe

import (
	"context"
	"fmt"
	"log/slog"
	"sync"
	"time"

	"github.com/gravitational/teleport/api/client"
	"github.com/gravitational/teleport/api/types"
	"golang.org/x/sync/errgroup"

	"televibe/ai"
	"televibe/jira"
	"televibe/reputation"
	"televibe/risk"
	"televibe/telemetry"
)

type App struct {
	cfg    *Config
	tele   *client.Client
	ai     ai.Provider
	jira   *jira.Client
	scorer *risk.Scorer
	rep    *reputation.Store
	meter  *telemetry.Meter

	pending   map[string]string
	pendingMu sync.Mutex
}

func NewApp(ctx context.Context, cfg *Config) (*App, error) {
	tele, err := client.New(ctx, client.Config{
		Addrs: []string{cfg.Teleport.Addr},
		Credentials: []client.Credentials{
			client.LoadIdentityFile(cfg.Teleport.IdentityFile),
		},
	})
	if err != nil {
		return nil, fmt.Errorf("teleport client: %w", err)
	}

	prov, err := ai.New(cfg.AI)
	if err != nil {
		return nil, fmt.Errorf("ai provider: %w", err)
	}

	jc, err := jira.New(cfg.Jira)
	if err != nil {
		return nil, fmt.Errorf("jira client: %w", err)
	}

	scorer, err := risk.NewScorer(cfg.Risk)
	if err != nil {
		return nil, fmt.Errorf("scorer: %w", err)
	}

	store, err := reputation.NewStore(cfg.Reputation)
	if err != nil {
		return nil, fmt.Errorf("reputation store: %w", err)
	}

	var meter *telemetry.Meter
	if cfg.Telemetry.Enabled {
		meter, err = telemetry.New(ctx, cfg.Telemetry, store)
		if err != nil {
			return nil, fmt.Errorf("telemetry: %w", err)
		}
	}

	return &App{
		cfg:     cfg,
		tele:    tele,
		ai:      prov,
		jira:    jc,
		scorer:  scorer,
		rep:     store,
		meter:   meter,
		pending: map[string]string{},
	}, nil
}

// Close flushes outstanding telemetry. Safe to call even if telemetry
// is disabled.
func (a *App) Close(ctx context.Context) error {
	return a.meter.Shutdown(ctx)
}

func (a *App) Run(ctx context.Context) error {
	slog.Info("televibe starting",
		"provider", a.cfg.AI.Provider,
		"threshold", a.cfg.Risk.TicketThreshold,
		"itsm_project", a.cfg.Jira.Project)

	if err := a.rehydrate(ctx); err != nil {
		return fmt.Errorf("rehydrate open ITSM tickets: %w", err)
	}

	g, gctx := errgroup.WithContext(ctx)
	g.Go(func() error { return a.watchRequests(gctx) })
	g.Go(func() error { return a.pollITSM(gctx) })
	return g.Wait()
}

func (a *App) rehydrate(ctx context.Context) error {
	open, err := a.jira.ListOpenTeleportTickets(ctx)
	if err != nil {
		return err
	}
	a.pendingMu.Lock()
	for reqID, ticket := range open {
		a.pending[reqID] = ticket
	}
	a.pendingMu.Unlock()
	slog.Info("rehydrated pending tickets", "count", len(open))
	return nil
}

func (a *App) watchRequests(ctx context.Context) error {
	watcher, err := a.tele.NewWatcher(ctx, types.Watch{
		Kinds: []types.WatchKind{{Kind: types.KindAccessRequest}},
	})
	if err != nil {
		return fmt.Errorf("start watcher: %w", err)
	}
	defer watcher.Close()

	for {
		select {
		case <-ctx.Done():
			return ctx.Err()
		case <-watcher.Done():
			return watcher.Error()
		case ev := <-watcher.Events():
			if ev.Type != types.OpPut {
				continue
			}
			req, ok := ev.Resource.(types.AccessRequest)
			if !ok {
				continue
			}
			if req.GetState() != types.RequestState_PENDING {
				continue
			}
			a.pendingMu.Lock()
			_, held := a.pending[req.GetName()]
			a.pendingMu.Unlock()
			if held {
				continue
			}
			go a.handleRequest(ctx, req)
		}
	}
}

func (a *App) handleRequest(ctx context.Context, req types.AccessRequest) {
	id := req.GetName()
	user := req.GetUser()
	log := slog.With("request_id", id, "user", user)

	log.Info("scoring access request")
	result, err := a.scorer.Score(ctx, a.ai, req)
	if err != nil {
		log.Error("scoring failed", "err", err)
		return
	}
	log.Info("risk scored",
		"score", result.Score,
		"tier", result.Tier.Name,
		"components", result.Components)

	assessment, err := a.rep.Assess(ctx, user, result.Components)
	if err != nil {
		log.Warn("reputation update failed", "err", err)
	}
	if len(assessment.Penalties) > 0 {
		reasons := make([]string, 0, len(assessment.Penalties))
		for _, p := range assessment.Penalties {
			reasons = append(reasons, p.Reason)
		}
		log.Info("reputation docked",
			"previous", assessment.Previous,
			"current", assessment.Score,
			"reasons", reasons)
	}

	threshold := a.cfg.Risk.TicketThreshold
	if result.Escalation {
		threshold = a.cfg.Risk.EscalationThresholdFor(req.GetRoles())
		log.Info("session escalation detected", "threshold", threshold)
	}

	if result.Score < threshold {
		reason := fmt.Sprintf("televibe: %s (score %d) — auto-approved",
			result.Tier.Name, result.Score)
		if err := a.submitReview(ctx, id, types.RequestState_APPROVED, reason); err != nil {
			log.Error("auto-approve failed", "err", err)
		}
		return
	}

	ticket, err := a.jira.CreateITSMTicket(ctx, jira.TicketInput{
		RequestID:   id,
		User:        req.GetUser(),
		Roles:       req.GetRoles(),
		Reason:      req.GetRequestReason(),
		Score:       result.Score,
		Tier:        result.Tier.Name,
		Components:  result.Components,
		AIReasoning: result.Reasoning,
		Escalation:  result.Escalation,
	})
	if err != nil {
		log.Error("create ITSM ticket failed", "err", err)

		return
	}

	a.pendingMu.Lock()
	a.pending[id] = ticket
	a.pendingMu.Unlock()
	log.Info("ITSM ticket created, request held for SRE review",
		"ticket", ticket, "tier", result.Tier.Name, "slang", result.Tier.Slang)
}

func (a *App) pollITSM(ctx context.Context) error {
	t := time.NewTicker(a.cfg.Jira.PollInterval)
	defer t.Stop()
	for {
		select {
		case <-ctx.Done():
			return ctx.Err()
		case <-t.C:
			a.reconcileITSM(ctx)
		}
	}
}

func (a *App) reconcileITSM(ctx context.Context) {
	a.pendingMu.Lock()
	snap := make(map[string]string, len(a.pending))
	for k, v := range a.pending {
		snap[k] = v
	}
	a.pendingMu.Unlock()

	for requestID, ticket := range snap {
		status, err := a.jira.TicketStatus(ctx, ticket)
		if err != nil {
			slog.Warn("ITSM status check failed", "ticket", ticket, "err", err)
			continue
		}
		var next types.RequestState
		switch status {
		case jira.StatusApproved:
			next = types.RequestState_APPROVED
		case jira.StatusRejected:
			next = types.RequestState_DENIED
		default:
			continue
		}
		reason := fmt.Sprintf("televibe: ITSM %s resolved (%s)", ticket, status)
		if err := a.submitReview(ctx, requestID, next, reason); err != nil {
			slog.Error("submit review failed", "request_id", requestID, "err", err)
			continue
		}
		a.pendingMu.Lock()
		delete(a.pending, requestID)
		a.pendingMu.Unlock()
	}
}

func (a *App) submitReview(ctx context.Context, requestID string, state types.RequestState, reason string) error {
	_, err := a.tele.SubmitAccessReview(ctx, types.AccessReviewSubmission{
		RequestID: requestID,
		Review: types.AccessReview{
			Author:        "televibe",
			ProposedState: state,
			Reason:        reason,
		},
	})
	return err
}

-    "ticket", ticket, "tier", result.Tier.Name, "slang", result.Tier.Slang)

+    "ticket", ticket, "tier", result.Tier.Name)


Now `jira/itsm.go` — drop the `Slang` field, swap `televibe` for `teleport` in every Jira-facing string, and delete the dead commented-out old implementation since it references slang I'd otherwise have to sanitize.


type TicketInput struct {
	RequestID   string
	User        string
	Roles       []string
	Reason      string
	Score       int
	Tier        string
	Slang       string
	Components  map[string]int
	AIReasoning string
	Escalation  bool
}

type TicketInput struct {
	RequestID   string
	User        string
	Roles       []string
	Reason      string
	Score       int
	Tier        string
	Components  map[string]int
	AIReasoning string
	Escalation  bool
}


type TicketInput struct {
	RequestID   string
	User        string
	Roles       []string
	Reason      string
	Score       int
	Tier        string
	Components  map[string]int
	AIReasoning string
	Escalation  bool
}

package jira

import (
	"context"
	"encoding/json"
	"fmt"
	"io"
	"regexp"
	"strings"
	"time"

	gojira "github.com/andygrunwald/go-jira/v2/cloud"
)

type Config struct {
	URL            string        `toml:"url"`
	Username       string        `toml:"username"`
	APIToken       string        `toml:"api_token"`
	Project        string        `toml:"project"`
	IssueType      string        `toml:"issue_type"`
	PollInterval   time.Duration `toml:"poll_interval"`
	ApprovedStatus string        `toml:"approved_status"`
	RejectedStatus string        `toml:"rejected_status"`
}

func (c *Config) ApplyDefaults() {
	if c.IssueType == "" {
		c.IssueType = "Task"
	}
	if c.PollInterval == 0 {
		c.PollInterval = 30 * time.Second
	}
	if c.ApprovedStatus == "" {
		c.ApprovedStatus = "Approved"
	}
	if c.RejectedStatus == "" {
		c.RejectedStatus = "Rejected"
	}
}

func (c *Config) Validate() error {
	if c.URL == "" {
		return fmt.Errorf("url is required")
	}
	if c.Project == "" {
		return fmt.Errorf("project is required")
	}
	if c.Username == "" || c.APIToken == "" {
		return fmt.Errorf("username and api_token are required")
	}
	return nil
}

type Status int

const (
	StatusOpen Status = iota
	StatusApproved
	StatusRejected
)

func (s Status) String() string {
	switch s {
	case StatusApproved:
		return "approved"
	case StatusRejected:
		return "rejected"
	}
	return "open"
}

type TicketInput struct {
	RequestID   string
	User        string
	Roles       []string
	Reason      string
	Score       int
	Tier        string
	Slang       string
	Components  map[string]int
	AIReasoning string
	Escalation  bool
}

type Client struct {
	cfg     Config
	jira    *gojira.Client
	reqIDRE *regexp.Regexp
}

func New(cfg Config) (*Client, error) {
	tp := gojira.BasicAuthTransport{Username: cfg.Username, APIToken: cfg.APIToken}
	jc, err := gojira.NewClient(cfg.URL, tp.Client())
	if err != nil {
		return nil, err
	}
	return &Client{
		cfg:     cfg,
		jira:    jc,
		reqIDRE: regexp.MustCompile(`Teleport Request ID:\s*([A-Za-z0-9-]+)`),
	}, nil
}

/*
	func (c *Client) CreateITSMTicket(ctx context.Context, in TicketInput) (string, error) {
		summary := fmt.Sprintf("[televibe] Access request %s — %s (%s)", in.RequestID, in.Tier, in.Slang)
		issue := &gojira.Issue{
			Fields: &gojira.IssueFields{
				Project:     gojira.Project{Key: c.cfg.Project},
				Type:        gojira.IssueType{Name: c.cfg.IssueType},
				Summary:     summary,
				Description: buildDescription(in),
				Labels:      []string{"televibe", "teleport-access", "tier-" + in.Tier},
			},
		}
		b, err := json.MarshalIndent(issue, "", "  ")
		if err != nil {
			return "", fmt.Errorf("marshal jira issue: %w", err)
		}
		fmt.Println("Jira request body:")
		fmt.Println(string(b))

		created, _, err := c.jira.Issue.Create(ctx, issue)
		if err != nil {
			return "", fmt.Errorf("create jira issue: %w", err)
		}
		if created == nil {
			return "", fmt.Errorf("jira returned nil issue without error")
		}

		return created.Key, nil
	}
*/
func (c *Client) CreateITSMTicket(ctx context.Context, in TicketInput) (string, error) {
	if c.cfg.Project == "" {
		return "", fmt.Errorf("jira project key is empty")
	}
	if c.cfg.IssueType == "" {
		return "", fmt.Errorf("jira issue type is empty")
	}
	if in.RequestID == "" {
		return "", fmt.Errorf("request id is empty")
	}

	labels := []string{"televibe", "teleport-access"}
	if in.Tier != "" {
		labels = append(labels, "tier-"+strings.ToLower(in.Tier))
	}
	if in.Escalation {
		labels = append(labels, "televibe-escalation")
	}

	summary := fmt.Sprintf("[televibe] Access request %s - %s (%s)", in.RequestID, in.Tier, in.Slang)
	if in.Escalation {
		summary = fmt.Sprintf("[televibe][escalation] Session escalation %s - %s (%s)", in.RequestID, in.Tier, in.Slang)
	}

	issue := &gojira.Issue{
		Fields: &gojira.IssueFields{
			Project:     gojira.Project{Key: c.cfg.Project},
			Type:        gojira.IssueType{Name: c.cfg.IssueType},
			Summary:     summary,
			Description: buildDescription(in),
			Labels:      labels,
		},
	}

	reqBody, err := json.MarshalIndent(issue, "", "  ")
	if err != nil {
		return "", fmt.Errorf("marshal jira issue: %w", err)
	}
	fmt.Println("Jira request body:")
	fmt.Println(string(reqBody))

	created, resp, err := c.jira.Issue.Create(ctx, issue)
	if err != nil {
		if resp != nil && resp.Body != nil {
			body, _ := io.ReadAll(resp.Body)
			fmt.Println("Jira error response:")
			fmt.Println(string(body))
		}
		return "", fmt.Errorf("create jira issue: %w", err)
	}

	if created == nil {
		return "", fmt.Errorf("jira returned nil issue without error")
	}

	return created.Key, nil
}

func (c *Client) TicketStatus(ctx context.Context, key string) (Status, error) {
	issue, _, err := c.jira.Issue.Get(ctx, key, nil)
	if err != nil {
		return StatusOpen, err
	}
	if issue.Fields == nil || issue.Fields.Status == nil {
		return StatusOpen, nil
	}
	switch issue.Fields.Status.Name {
	case c.cfg.ApprovedStatus:
		return StatusApproved, nil
	case c.cfg.RejectedStatus:
		return StatusRejected, nil
	}
	return StatusOpen, nil
}

func (c *Client) ListOpenTeleportTickets(ctx context.Context) (map[string]string, error) {
	jql := fmt.Sprintf(`project = %q AND labels = televibe AND status not in (%q, %q)`,
		c.cfg.Project, c.cfg.ApprovedStatus, c.cfg.RejectedStatus)
	issues, _, err := c.jira.Issue.Search(ctx, jql, nil)
	if err != nil {
		return nil, err
	}
	out := map[string]string{}
	for _, iss := range issues {
		if iss.Fields == nil {
			continue
		}
		m := c.reqIDRE.FindStringSubmatch(iss.Fields.Description)
		if len(m) < 2 {
			continue
		}
		out[m[1]] = iss.Key
	}
	return out, nil
}

func buildDescription(in TicketInput) string {
	var sb strings.Builder
	if in.Escalation {
		sb.WriteString("h2. Teleport Session Escalation Review\n\n")
		sb.WriteString("{panel:title=Session escalation}This request is a mid-session privilege bump — please prioritise review.{panel}\n\n")
	} else {
		sb.WriteString("h2. Teleport Access Request Review\n\n")
	}
	fmt.Fprintf(&sb, "* *Teleport Request ID:* %s\n", in.RequestID)
	fmt.Fprintf(&sb, "* *Requester:* %s\n", in.User)
	fmt.Fprintf(&sb, "* *Roles requested:* %s\n", strings.Join(in.Roles, ", "))
	fmt.Fprintf(&sb, "* *Risk score:* %d\n", in.Score)
	fmt.Fprintf(&sb, "* *Risk tier:* %s (%q)\n\n", in.Tier, in.Slang)

	sb.WriteString("h3. Request reason\n")
	sb.WriteString("{code}\n")
	sb.WriteString(in.Reason)
	sb.WriteString("\n{code}\n\n")

	if len(in.Components) > 0 {
		sb.WriteString("h3. Risk components\n")
		for k, v := range in.Components {
			fmt.Fprintf(&sb, "* %s: +%d\n", k, v)
		}
		sb.WriteString("\n")
	}

	if in.AIReasoning != "" {
		sb.WriteString("h3. AI reasoning\n")
		sb.WriteString("{quote}\n")
		sb.WriteString(in.AIReasoning)
		sb.WriteString("\n{quote}\n\n")
	}

	sb.WriteString("h3. SRE action required\n")
	sb.WriteString("Transition this ticket to the configured approved or rejected status to resolve the Teleport access request.\n")

	return sb.String()
}

Reason      string
Score       int
Tier        string
-  Slang       string
Components  map[string]int
AIReasoning string
Escalation  bool


/*
	func (c *Client) CreateITSMTicket(ctx context.Context, in TicketInput) (string, error) {
		summary := fmt.Sprintf("[televibe] Access request %s — %s (%s)", in.RequestID, in.Tier, in.Slang)
		issue := &gojira.Issue{
			Fields: &gojira.IssueFields{
				Project:     gojira.Project{Key: c.cfg.Project},
				Type:        gojira.IssueType{Name: c.cfg.IssueType},
				Summary:     summary,
				Description: buildDescription(in),
				Labels:      []string{"televibe", "teleport-access", "tier-" + in.Tier},
			},
		}
		b, err := json.MarshalIndent(issue, "", "  ")
		if err != nil {
			return "", fmt.Errorf("marshal jira issue: %w", err)
		}
		fmt.Println("Jira request body:")
		fmt.Println(string(b))

		created, _, err := c.jira.Issue.Create(ctx, issue)
		if err != nil {
			return "", fmt.Errorf("create jira issue: %w", err)
		}
		if created == nil {
			return "", fmt.Errorf("jira returned nil issue without error")
		}

		return created.Key, nil
	}
*/
func (c *Client) CreateITSMTicket(ctx context.Context, in TicketInput) (string, error) {

func (c *Client) CreateITSMTicket(ctx context.Context, in TicketInput) (string, error) {


package jira

import (
	"context"
	"encoding/json"
	"fmt"
	"io"
	"regexp"
	"strings"
	"time"

	gojira "github.com/andygrunwald/go-jira/v2/cloud"
)

type Config struct {
	URL            string        `toml:"url"`
	Username       string        `toml:"username"`
	APIToken       string        `toml:"api_token"`
	Project        string        `toml:"project"`
	IssueType      string        `toml:"issue_type"`
	PollInterval   time.Duration `toml:"poll_interval"`
	ApprovedStatus string        `toml:"approved_status"`
	RejectedStatus string        `toml:"rejected_status"`
}

func (c *Config) ApplyDefaults() {
	if c.IssueType == "" {
		c.IssueType = "Task"
	}
	if c.PollInterval == 0 {
		c.PollInterval = 30 * time.Second
	}
	if c.ApprovedStatus == "" {
		c.ApprovedStatus = "Approved"
	}
	if c.RejectedStatus == "" {
		c.RejectedStatus = "Rejected"
	}
}

func (c *Config) Validate() error {
	if c.URL == "" {
		return fmt.Errorf("url is required")
	}
	if c.Project == "" {
		return fmt.Errorf("project is required")
	}
	if c.Username == "" || c.APIToken == "" {
		return fmt.Errorf("username and api_token are required")
	}
	return nil
}

type Status int

const (
	StatusOpen Status = iota
	StatusApproved
	StatusRejected
)

func (s Status) String() string {
	switch s {
	case StatusApproved:
		return "approved"
	case StatusRejected:
		return "rejected"
	}
	return "open"
}

type TicketInput struct {
	RequestID   string
	User        string
	Roles       []string
	Reason      string
	Score       int
	Tier        string
	Components  map[string]int
	AIReasoning string
	Escalation  bool
}

type Client struct {
	cfg     Config
	jira    *gojira.Client
	reqIDRE *regexp.Regexp
}

func New(cfg Config) (*Client, error) {
	tp := gojira.BasicAuthTransport{Username: cfg.Username, APIToken: cfg.APIToken}
	jc, err := gojira.NewClient(cfg.URL, tp.Client())
	if err != nil {
		return nil, err
	}
	return &Client{
		cfg:     cfg,
		jira:    jc,
		reqIDRE: regexp.MustCompile(`Teleport Request ID:\s*([A-Za-z0-9-]+)`),
	}, nil
}

/*
	func (c *Client) CreateITSMTicket(ctx context.Context, in TicketInput) (string, error) {
		summary := fmt.Sprintf("[televibe] Access request %s — %s (%s)", in.RequestID, in.Tier, in.Slang)
		issue := &gojira.Issue{
			Fields: &gojira.IssueFields{
				Project:     gojira.Project{Key: c.cfg.Project},
				Type:        gojira.IssueType{Name: c.cfg.IssueType},
				Summary:     summary,
				Description: buildDescription(in),
				Labels:      []string{"televibe", "teleport-access", "tier-" + in.Tier},
			},
		}
		b, err := json.MarshalIndent(issue, "", "  ")
		if err != nil {
			return "", fmt.Errorf("marshal jira issue: %w", err)
		}
		fmt.Println("Jira request body:")
		fmt.Println(string(b))

		created, _, err := c.jira.Issue.Create(ctx, issue)
		if err != nil {
			return "", fmt.Errorf("create jira issue: %w", err)
		}
		if created == nil {
			return "", fmt.Errorf("jira returned nil issue without error")
		}

		return created.Key, nil
	}
*/
func (c *Client) CreateITSMTicket(ctx context.Context, in TicketInput) (string, error) {
	if c.cfg.Project == "" {
		return "", fmt.Errorf("jira project key is empty")
	}
	if c.cfg.IssueType == "" {
		return "", fmt.Errorf("jira issue type is empty")
	}
	if in.RequestID == "" {
		return "", fmt.Errorf("request id is empty")
	}

	labels := []string{"televibe", "teleport-access"}
	if in.Tier != "" {
		labels = append(labels, "tier-"+strings.ToLower(in.Tier))
	}
	if in.Escalation {
		labels = append(labels, "televibe-escalation")
	}

	summary := fmt.Sprintf("[televibe] Access request %s - %s (%s)", in.RequestID, in.Tier, in.Slang)
	if in.Escalation {
		summary = fmt.Sprintf("[televibe][escalation] Session escalation %s - %s (%s)", in.RequestID, in.Tier, in.Slang)
	}

	issue := &gojira.Issue{
		Fields: &gojira.IssueFields{
			Project:     gojira.Project{Key: c.cfg.Project},
			Type:        gojira.IssueType{Name: c.cfg.IssueType},
			Summary:     summary,
			Description: buildDescription(in),
			Labels:      labels,
		},
	}

	reqBody, err := json.MarshalIndent(issue, "", "  ")
	if err != nil {
		return "", fmt.Errorf("marshal jira issue: %w", err)
	}
	fmt.Println("Jira request body:")
	fmt.Println(string(reqBody))

	created, resp, err := c.jira.Issue.Create(ctx, issue)
	if err != nil {
		if resp != nil && resp.Body != nil {
			body, _ := io.ReadAll(resp.Body)
			fmt.Println("Jira error response:")
			fmt.Println(string(body))
		}
		return "", fmt.Errorf("create jira issue: %w", err)
	}

	if created == nil {
		return "", fmt.Errorf("jira returned nil issue without error")
	}

	return created.Key, nil
}

func (c *Client) TicketStatus(ctx context.Context, key string) (Status, error) {
	issue, _, err := c.jira.Issue.Get(ctx, key, nil)
	if err != nil {
		return StatusOpen, err
	}
	if issue.Fields == nil || issue.Fields.Status == nil {
		return StatusOpen, nil
	}
	switch issue.Fields.Status.Name {
	case c.cfg.ApprovedStatus:
		return StatusApproved, nil
	case c.cfg.RejectedStatus:
		return StatusRejected, nil
	}
	return StatusOpen, nil
}

func (c *Client) ListOpenTeleportTickets(ctx context.Context) (map[string]string, error) {
	jql := fmt.Sprintf(`project = %q AND labels = televibe AND status not in (%q, %q)`,
		c.cfg.Project, c.cfg.ApprovedStatus, c.cfg.RejectedStatus)
	issues, _, err := c.jira.Issue.Search(ctx, jql, nil)
	if err != nil {
		return nil, err
	}
	out := map[string]string{}
	for _, iss := range issues {
		if iss.Fields == nil {
			continue
		}
		m := c.reqIDRE.FindStringSubmatch(iss.Fields.Description)
		if len(m) < 2 {
			continue
		}
		out[m[1]] = iss.Key
	}
	return out, nil
}

func buildDescription(in TicketInput) string {
	var sb strings.Builder
	if in.Escalation {
		sb.WriteString("h2. Teleport Session Escalation Review\n\n")
		sb.WriteString("{panel:title=Session escalation}This request is a mid-session privilege bump — please prioritise review.{panel}\n\n")
	} else {
		sb.WriteString("h2. Teleport Access Request Review\n\n")
	}
	fmt.Fprintf(&sb, "* *Teleport Request ID:* %s\n", in.RequestID)
	fmt.Fprintf(&sb, "* *Requester:* %s\n", in.User)
	fmt.Fprintf(&sb, "* *Roles requested:* %s\n", strings.Join(in.Roles, ", "))
	fmt.Fprintf(&sb, "* *Risk score:* %d\n", in.Score)
	fmt.Fprintf(&sb, "* *Risk tier:* %s (%q)\n\n", in.Tier, in.Slang)

	sb.WriteString("h3. Request reason\n")
	sb.WriteString("{code}\n")
	sb.WriteString(in.Reason)
	sb.WriteString("\n{code}\n\n")

	if len(in.Components) > 0 {
		sb.WriteString("h3. Risk components\n")
		for k, v := range in.Components {
			fmt.Fprintf(&sb, "* %s: +%d\n", k, v)
		}
		sb.WriteString("\n")
	}

	if in.AIReasoning != "" {
		sb.WriteString("h3. AI reasoning\n")
		sb.WriteString("{quote}\n")
		sb.WriteString(in.AIReasoning)
		sb.WriteString("\n{quote}\n\n")
	}

	sb.WriteString("h3. SRE action required\n")
	sb.WriteString("Transition this ticket to the configured approved or rejected status to resolve the Teleport access request.\n")

	return sb.String()
}

-  func (c *Client) CreateITSMTicket(ctx context.Context, in TicketInput) (string, error) {

-    summary := fmt.Sprintf("[televibe] Access request %s — %s (%s)", in.RequestID, in.Tier, in.Slang)

-        Project:     gojira.Project{Key: c.cfg.Project},

-        Type:        gojira.IssueType{Name: c.cfg.IssueType},

-        Description: buildDescription(in),

-        Labels:      []string{"televibe", "teleport-access", "tier-" + in.Tier},

-    b, err := json.MarshalIndent(issue, "", "  ")

-      return "", fmt.Errorf("marshal jira issue: %w", err)

-    created, _, err := c.jira.Issue.Create(ctx, issue)

-      return "", fmt.Errorf("create jira issue: %w", err)

-      return "", fmt.Errorf("jira returned nil issue without error")


	labels := []string{"televibe", "teleport-access"}
	if in.Tier != "" {
		labels = append(labels, "tier-"+strings.ToLower(in.Tier))
	}
	if in.Escalation {
		labels = append(labels, "televibe-escalation")
	}

	summary := fmt.Sprintf("[televibe] Access request %s - %s (%s)", in.RequestID, in.Tier, in.Slang)
	if in.Escalation {
		summary = fmt.Sprintf("[televibe][escalation] Session escalation %s - %s (%s)", in.RequestID, in.Tier, in.Slang)
	}

	labels := []string{"teleport", "teleport-access"}
	if in.Tier != "" {
		labels = append(labels, "tier-"+strings.ToLower(in.Tier))
	}
	if in.Escalation {
		labels = append(labels, "teleport-escalation")
	}

	summary := fmt.Sprintf("[teleport] Access request %s - %s", in.RequestID, in.Tier)
	if in.Escalation {
		summary = fmt.Sprintf("[teleport][escalation] Session escalation %s - %s", in.RequestID, in.Tier)
	}


	labels := []string{"teleport", "teleport-access"}
	if in.Tier != "" {
		labels = append(labels, "tier-"+strings.ToLower(in.Tier))
	}
	if in.Escalation {
		labels = append(labels, "teleport-escalation")
	}

	summary := fmt.Sprintf("[teleport] Access request %s - %s", in.RequestID, in.Tier)
	if in.Escalation {
		summary = fmt.Sprintf("[teleport][escalation] Session escalation %s - %s", in.RequestID, in.Tier)
	}

package jira

import (
	"context"
	"encoding/json"
	"fmt"
	"io"
	"regexp"
	"strings"
	"time"

	gojira "github.com/andygrunwald/go-jira/v2/cloud"
)

type Config struct {
	URL            string        `toml:"url"`
	Username       string        `toml:"username"`
	APIToken       string        `toml:"api_token"`
	Project        string        `toml:"project"`
	IssueType      string        `toml:"issue_type"`
	PollInterval   time.Duration `toml:"poll_interval"`
	ApprovedStatus string        `toml:"approved_status"`
	RejectedStatus string        `toml:"rejected_status"`
}

func (c *Config) ApplyDefaults() {
	if c.IssueType == "" {
		c.IssueType = "Task"
	}
	if c.PollInterval == 0 {
		c.PollInterval = 30 * time.Second
	}
	if c.ApprovedStatus == "" {
		c.ApprovedStatus = "Approved"
	}
	if c.RejectedStatus == "" {
		c.RejectedStatus = "Rejected"
	}
}

func (c *Config) Validate() error {
	if c.URL == "" {
		return fmt.Errorf("url is required")
	}
	if c.Project == "" {
		return fmt.Errorf("project is required")
	}
	if c.Username == "" || c.APIToken == "" {
		return fmt.Errorf("username and api_token are required")
	}
	return nil
}

type Status int

const (
	StatusOpen Status = iota
	StatusApproved
	StatusRejected
)

func (s Status) String() string {
	switch s {
	case StatusApproved:
		return "approved"
	case StatusRejected:
		return "rejected"
	}
	return "open"
}

type TicketInput struct {
	RequestID   string
	User        string
	Roles       []string
	Reason      string
	Score       int
	Tier        string
	Components  map[string]int
	AIReasoning string
	Escalation  bool
}

type Client struct {
	cfg     Config
	jira    *gojira.Client
	reqIDRE *regexp.Regexp
}

func New(cfg Config) (*Client, error) {
	tp := gojira.BasicAuthTransport{Username: cfg.Username, APIToken: cfg.APIToken}
	jc, err := gojira.NewClient(cfg.URL, tp.Client())
	if err != nil {
		return nil, err
	}
	return &Client{
		cfg:     cfg,
		jira:    jc,
		reqIDRE: regexp.MustCompile(`Teleport Request ID:\s*([A-Za-z0-9-]+)`),
	}, nil
}

func (c *Client) CreateITSMTicket(ctx context.Context, in TicketInput) (string, error) {
	if c.cfg.Project == "" {
		return "", fmt.Errorf("jira project key is empty")
	}
	if c.cfg.IssueType == "" {
		return "", fmt.Errorf("jira issue type is empty")
	}
	if in.RequestID == "" {
		return "", fmt.Errorf("request id is empty")
	}

	labels := []string{"televibe", "teleport-access"}
	if in.Tier != "" {
		labels = append(labels, "tier-"+strings.ToLower(in.Tier))
	}
	if in.Escalation {
		labels = append(labels, "televibe-escalation")
	}

	summary := fmt.Sprintf("[televibe] Access request %s - %s (%s)", in.RequestID, in.Tier, in.Slang)
	if in.Escalation {
		summary = fmt.Sprintf("[televibe][escalation] Session escalation %s - %s (%s)", in.RequestID, in.Tier, in.Slang)
	}

	issue := &gojira.Issue{
		Fields: &gojira.IssueFields{
			Project:     gojira.Project{Key: c.cfg.Project},
			Type:        gojira.IssueType{Name: c.cfg.IssueType},
			Summary:     summary,
			Description: buildDescription(in),
			Labels:      labels,
		},
	}

	reqBody, err := json.MarshalIndent(issue, "", "  ")
	if err != nil {
		return "", fmt.Errorf("marshal jira issue: %w", err)
	}
	fmt.Println("Jira request body:")
	fmt.Println(string(reqBody))

	created, resp, err := c.jira.Issue.Create(ctx, issue)
	if err != nil {
		if resp != nil && resp.Body != nil {
			body, _ := io.ReadAll(resp.Body)
			fmt.Println("Jira error response:")
			fmt.Println(string(body))
		}
		return "", fmt.Errorf("create jira issue: %w", err)
	}

	if created == nil {
		return "", fmt.Errorf("jira returned nil issue without error")
	}

	return created.Key, nil
}

func (c *Client) TicketStatus(ctx context.Context, key string) (Status, error) {
	issue, _, err := c.jira.Issue.Get(ctx, key, nil)
	if err != nil {
		return StatusOpen, err
	}
	if issue.Fields == nil || issue.Fields.Status == nil {
		return StatusOpen, nil
	}
	switch issue.Fields.Status.Name {
	case c.cfg.ApprovedStatus:
		return StatusApproved, nil
	case c.cfg.RejectedStatus:
		return StatusRejected, nil
	}
	return StatusOpen, nil
}

func (c *Client) ListOpenTeleportTickets(ctx context.Context) (map[string]string, error) {
	jql := fmt.Sprintf(`project = %q AND labels = televibe AND status not in (%q, %q)`,
		c.cfg.Project, c.cfg.ApprovedStatus, c.cfg.RejectedStatus)
	issues, _, err := c.jira.Issue.Search(ctx, jql, nil)
	if err != nil {
		return nil, err
	}
	out := map[string]string{}
	for _, iss := range issues {
		if iss.Fields == nil {
			continue
		}
		m := c.reqIDRE.FindStringSubmatch(iss.Fields.Description)
		if len(m) < 2 {
			continue
		}
		out[m[1]] = iss.Key
	}
	return out, nil
}

func buildDescription(in TicketInput) string {
	var sb strings.Builder
	if in.Escalation {
		sb.WriteString("h2. Teleport Session Escalation Review\n\n")
		sb.WriteString("{panel:title=Session escalation}This request is a mid-session privilege bump — please prioritise review.{panel}\n\n")
	} else {
		sb.WriteString("h2. Teleport Access Request Review\n\n")
	}
	fmt.Fprintf(&sb, "* *Teleport Request ID:* %s\n", in.RequestID)
	fmt.Fprintf(&sb, "* *Requester:* %s\n", in.User)
	fmt.Fprintf(&sb, "* *Roles requested:* %s\n", strings.Join(in.Roles, ", "))
	fmt.Fprintf(&sb, "* *Risk score:* %d\n", in.Score)
	fmt.Fprintf(&sb, "* *Risk tier:* %s (%q)\n\n", in.Tier, in.Slang)

	sb.WriteString("h3. Request reason\n")
	sb.WriteString("{code}\n")
	sb.WriteString(in.Reason)
	sb.WriteString("\n{code}\n\n")

	if len(in.Components) > 0 {
		sb.WriteString("h3. Risk components\n")
		for k, v := range in.Components {
			fmt.Fprintf(&sb, "* %s: +%d\n", k, v)
		}
		sb.WriteString("\n")
	}

	if in.AIReasoning != "" {
		sb.WriteString("h3. AI reasoning\n")
		sb.WriteString("{quote}\n")
		sb.WriteString(in.AIReasoning)
		sb.WriteString("\n{quote}\n\n")
	}

	sb.WriteString("h3. SRE action required\n")
	sb.WriteString("Transition this ticket to the configured approved or rejected status to resolve the Teleport access request.\n")

	return sb.String()
}

-  labels := []string{"televibe", "teleport-access"}

+  labels := []string{"teleport", "teleport-access"}

-    labels = append(labels, "televibe-escalation")

+    labels = append(labels, "teleport-escalation")

-  summary := fmt.Sprintf("[televibe] Access request %s - %s (%s)", in.RequestID, in.Tier, in.Slang)

+  summary := fmt.Sprintf("[teleport] Access request %s - %s", in.RequestID, in.Tier)

-    summary = fmt.Sprintf("[televibe][escalation] Session escalation %s - %s (%s)", in.RequestID, in.Tier, in.Slang)

+    summary = fmt.Sprintf("[teleport][escalation] Session escalation %s - %s", in.RequestID, in.Tier)


fmt.Fprintf(&sb, "* *Risk tier:* %s (%q)\n\n", in.Tier, in.Slang)

fmt.Fprintf(&sb, "* *Risk tier:* %s\n\n", in.Tier)


package jira

import (
	"context"
	"encoding/json"
	"fmt"
	"io"
	"regexp"
	"strings"
	"time"

	gojira "github.com/andygrunwald/go-jira/v2/cloud"
)

type Config struct {
	URL            string        `toml:"url"`
	Username       string        `toml:"username"`
	APIToken       string        `toml:"api_token"`
	Project        string        `toml:"project"`
	IssueType      string        `toml:"issue_type"`
	PollInterval   time.Duration `toml:"poll_interval"`
	ApprovedStatus string        `toml:"approved_status"`
	RejectedStatus string        `toml:"rejected_status"`
}

func (c *Config) ApplyDefaults() {
	if c.IssueType == "" {
		c.IssueType = "Task"
	}
	if c.PollInterval == 0 {
		c.PollInterval = 30 * time.Second
	}
	if c.ApprovedStatus == "" {
		c.ApprovedStatus = "Approved"
	}
	if c.RejectedStatus == "" {
		c.RejectedStatus = "Rejected"
	}
}

func (c *Config) Validate() error {
	if c.URL == "" {
		return fmt.Errorf("url is required")
	}
	if c.Project == "" {
		return fmt.Errorf("project is required")
	}
	if c.Username == "" || c.APIToken == "" {
		return fmt.Errorf("username and api_token are required")
	}
	return nil
}

type Status int

const (
	StatusOpen Status = iota
	StatusApproved
	StatusRejected
)

func (s Status) String() string {
	switch s {
	case StatusApproved:
		return "approved"
	case StatusRejected:
		return "rejected"
	}
	return "open"
}

type TicketInput struct {
	RequestID   string
	User        string
	Roles       []string
	Reason      string
	Score       int
	Tier        string
	Components  map[string]int
	AIReasoning string
	Escalation  bool
}

type Client struct {
	cfg     Config
	jira    *gojira.Client
	reqIDRE *regexp.Regexp
}

func New(cfg Config) (*Client, error) {
	tp := gojira.BasicAuthTransport{Username: cfg.Username, APIToken: cfg.APIToken}
	jc, err := gojira.NewClient(cfg.URL, tp.Client())
	if err != nil {
		return nil, err
	}
	return &Client{
		cfg:     cfg,
		jira:    jc,
		reqIDRE: regexp.MustCompile(`Teleport Request ID:\s*([A-Za-z0-9-]+)`),
	}, nil
}

func (c *Client) CreateITSMTicket(ctx context.Context, in TicketInput) (string, error) {
	if c.cfg.Project == "" {
		return "", fmt.Errorf("jira project key is empty")
	}
	if c.cfg.IssueType == "" {
		return "", fmt.Errorf("jira issue type is empty")
	}
	if in.RequestID == "" {
		return "", fmt.Errorf("request id is empty")
	}

	labels := []string{"teleport", "teleport-access"}
	if in.Tier != "" {
		labels = append(labels, "tier-"+strings.ToLower(in.Tier))
	}
	if in.Escalation {
		labels = append(labels, "teleport-escalation")
	}

	summary := fmt.Sprintf("[teleport] Access request %s - %s", in.RequestID, in.Tier)
	if in.Escalation {
		summary = fmt.Sprintf("[teleport][escalation] Session escalation %s - %s", in.RequestID, in.Tier)
	}

	issue := &gojira.Issue{
		Fields: &gojira.IssueFields{
			Project:     gojira.Project{Key: c.cfg.Project},
			Type:        gojira.IssueType{Name: c.cfg.IssueType},
			Summary:     summary,
			Description: buildDescription(in),
			Labels:      labels,
		},
	}

	reqBody, err := json.MarshalIndent(issue, "", "  ")
	if err != nil {
		return "", fmt.Errorf("marshal jira issue: %w", err)
	}
	fmt.Println("Jira request body:")
	fmt.Println(string(reqBody))

	created, resp, err := c.jira.Issue.Create(ctx, issue)
	if err != nil {
		if resp != nil && resp.Body != nil {
			body, _ := io.ReadAll(resp.Body)
			fmt.Println("Jira error response:")
			fmt.Println(string(body))
		}
		return "", fmt.Errorf("create jira issue: %w", err)
	}

	if created == nil {
		return "", fmt.Errorf("jira returned nil issue without error")
	}

	return created.Key, nil
}

func (c *Client) TicketStatus(ctx context.Context, key string) (Status, error) {
	issue, _, err := c.jira.Issue.Get(ctx, key, nil)
	if err != nil {
		return StatusOpen, err
	}
	if issue.Fields == nil || issue.Fields.Status == nil {
		return StatusOpen, nil
	}
	switch issue.Fields.Status.Name {
	case c.cfg.ApprovedStatus:
		return StatusApproved, nil
	case c.cfg.RejectedStatus:
		return StatusRejected, nil
	}
	return StatusOpen, nil
}

func (c *Client) ListOpenTeleportTickets(ctx context.Context) (map[string]string, error) {
	jql := fmt.Sprintf(`project = %q AND labels = televibe AND status not in (%q, %q)`,
		c.cfg.Project, c.cfg.ApprovedStatus, c.cfg.RejectedStatus)
	issues, _, err := c.jira.Issue.Search(ctx, jql, nil)
	if err != nil {
		return nil, err
	}
	out := map[string]string{}
	for _, iss := range issues {
		if iss.Fields == nil {
			continue
		}
		m := c.reqIDRE.FindStringSubmatch(iss.Fields.Description)
		if len(m) < 2 {
			continue
		}
		out[m[1]] = iss.Key
	}
	return out, nil
}

func buildDescription(in TicketInput) string {
	var sb strings.Builder
	if in.Escalation {
		sb.WriteString("h2. Teleport Session Escalation Review\n\n")
		sb.WriteString("{panel:title=Session escalation}This request is a mid-session privilege bump — please prioritise review.{panel}\n\n")
	} else {
		sb.WriteString("h2. Teleport Access Request Review\n\n")
	}
	fmt.Fprintf(&sb, "* *Teleport Request ID:* %s\n", in.RequestID)
	fmt.Fprintf(&sb, "* *Requester:* %s\n", in.User)
	fmt.Fprintf(&sb, "* *Roles requested:* %s\n", strings.Join(in.Roles, ", "))
	fmt.Fprintf(&sb, "* *Risk score:* %d\n", in.Score)
	fmt.Fprintf(&sb, "* *Risk tier:* %s (%q)\n\n", in.Tier, in.Slang)

	sb.WriteString("h3. Request reason\n")
	sb.WriteString("{code}\n")
	sb.WriteString(in.Reason)
	sb.WriteString("\n{code}\n\n")

	if len(in.Components) > 0 {
		sb.WriteString("h3. Risk components\n")
		for k, v := range in.Components {
			fmt.Fprintf(&sb, "* %s: +%d\n", k, v)
		}
		sb.WriteString("\n")
	}

	if in.AIReasoning != "" {
		sb.WriteString("h3. AI reasoning\n")
		sb.WriteString("{quote}\n")
		sb.WriteString(in.AIReasoning)
		sb.WriteString("\n{quote}\n\n")
	}

	sb.WriteString("h3. SRE action required\n")
	sb.WriteString("Transition this ticket to the configured approved or rejected status to resolve the Teleport access request.\n")

	return sb.String()
}

-  fmt.Fprintf(&sb, "* *Risk tier:* %s (%q)\n\n", in.Tier, in.Slang)

+  fmt.Fprintf(&sb, "* *Risk tier:* %s\n\n", in.Tier)


jql := fmt.Sprintf(`project = %q AND labels = televibe AND status not in (%q, %q)`,

jql := fmt.Sprintf(`project = %q AND labels = teleport AND status not in (%q, %q)`,


package jira

import (
	"context"
	"encoding/json"
	"fmt"
	"io"
	"regexp"
	"strings"
	"time"

	gojira "github.com/andygrunwald/go-jira/v2/cloud"
)

type Config struct {
	URL            string        `toml:"url"`
	Username       string        `toml:"username"`
	APIToken       string        `toml:"api_token"`
	Project        string        `toml:"project"`
	IssueType      string        `toml:"issue_type"`
	PollInterval   time.Duration `toml:"poll_interval"`
	ApprovedStatus string        `toml:"approved_status"`
	RejectedStatus string        `toml:"rejected_status"`
}

func (c *Config) ApplyDefaults() {
	if c.IssueType == "" {
		c.IssueType = "Task"
	}
	if c.PollInterval == 0 {
		c.PollInterval = 30 * time.Second
	}
	if c.ApprovedStatus == "" {
		c.ApprovedStatus = "Approved"
	}
	if c.RejectedStatus == "" {
		c.RejectedStatus = "Rejected"
	}
}

func (c *Config) Validate() error {
	if c.URL == "" {
		return fmt.Errorf("url is required")
	}
	if c.Project == "" {
		return fmt.Errorf("project is required")
	}
	if c.Username == "" || c.APIToken == "" {
		return fmt.Errorf("username and api_token are required")
	}
	return nil
}

type Status int

const (
	StatusOpen Status = iota
	StatusApproved
	StatusRejected
)

func (s Status) String() string {
	switch s {
	case StatusApproved:
		return "approved"
	case StatusRejected:
		return "rejected"
	}
	return "open"
}

type TicketInput struct {
	RequestID   string
	User        string
	Roles       []string
	Reason      string
	Score       int
	Tier        string
	Components  map[string]int
	AIReasoning string
	Escalation  bool
}

type Client struct {
	cfg     Config
	jira    *gojira.Client
	reqIDRE *regexp.Regexp
}

func New(cfg Config) (*Client, error) {
	tp := gojira.BasicAuthTransport{Username: cfg.Username, APIToken: cfg.APIToken}
	jc, err := gojira.NewClient(cfg.URL, tp.Client())
	if err != nil {
		return nil, err
	}
	return &Client{
		cfg:     cfg,
		jira:    jc,
		reqIDRE: regexp.MustCompile(`Teleport Request ID:\s*([A-Za-z0-9-]+)`),
	}, nil
}

func (c *Client) CreateITSMTicket(ctx context.Context, in TicketInput) (string, error) {
	if c.cfg.Project == "" {
		return "", fmt.Errorf("jira project key is empty")
	}
	if c.cfg.IssueType == "" {
		return "", fmt.Errorf("jira issue type is empty")
	}
	if in.RequestID == "" {
		return "", fmt.Errorf("request id is empty")
	}

	labels := []string{"teleport", "teleport-access"}
	if in.Tier != "" {
		labels = append(labels, "tier-"+strings.ToLower(in.Tier))
	}
	if in.Escalation {
		labels = append(labels, "teleport-escalation")
	}

	summary := fmt.Sprintf("[teleport] Access request %s - %s", in.RequestID, in.Tier)
	if in.Escalation {
		summary = fmt.Sprintf("[teleport][escalation] Session escalation %s - %s", in.RequestID, in.Tier)
	}

	issue := &gojira.Issue{
		Fields: &gojira.IssueFields{
			Project:     gojira.Project{Key: c.cfg.Project},
			Type:        gojira.IssueType{Name: c.cfg.IssueType},
			Summary:     summary,
			Description: buildDescription(in),
			Labels:      labels,
		},
	}

	reqBody, err := json.MarshalIndent(issue, "", "  ")
	if err != nil {
		return "", fmt.Errorf("marshal jira issue: %w", err)
	}
	fmt.Println("Jira request body:")
	fmt.Println(string(reqBody))

	created, resp, err := c.jira.Issue.Create(ctx, issue)
	if err != nil {
		if resp != nil && resp.Body != nil {
			body, _ := io.ReadAll(resp.Body)
			fmt.Println("Jira error response:")
			fmt.Println(string(body))
		}
		return "", fmt.Errorf("create jira issue: %w", err)
	}

	if created == nil {
		return "", fmt.Errorf("jira returned nil issue without error")
	}

	return created.Key, nil
}

func (c *Client) TicketStatus(ctx context.Context, key string) (Status, error) {
	issue, _, err := c.jira.Issue.Get(ctx, key, nil)
	if err != nil {
		return StatusOpen, err
	}
	if issue.Fields == nil || issue.Fields.Status == nil {
		return StatusOpen, nil
	}
	switch issue.Fields.Status.Name {
	case c.cfg.ApprovedStatus:
		return StatusApproved, nil
	case c.cfg.RejectedStatus:
		return StatusRejected, nil
	}
	return StatusOpen, nil
}

func (c *Client) ListOpenTeleportTickets(ctx context.Context) (map[string]string, error) {
	jql := fmt.Sprintf(`project = %q AND labels = televibe AND status not in (%q, %q)`,
		c.cfg.Project, c.cfg.ApprovedStatus, c.cfg.RejectedStatus)
	issues, _, err := c.jira.Issue.Search(ctx, jql, nil)
	if err != nil {
		return nil, err
	}
	out := map[string]string{}
	for _, iss := range issues {
		if iss.Fields == nil {
			continue
		}
		m := c.reqIDRE.FindStringSubmatch(iss.Fields.Description)
		if len(m) < 2 {
			continue
		}
		out[m[1]] = iss.Key
	}
	return out, nil
}

func buildDescription(in TicketInput) string {
	var sb strings.Builder
	if in.Escalation {
		sb.WriteString("h2. Teleport Session Escalation Review\n\n")
		sb.WriteString("{panel:title=Session escalation}This request is a mid-session privilege bump — please prioritise review.{panel}\n\n")
	} else {
		sb.WriteString("h2. Teleport Access Request Review\n\n")
	}
	fmt.Fprintf(&sb, "* *Teleport Request ID:* %s\n", in.RequestID)
	fmt.Fprintf(&sb, "* *Requester:* %s\n", in.User)
	fmt.Fprintf(&sb, "* *Roles requested:* %s\n", strings.Join(in.Roles, ", "))
	fmt.Fprintf(&sb, "* *Risk score:* %d\n", in.Score)
	fmt.Fprintf(&sb, "* *Risk tier:* %s\n\n", in.Tier)

	sb.WriteString("h3. Request reason\n")
	sb.WriteString("{code}\n")
	sb.WriteString(in.Reason)
	sb.WriteString("\n{code}\n\n")

	if len(in.Components) > 0 {
		sb.WriteString("h3. Risk components\n")
		for k, v := range in.Components {
			fmt.Fprintf(&sb, "* %s: +%d\n", k, v)
		}
		sb.WriteString("\n")
	}

	if in.AIReasoning != "" {
		sb.WriteString("h3. AI reasoning\n")
		sb.WriteString("{quote}\n")
		sb.WriteString(in.AIReasoning)
		sb.WriteString("\n{quote}\n\n")
	}

	sb.WriteString("h3. SRE action required\n")
	sb.WriteString("Transition this ticket to the configured approved or rejected status to resolve the Teleport access request.\n")

	return sb.String()
}

-  jql := fmt.Sprintf(`project = %q AND labels = televibe AND status not in (%q, %q)`,

+  jql := fmt.Sprintf(`project = %q AND labels = teleport AND status not in (%q, %q)`,


Bash

Build and run all tests with race detector


?   	televibe	[no test files]
?   	televibe/ai	[no test files]
?   	televibe/cmd/submit	[no test files]
?   	televibe/cmd/televibe	[no test files]
?   	televibe/jira	[no test files]
ok  	televibe/reputation	(cached)
ok  	televibe/risk	1.855s
?   	televibe/telemetry	[no test files]


grep -rn "Slang\|slang\|no cap\|mad sus\|straight L\|touch grass\|\"sus\"" --include="*.go" . 2>&1; echo '---jira televibe---'; grep -n "televibe" /Users/steve.briggs/projects/operations/apps/televibe/jira/itsm.go 2>&1

Verify no slang left and no televibe in jira/itsm.go


---jira televibe---


Clean — zero slang anywhere, zero `televibe` strings in `jira/itsm.go`, build is green, `-race` tests pass.

Summary:
- `risk/risk.go:23-32` — dropped `Tier.Slang` field and the slang values from each tier.
- `app.go` — removed `"slang"` log fields and the slang placeholder from the auto-approved review reason and the `jira.TicketInput`.
- `jira/itsm.go` — dropped `TicketInput.Slang`, deleted the dead commented-out old `CreateITSMTicket`, Jira summary prefixes are now `[teleport]` / `[teleport][escalation]`, labels are `teleport` / `teleport-escalation`, description's risk-tier line no longer prints a slang string, and the rehydrate JQL now matches on `labels = teleport`.
- Left deliberately alone (not Jira-facing): `slog.Info("televibe starting")`, Teleport review `Author: "televibe"` and review `reason` strings, the `televibe.escalation` annotation key, and the `televibe` telemetry service name.

One migration note, Sir Charles: the JQL label change means any tickets created before this diff (labelled `televibe`) won't be picked up by `rehydrate` on the next restart. If you have live pending tickets, reapply the `teleport` label before redeploying or they'll drift.


for the AI review i want to use AWS bedrock, I plan to use primarily IRSA but would like to be able to configure it to use other authentication if possible what options do we have here?


ls /Users/steve.briggs/projects/operations/apps/televibe/ai/ && echo '---'; grep -n "bedrock\|Bedrock\|BEDROCK" /Users/steve.briggs/projects/operations/apps/televibe/ai/*.go 2>&1 | head -30

Inspect ai package