diff --git a/internal/examples/self_development_test.go b/internal/examples/self_development_test.go index 3a401e2a..e9553b21 100644 --- a/internal/examples/self_development_test.go +++ b/internal/examples/self_development_test.go @@ -14,6 +14,7 @@ import ( batchv1 "k8s.io/api/batch/v1" corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" yamlutil "k8s.io/apimachinery/pkg/util/yaml" sigyaml "sigs.k8s.io/yaml" @@ -105,13 +106,11 @@ func TestSelfDevelopmentRoleAgentConfigsDoNotDuplicateBaseSkills(t *testing.T) { {dir: "self-development/agora", file: "agora-fake-user.yaml"}, {dir: "self-development/agora", file: "agora-planner.yaml"}, {dir: "self-development/agora", file: "agora-reviewer.yaml"}, - {dir: "self-development/agora", file: "agora-workers.yaml"}, {dir: "self-development/kanon", file: "agentconfig.yaml"}, {dir: "self-development/kanon", file: "kanon-fake-strategist.yaml"}, {dir: "self-development/kanon", file: "kanon-fake-user.yaml"}, {dir: "self-development/kanon", file: "kanon-planner.yaml"}, {dir: "self-development/kanon", file: "kanon-reviewer.yaml"}, - {dir: "self-development/kanon", file: "kanon-workers.yaml"}, } for _, tt := range files { @@ -143,7 +142,7 @@ func TestSelfDevelopmentSpawnersUseBaseAgent(t *testing.T) { {dir: "self-development", file: "kelos-glm-reviewer.yaml", refs: []kelos.AgentConfigReference{{Name: "base-agent"}, {Name: "kelos-glm-reviewer-agent"}}}, {dir: "self-development", file: "kelos-image-update.yaml", refs: []kelos.AgentConfigReference{{Name: "base-agent"}, {Name: "kelos-image-update-agent"}}}, {dir: "self-development", file: "kelos-planner.yaml", refs: []kelos.AgentConfigReference{{Name: "base-agent"}, {Name: "kelos-planner-agent"}}}, - {dir: "self-development", file: "kelos-pr-responder.yaml", refs: []kelos.AgentConfigReference{{Name: "base-agent"}, {Name: "kelos-dev-agent"}}}, + {dir: "self-development", file: "kelos-pr-responder.yaml", refs: []kelos.AgentConfigReference{{Name: "base-agent"}}}, {dir: "self-development", file: "kelos-reviewer.yaml", refs: []kelos.AgentConfigReference{{Name: "base-agent"}, {Name: "kelos-reviewer-agent"}}}, {dir: "self-development", file: "kelos-self-update.yaml", refs: []kelos.AgentConfigReference{{Name: "base-agent"}, {Name: "kelos-self-update-agent"}}}, {dir: "self-development", file: "kelos-squash-commits.yaml", refs: []kelos.AgentConfigReference{{Name: "base-agent"}, {Name: "kelos-dev-agent"}}}, @@ -153,22 +152,22 @@ func TestSelfDevelopmentSpawnersUseBaseAgent(t *testing.T) { {dir: "self-development/agora", file: "agora-fake-strategist.yaml", refs: []kelos.AgentConfigReference{{Name: "base-agent"}, {Name: "agora-fake-strategist-agent"}}}, {dir: "self-development/agora", file: "agora-fake-user.yaml", refs: []kelos.AgentConfigReference{{Name: "base-agent"}, {Name: "agora-fake-user-agent"}}}, {dir: "self-development/agora", file: "agora-planner.yaml", refs: []kelos.AgentConfigReference{{Name: "base-agent"}, {Name: "agora-planner-agent"}}}, - {dir: "self-development/agora", file: "agora-pr-responder.yaml", refs: []kelos.AgentConfigReference{{Name: "base-agent"}, {Name: "agora-dev-agent"}}}, + {dir: "self-development/agora", file: "agora-pr-responder.yaml", refs: []kelos.AgentConfigReference{{Name: "base-agent"}}}, {dir: "self-development/agora", file: "agora-reviewer.yaml", refs: []kelos.AgentConfigReference{{Name: "base-agent"}, {Name: "agora-reviewer-agent"}}}, {dir: "self-development/agora", file: "agora-self-update.yaml", refs: []kelos.AgentConfigReference{{Name: "base-agent"}, {Name: "kelos-dev-agent"}}}, {dir: "self-development/agora", file: "agora-squash-commits.yaml", refs: []kelos.AgentConfigReference{{Name: "base-agent"}, {Name: "agora-dev-agent"}}}, {dir: "self-development/agora", file: "agora-triage.yaml", refs: []kelos.AgentConfigReference{{Name: "base-agent"}, {Name: "agora-dev-agent"}}}, - {dir: "self-development/agora", file: "agora-workers.yaml", refs: []kelos.AgentConfigReference{{Name: "base-agent"}, {Name: "agora-workers-agent"}}}, + {dir: "self-development/agora", file: "agora-workers.yaml", refs: []kelos.AgentConfigReference{{Name: "base-agent"}}}, {dir: "self-development/kanon", file: "kanon-config-update.yaml", refs: []kelos.AgentConfigReference{{Name: "base-agent"}, {Name: "kelos-dev-agent"}}}, {dir: "self-development/kanon", file: "kanon-fake-strategist.yaml", refs: []kelos.AgentConfigReference{{Name: "base-agent"}, {Name: "kanon-fake-strategist-agent"}}}, {dir: "self-development/kanon", file: "kanon-fake-user.yaml", refs: []kelos.AgentConfigReference{{Name: "base-agent"}, {Name: "kanon-fake-user-agent"}}}, {dir: "self-development/kanon", file: "kanon-planner.yaml", refs: []kelos.AgentConfigReference{{Name: "base-agent"}, {Name: "kanon-planner-agent"}}}, - {dir: "self-development/kanon", file: "kanon-pr-responder.yaml", refs: []kelos.AgentConfigReference{{Name: "base-agent"}, {Name: "kanon-dev-agent"}}}, + {dir: "self-development/kanon", file: "kanon-pr-responder.yaml", refs: []kelos.AgentConfigReference{{Name: "base-agent"}}}, {dir: "self-development/kanon", file: "kanon-reviewer.yaml", refs: []kelos.AgentConfigReference{{Name: "base-agent"}, {Name: "kanon-reviewer-agent"}}}, {dir: "self-development/kanon", file: "kanon-self-update.yaml", refs: []kelos.AgentConfigReference{{Name: "base-agent"}, {Name: "kelos-dev-agent"}}}, {dir: "self-development/kanon", file: "kanon-squash-commits.yaml", refs: []kelos.AgentConfigReference{{Name: "base-agent"}, {Name: "kanon-dev-agent"}}}, {dir: "self-development/kanon", file: "kanon-triage.yaml", refs: []kelos.AgentConfigReference{{Name: "base-agent"}, {Name: "kanon-dev-agent"}}}, - {dir: "self-development/kanon", file: "kanon-workers.yaml", refs: []kelos.AgentConfigReference{{Name: "base-agent"}, {Name: "kanon-workers-agent"}}}, + {dir: "self-development/kanon", file: "kanon-workers.yaml", refs: []kelos.AgentConfigReference{{Name: "base-agent"}}}, } for _, tt := range tests { tt := tt @@ -204,7 +203,6 @@ func TestDevelopmentTaskSpawnersIgnoreDisruptions(t *testing.T) { {dir: "self-development", file: "kelos-glm-reviewer.yaml"}, {dir: "self-development", file: "kelos-image-update.yaml"}, {dir: "self-development", file: "kelos-planner.yaml"}, - {dir: "self-development", file: "kelos-pr-responder.yaml"}, {dir: "self-development", file: "kelos-reviewer.yaml"}, {dir: "self-development", file: "kelos-self-update.yaml"}, {dir: "self-development", file: "kelos-squash-commits.yaml"}, @@ -213,12 +211,10 @@ func TestDevelopmentTaskSpawnersIgnoreDisruptions(t *testing.T) { {dir: "self-development/kanon", file: "kanon-fake-strategist.yaml"}, {dir: "self-development/kanon", file: "kanon-fake-user.yaml"}, {dir: "self-development/kanon", file: "kanon-planner.yaml"}, - {dir: "self-development/kanon", file: "kanon-pr-responder.yaml"}, {dir: "self-development/kanon", file: "kanon-reviewer.yaml"}, {dir: "self-development/kanon", file: "kanon-self-update.yaml"}, {dir: "self-development/kanon", file: "kanon-squash-commits.yaml"}, {dir: "self-development/kanon", file: "kanon-triage.yaml"}, - {dir: "self-development/kanon", file: "kanon-workers.yaml"}, } for _, tt := range tests { @@ -232,108 +228,145 @@ func TestDevelopmentTaskSpawnersIgnoreDisruptions(t *testing.T) { } } -func TestDevelopmentSessionSpawnerUsesPersistentWorkspace(t *testing.T) { +func TestDevelopmentSessionSpawnersUsePersistentWorkspace(t *testing.T) { t.Parallel() - _, spawner := readSpawnerFromDir(t, "self-development", "kelos-workers.yaml") - if spawner == nil { - t.Fatal("SessionSpawner is nil") - } - if spawner.Spec.SessionTemplate.VolumeClaimTemplate == nil { - t.Fatal("SessionSpawner sessionTemplate.volumeClaimTemplate is nil") - } - if spawner.Spec.SessionTemplate.InitialPrompt == "" { - t.Fatal("SessionSpawner sessionTemplate.initialPrompt is empty") - } - if !strings.Contains(spawner.Spec.SessionTemplate.InitialPrompt, "Your workspace is backed by a PVC") { - t.Fatal("SessionSpawner initialPrompt does not describe its persistent workspace") + tests := []struct { + dir string + file string + }{ + {dir: "self-development", file: "kelos-workers.yaml"}, + {dir: "self-development", file: "kelos-pr-responder.yaml"}, + {dir: "self-development/agora", file: "agora-workers.yaml"}, + {dir: "self-development/agora", file: "agora-pr-responder.yaml"}, + {dir: "self-development/kanon", file: "kanon-workers.yaml"}, + {dir: "self-development/kanon", file: "kanon-pr-responder.yaml"}, } - wantAgentConfigs := []kelos.AgentConfigReference{{Name: "base-agent"}} - if !reflect.DeepEqual(spawner.Spec.SessionTemplate.Worker.AgentConfigRefs, wantAgentConfigs) { - t.Fatalf("SessionSpawner agentConfigRefs = %v, want %v", spawner.Spec.SessionTemplate.Worker.AgentConfigRefs, wantAgentConfigs) + for _, tt := range tests { + tt := tt + t.Run(tt.dir+"/"+tt.file, func(t *testing.T) { + t.Parallel() + + sessionSpawner := readSessionSpawnerFromDir(t, tt.dir, tt.file) + if sessionSpawner.Spec.SessionTemplate.VolumeClaimTemplate == nil { + t.Fatalf("%s sessionTemplate.volumeClaimTemplate is nil", tt.file) + } + claim := sessionSpawner.Spec.SessionTemplate.VolumeClaimTemplate + if !reflect.DeepEqual(claim.AccessModes, []corev1.PersistentVolumeAccessMode{corev1.ReadWriteOnce}) { + t.Fatalf("%s volumeClaimTemplate.accessModes = %v, want [ReadWriteOnce]", tt.file, claim.AccessModes) + } + if storage := claim.Resources.Requests[corev1.ResourceStorage]; storage.Cmp(resource.MustParse("10Gi")) != 0 { + t.Fatalf("%s volumeClaimTemplate storage = %s, want 10Gi", tt.file, storage.String()) + } + if sessionSpawner.Spec.SessionTemplate.InitialPrompt == "" { + t.Fatalf("%s sessionTemplate.initialPrompt is empty", tt.file) + } + if !strings.Contains(sessionSpawner.Spec.SessionTemplate.InitialPrompt, "Your workspace is backed by a PVC") { + t.Fatalf("%s initialPrompt does not describe its persistent workspace", tt.file) + } + }) } } -func TestDevelopmentSessionSpawnerMatchesEveryIssueAndPullRequestComment(t *testing.T) { +func TestDevelopmentSessionSpawnersMatchPickUpOnly(t *testing.T) { t.Parallel() - _, sessionSpawner := readSpawnerFromDir(t, "self-development", "kelos-workers.yaml") - if sessionSpawner == nil { - t.Fatal("SessionSpawner is nil") - } - spawner := sessionSpawner.Spec.When.GitHubWebhook - if spawner == nil { - t.Fatal("SessionSpawner spec.when.githubWebhook is nil") - } - if !reflect.DeepEqual(spawner.ExcludeAuthors, []string{"kelos-bot[bot]"}) { - t.Fatalf("excludeAuthors = %v, want [kelos-bot[bot]]", spawner.ExcludeAuthors) + wantFilter := kelos.GitHubWebhookFilter{ + Event: "issue_comment", + Action: "created", + BodyPattern: `(?m)^/kelos pick-up[ \t]*\r?$`, + CommentOn: kelos.CommentOnIssue, + State: "open", + Author: "gjkim42", } - if len(spawner.Filters) != 1 { - t.Fatalf("filters length = %d, want 1", len(spawner.Filters)) + spawners := []struct { + dir string + file string + }{ + {dir: "self-development", file: "kelos-workers.yaml"}, + {dir: "self-development/agora", file: "agora-workers.yaml"}, + {dir: "self-development/kanon", file: "kanon-workers.yaml"}, } - filter := spawner.Filters[0] - wantFilter := kelos.GitHubWebhookFilter{Event: "issue_comment", Action: "created"} - if !reflect.DeepEqual(filter, wantFilter) { - t.Fatalf("filter = %#v, want %#v", filter, wantFilter) - } - - for _, commentOn := range []string{kelos.CommentOnIssue, kelos.CommentOnPullRequest} { - commentOn := commentOn - for _, body := range []string{"Please investigate this", "/kelos pick-up"} { - body := body - t.Run(commentOn+"/"+body, func(t *testing.T) { - payloadFilter := filter - payloadFilter.CommentOn = commentOn - payload := developmentWebhookPayload(t, spawner.Repository, payloadFilter, body) - eventData, err := webhook.ParseGitHubWebhook("issue_comment", payload) - if err != nil { - t.Fatalf("ParseGitHubWebhook() error = %v", err) - } - got, err := webhook.MatchesGitHubEvent(spawner, "issue_comment", eventData) - if err != nil { - t.Fatalf("MatchesGitHubEvent() error = %v", err) - } - if !got { - t.Fatal("MatchesGitHubEvent() = false, want true") - } - }) - } - } + for _, tt := range spawners { + tt := tt + t.Run(tt.dir+"/"+tt.file, func(t *testing.T) { + t.Parallel() - botFilter := filter - botFilter.Author = "kelos-bot[bot]" - botPayload := developmentWebhookPayload(t, spawner.Repository, botFilter, "/kelos pick-up") - botEvent, err := webhook.ParseGitHubWebhook("issue_comment", botPayload) - if err != nil { - t.Fatalf("ParseGitHubWebhook(bot) error = %v", err) - } - if got, err := webhook.MatchesGitHubEvent(spawner, "issue_comment", botEvent); err != nil || got { - t.Fatalf("MatchesGitHubEvent(bot) = %v, %v, want false, nil", got, err) + sessionSpawner := readSessionSpawnerFromDir(t, tt.dir, tt.file) + spawner := sessionSpawner.Spec.When.GitHubWebhook + if spawner == nil { + t.Fatal("SessionSpawner spec.when.githubWebhook is nil") + } + if !reflect.DeepEqual(spawner.ExcludeAuthors, []string{"kelos-bot[bot]"}) { + t.Fatalf("excludeAuthors = %v, want [kelos-bot[bot]]", spawner.ExcludeAuthors) + } + if !reflect.DeepEqual(spawner.Filters, []kelos.GitHubWebhookFilter{wantFilter}) { + t.Fatalf("filters = %#v, want %#v", spawner.Filters, []kelos.GitHubWebhookFilter{wantFilter}) + } + + prFilter := wantFilter + prFilter.CommentOn = kelos.CommentOnPullRequest + closedFilter := wantFilter + closedFilter.State = "closed" + otherAuthorFilter := wantFilter + otherAuthorFilter.Author = "octocat" + + cases := []struct { + name string + filter kelos.GitHubWebhookFilter + body string + want bool + }{ + {name: "pick-up on open issue", filter: wantFilter, body: "/kelos pick-up", want: true}, + {name: "ordinary issue comment", filter: wantFilter, body: "Please investigate this", want: false}, + {name: "pick-up on pull request", filter: prFilter, body: "/kelos pick-up", want: false}, + {name: "pick-up on closed issue", filter: closedFilter, body: "/kelos pick-up", want: false}, + {name: "pick-up from another author", filter: otherAuthorFilter, body: "/kelos pick-up", want: false}, + } + + for _, tc := range cases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + payload := developmentWebhookPayload(t, spawner.Repository, tc.filter, tc.body) + eventData, err := webhook.ParseGitHubWebhook("issue_comment", payload) + if err != nil { + t.Fatalf("ParseGitHubWebhook() error = %v", err) + } + + got, err := webhook.MatchesGitHubEvent(spawner, "issue_comment", eventData) + if err != nil { + t.Fatalf("MatchesGitHubEvent() error = %v", err) + } + if got != tc.want { + t.Fatalf("MatchesGitHubEvent() = %v, want %v", got, tc.want) + } + }) + } + }) } } -func TestDevelopmentSessionSpawnerUsesPRBranchAndIssueFallback(t *testing.T) { +func TestDevelopmentSessionSpawnersUseIssueBranches(t *testing.T) { t.Parallel() - _, spawner := readSpawnerFromDir(t, "self-development", "kelos-workers.yaml") - if spawner == nil { - t.Fatal("SessionSpawner is nil") - } - tests := []struct { - name string - branch string - want string + dir string + file string + want string }{ - {name: "pull request", branch: "feature-branch", want: "feature-branch"}, - {name: "issue", branch: "", want: "kelos-task-42"}, + {dir: "self-development", file: "kelos-workers.yaml", want: "kelos-task-42"}, + {dir: "self-development/agora", file: "agora-workers.yaml", want: "agora-task-42"}, + {dir: "self-development/kanon", file: "kanon-workers.yaml", want: "kanon-task-42"}, } for _, tt := range tests { tt := tt - t.Run(tt.name, func(t *testing.T) { + t.Run(tt.dir+"/"+tt.file, func(t *testing.T) { + t.Parallel() + + spawner := readSessionSpawnerFromDir(t, tt.dir, tt.file) + got, err := sessionbuilder.Render("initialBranch", spawner.Spec.SessionTemplate.InitialBranch, map[string]interface{}{ - "Branch": tt.branch, "Number": 42, }) if err != nil { @@ -346,6 +379,67 @@ func TestDevelopmentSessionSpawnerUsesPRBranchAndIssueFallback(t *testing.T) { } } +func TestDevelopmentPRResponderSessionSpawnersMatchPickUpOnPullRequests(t *testing.T) { + t.Parallel() + + notDraft := false + wantFilters := []kelos.GitHubWebhookFilter{ + { + Event: "issue_comment", + Action: "created", + BodyPattern: `(?m)^/kelos pick-up[ \t]*\r?$`, + CommentOn: kelos.CommentOnPullRequest, + State: "open", + Author: "gjkim42", + }, + { + Event: "pull_request_review", + Action: "submitted", + BodyPattern: `(?m)^/kelos pick-up[ \t]*\r?$`, + State: "open", + Draft: ¬Draft, + Author: "gjkim42", + }, + } + tests := []struct { + dir string + file string + }{ + {dir: "self-development", file: "kelos-pr-responder.yaml"}, + {dir: "self-development/agora", file: "agora-pr-responder.yaml"}, + {dir: "self-development/kanon", file: "kanon-pr-responder.yaml"}, + } + for _, tt := range tests { + tt := tt + t.Run(tt.dir+"/"+tt.file, func(t *testing.T) { + t.Parallel() + + sessionSpawner := readSessionSpawnerFromDir(t, tt.dir, tt.file) + + spawner := sessionSpawner.Spec.When.GitHubWebhook + if spawner == nil { + t.Fatal("SessionSpawner spec.when.githubWebhook is nil") + } + if spawner.Reporting != nil { + t.Fatalf("SessionSpawner githubWebhook.reporting = %v, want nil", spawner.Reporting) + } + if !reflect.DeepEqual(spawner.Filters, wantFilters) { + t.Fatalf("filters = %#v, want %#v", spawner.Filters, wantFilters) + } + + got, err := sessionbuilder.Render("initialBranch", sessionSpawner.Spec.SessionTemplate.InitialBranch, map[string]interface{}{ + "Branch": "feature-branch", + }) + if err != nil { + t.Fatalf("Render() error = %v", err) + } + if want := "feature-branch"; got != want { + t.Fatalf("initialBranch = %q, want %q", got, want) + } + }) + } +} + func TestDevelopmentCommandPatternsMatchCommandLines(t *testing.T) { t.Parallel() @@ -354,6 +448,7 @@ func TestDevelopmentCommandPatternsMatchCommandLines(t *testing.T) { file string command string }{ + {dir: "self-development", file: "kelos-workers.yaml", command: "/kelos pick-up"}, {dir: "self-development", file: "kelos-planner.yaml", command: "/kelos plan"}, {dir: "self-development", file: "kelos-reviewer.yaml", command: "/kelos review"}, {dir: "self-development", file: "kelos-api-reviewer.yaml", command: "/kelos api-review"}, @@ -361,6 +456,8 @@ func TestDevelopmentCommandPatternsMatchCommandLines(t *testing.T) { {dir: "self-development", file: "kelos-glm-api-reviewer.yaml", command: "/kelos glm-api-review"}, {dir: "self-development", file: "kelos-pr-responder.yaml", command: "/kelos pick-up"}, {dir: "self-development", file: "kelos-squash-commits.yaml", command: "/kelos squash-commits"}, + {dir: "self-development/agora", file: "agora-workers.yaml", command: "/kelos pick-up"}, + {dir: "self-development/agora", file: "agora-pr-responder.yaml", command: "/kelos pick-up"}, {dir: "self-development/kanon", file: "kanon-workers.yaml", command: "/kelos pick-up"}, {dir: "self-development/kanon", file: "kanon-planner.yaml", command: "/kelos plan"}, {dir: "self-development/kanon", file: "kanon-reviewer.yaml", command: "/kelos review"}, @@ -689,8 +786,13 @@ func TestAgoraIssueCreatorsUseTriageAcceptedLabel(t *testing.T) { t.Run(file, func(t *testing.T) { t.Parallel() - ts := readTaskSpawnerFromDir(t, "self-development/agora", file) - prompt := ts.Spec.TaskTemplate.PromptTemplate + taskSpawner, sessionSpawner := readSpawnerFromDir(t, "self-development/agora", file) + var prompt string + if taskSpawner != nil { + prompt = taskSpawner.Spec.TaskTemplate.PromptTemplate + } else { + prompt = sessionSpawner.Spec.SessionTemplate.InitialPrompt + } if !strings.Contains(prompt, "triage-accepted") { t.Fatalf("%s prompt does not mention triage-accepted", file) } @@ -826,7 +928,7 @@ func TestAgoraPlannerOnlyTriggersForIssues(t *testing.T) { } } -func TestDevelopmentTaskSpawnersSetExpectedEffort(t *testing.T) { +func TestDevelopmentSpawnersSetExpectedEffort(t *testing.T) { t.Parallel() tests := []struct { @@ -848,6 +950,8 @@ func TestDevelopmentTaskSpawnersSetExpectedEffort(t *testing.T) { {dir: "self-development", file: "kelos-image-update.yaml", effort: "medium"}, {dir: "self-development", file: "kelos-fake-user.yaml", effort: "medium"}, {dir: "self-development", file: "kelos-squash-commits.yaml", effort: "medium"}, + {dir: "self-development/agora", file: "agora-workers.yaml", effort: "xhigh"}, + {dir: "self-development/agora", file: "agora-pr-responder.yaml", effort: "xhigh"}, {dir: "self-development/kanon", file: "kanon-workers.yaml", effort: "xhigh"}, {dir: "self-development/kanon", file: "kanon-planner.yaml", effort: "xhigh"}, {dir: "self-development/kanon", file: "kanon-reviewer.yaml", effort: "xhigh"}, @@ -1215,6 +1319,16 @@ func readTaskSpawnerFromDir(t *testing.T, dir, file string) *kelos.TaskSpawner { return taskSpawner } +func readSessionSpawnerFromDir(t *testing.T, dir, file string) *kelos.SessionSpawner { + t.Helper() + + _, sessionSpawner := readSpawnerFromDir(t, dir, file) + if sessionSpawner == nil { + t.Fatalf("no SessionSpawner found in %s", filepath.Join("..", "..", dir, file)) + } + return sessionSpawner +} + func readGitHubWebhookFromDir(t *testing.T, dir, file string) *kelos.GitHubWebhook { t.Helper() diff --git a/self-development/README.md b/self-development/README.md index 3bea9891..62f0cb91 100644 --- a/self-development/README.md +++ b/self-development/README.md @@ -23,8 +23,9 @@ directory and its nested Agora and Kanon directories references [`gjkim42/kanon-repo`'s `instructions/AGENTS.md`](https://github.com/gjkim42/kanon-repo/blob/main/instructions/AGENTS.md) and installs all skills from that repository through `spec.skills`. Tasks and TaskSpawners add a second role-specific AgentConfig when they need -local identity, conventions, or workflow instructions. The `kelos-workers` -SessionSpawner and Sessions created with `cs` use only `base-agent`. +local identity, conventions, or workflow instructions. The issue and PR +pick-up SessionSpawners for Kelos, Agora, and Kanon, plus Sessions created with +`cs`, use only `base-agent`. Apply the shared AgentConfig before deploying any self-development resource: @@ -48,13 +49,13 @@ while a worker or PR responder is handling an explicitly requested issue or PR. | Spawner | Trigger | Agent | Description | |---|---|---|---| -| **kelos-workers** | Webhook: every new issue/PR conversation comment | Codex | Responds in a durable Session, creates or updates PRs when needed, self-reviews, and ensures CI passes | +| **kelos-workers** | Webhook: issue comment `/kelos pick-up` | Codex | Picks up an open issue in a durable Session, creates or updates its PR, self-reviews, and ensures CI passes | | **kelos-planner** | Webhook: issue comment `/kelos plan` | Codex | Investigates an issue and posts a structured implementation plan — advisory only, no code changes | | **kelos-reviewer** | Webhook: PR comment `/kelos review` | Codex | Reviews PRs on demand — analyzes code, checks conventions, and updates a sticky review comment | | **kelos-glm-reviewer** | Webhook: PR comment `/kelos glm-review` | GLM-5.2 | Runs a second code review path with Z.AI GLM-5.2 through OpenCode and updates a sticky review comment | | **kelos-api-reviewer** | Webhook: issue/PR comment `/kelos api-review` | Codex | Reviews Kubernetes API design on issues or PRs — naming, compatibility, CRD validation | | **kelos-glm-api-reviewer** | Webhook: issue/PR comment `/kelos glm-api-review` | GLM-5.2 | Runs a second Kubernetes API design review path with Z.AI GLM-5.2 through OpenCode and updates sticky PR comments | -| **kelos-pr-responder** | Webhook: PR review/comment on `generated-by-kelos` PRs | Codex | Re-engages on PR review feedback and updates the existing branch incrementally | +| **kelos-pr-responder** | Webhook: PR comment/review `/kelos pick-up` | Codex | Picks up an open PR in a durable Session and updates its existing branch incrementally | | **kelos-triage** | Webhook: issue opened/labeled/reopened (`needs-actor`) | Codex | Classifies issues by kind/priority, detects duplicates, and recommends an actor | | **kelos-fake-user** | Cron (daily 09:00 UTC) | Codex | Tests DX as a new user and maintains one unassigned issue slot for the highest-impact problem found | | **kelos-fake-strategist** | Cron (every 12 hours) | Codex | Explores new use cases, integrations, and API ideas while maintaining one unassigned strategic issue slot | @@ -65,41 +66,34 @@ while a worker or PR responder is handling an explicitly requested issue or PR. ### kelos-workers.yaml -Creates a durable Session for every new non-bot comment in an issue or pull -request conversation. The triggering comment is the immediate request; -`/kelos pick-up` tells the worker to take ownership of the complete issue or PR. -Follow-ups can continue through the Session's web or terminal clients after the -initial turn. +Creates a durable Session when the maintainer posts `/kelos pick-up` on an open +issue. Follow-ups continue through the Session's web or terminal clients after +the initial turn. | | | |---|---| -| **Trigger** | Every GitHub `issue_comment` webhook with action `created`, on issues and PRs | +| **Trigger** | GitHub `issue_comment` webhook with an exact `/kelos pick-up` command from `gjkim42` on an open issue | | **Agent** | Codex | | **Storage** | 10 GiB persistent volume per created Session | **Key features:** - Automatically checks for existing PRs and updates them incrementally -- Uses the PR head branch for PR comments and `kelos-task-` for issue comments +- Uses `kelos-task-` for the issue branch - Self-reviews PRs before requesting human review - Ensures CI passes before completion -- Treats `/kelos pick-up` as a request to handle the complete issue or PR +- Requires `/kelos pick-up` from the maintainer before starting work - Excludes comments from `kelos-bot[bot]` to prevent self-trigger loops - Keeps the Session available for later web or terminal follow-ups - Hands off PR review feedback to `kelos-pr-responder` - May create separate follow-up issues for out-of-scope discoveries; those - follow-ups are exempt from the per-TaskSpawner issue slot cap + follow-ups are exempt from autonomous discovery issue slot caps **Deploy:** ```bash -kubectl delete taskspawner kelos-workers --ignore-not-found kubectl apply -f self-development/base-agent.yaml kubectl apply -f self-development/kelos-workers.yaml ``` -The delete is required when migrating an existing installation because -`TaskSpawner/kelos-workers` and `SessionSpawner/kelos-workers` are distinct -Kubernetes objects. Leaving both installed would run both worker flows. - ### kelos-planner.yaml Reacts to `/kelos plan` comments on open issues. Investigates the issue, inspects the codebase, and posts a structured implementation plan — advisory only, no code changes. @@ -237,24 +231,26 @@ kubectl apply -f self-development/kelos-glm-api-reviewer.yaml ### kelos-pr-responder.yaml -Picks up open GitHub pull requests labeled `generated-by-kelos` when a reviewer requests changes. +Creates a durable Session when the maintainer posts an exact `/kelos pick-up` +PR comment or review on an open pull request. | | | |---|---| -| **Trigger** | GitHub PR review/comment webhooks on `generated-by-kelos` pull requests | +| **Trigger** | GitHub PR comment or review webhook with an exact `/kelos pick-up` command from `gjkim42` on an open PR; review events also exclude drafts | | **Agent** | Codex | -| **Concurrency** | 8 | +| **Storage** | 10 GiB persistent volume per created Session | **Key features:** - Reuses the existing PR branch instead of starting over - Reads review comments and PR conversation before making incremental changes -- Lets the maintainer stay on the PR page for the common review-feedback loop +- Keeps the Session available for later web or terminal follow-ups - Requires `/kelos pick-up` PR comment or review body to be picked up - May create separate follow-up issues for out-of-scope discoveries; those - follow-ups are exempt from the per-TaskSpawner issue slot cap + follow-ups are exempt from autonomous discovery issue slot caps **Deploy:** ```bash +kubectl apply -f self-development/base-agent.yaml kubectl apply -f self-development/kelos-pr-responder.yaml ``` @@ -573,7 +569,7 @@ To adapt these examples for your own repository: | `bodyPattern` | `TaskSpawner.spec.when.githubWebhook.filters[]` | Go re2 regex match against the comment/review body — the modern replacement for substring-only matching. | | `excludeBodyPatterns` | `TaskSpawner.spec.when.githubWebhook.filters[]` | Companion to `bodyPattern`: a list of regexes that, if any match, drop the event. Use to carve out bot-echo replies that would otherwise match `bodyPattern`. | | `commentOn` | `TaskSpawner.spec.when.githubWebhook.filters[]` | Scopes `issue_comment` events to `Issue` or `PullRequest`. GitHub fires `issue_comment` for both, so set this to keep issue-only spawners off PRs (and vice versa). | - | `author` | `TaskSpawner.spec.when.githubWebhook.filters[]` | Restrict matches to a single sender's username. Omit it to accept every sender not listed in top-level `excludeAuthors`, as `kelos-workers` does. | + | `author` | `TaskSpawner.spec.when.githubWebhook.filters[]` | Restrict matches to a single sender's username. Omit it to accept every sender not listed in top-level `excludeAuthors`; `kelos-workers` sets it to the maintainer as an approval gate. | | `draft` | `TaskSpawner.spec.when.githubWebhook.filters[]` | Match by PR draft status. Set `false` to skip drafts; omit to match both. | See [docs/reference.md](../docs/reference.md#taskspawner) for the full @@ -630,10 +626,10 @@ To adapt these examples for your own repository: The key pattern in these examples is webhook-triggered handoff plus runtime re-validation: 1. GitHub delivers an `issue_comment`, `issues`, or `pull_request_review` webhook -2. The matching TaskSpawner creates a Task, or `kelos-workers` creates a Session +2. The matching TaskSpawner creates a Task, while the issue and PR pick-up spawners for Kelos, Agora, and Kanon create Sessions 3. The agent re-reads the latest issue or PR state with `gh` before acting, so asynchronous label updates are respected 4. If the agent needs human input, it posts a plain-English status comment describing what happened -5. Every fresh issue or PR conversation comment creates a `kelos-workers` Session; explicit commands or relabel events retrigger the other matching automation +5. An exact `/kelos pick-up` command creates a Session for an open issue or PR; explicit commands or relabel events retrigger the other matching automation Each matching webhook delivery creates a discrete Task or Session. A created Session remains available for interactive follow-ups through Session clients. @@ -653,9 +649,8 @@ spawners when those spawners include a matching bot-author filter. - If the issue or PR matched before you deployed the webhook server, retrigger it with a new comment or relabel **SessionSpawner not creating a Session:** -- Check the SessionSpawner status: `kubectl get sessionspawner kelos-workers -o yaml` -- Check created Sessions: `SPAWNER_UID=$(kubectl get sessionspawner kelos-workers -o jsonpath='{.metadata.uid}'); kubectl get sessions -l kelos.dev/sessionspawner="$SPAWNER_UID"` -- Ensure the old `TaskSpawner/kelos-workers` was removed during migration +- Check the SessionSpawner status: `kubectl get sessionspawner -o yaml` +- Check created Sessions: `SPAWNER_UID=$(kubectl get sessionspawner -o jsonpath='{.metadata.uid}'); kubectl get sessions -l kelos.dev/sessionspawner="$SPAWNER_UID"` - Check the same Workspace, credentials, webhook server, and recent-delivery details listed above **Tasks failing immediately:** diff --git a/self-development/agora/README.md b/self-development/agora/README.md index 3a1b3c91..99af5b67 100644 --- a/self-development/agora/README.md +++ b/self-development/agora/README.md @@ -11,12 +11,12 @@ the Agora repository. ## How It Works -Each TaskSpawner references the root [`base-agent`](../base-agent.yaml) first -for shared instructions and skills. It then references an AgentConfig with -repository- or role-specific instructions: pr-responder, triage, and -squash-commits share `agentconfig.yaml` (`agora-dev-agent`), while workers, -planner, reviewer, fake-user, and fake-strategist define their own AgentConfig -inline. +Every spawner references the root [`base-agent`](../base-agent.yaml) for shared +instructions and skills. The issue and PR pick-up SessionSpawners reference +only `base-agent`. The remaining TaskSpawners add repository- or role-specific +instructions where needed: triage and squash-commits share `agentconfig.yaml` +(`agora-dev-agent`), while planner, reviewer, fake-user, and fake-strategist +define their own AgentConfig inline. Autonomous discovery agents that publish GitHub issues maintain at most one open `generated-by-kelos` issue slot per TaskSpawner. The issue body includes a @@ -35,14 +35,14 @@ maintain (`self-development/agora/*`) live in *this* repository, so they use the `self-development/`, and they read Agora's activity cross-repo with `gh ... --repo kelos-dev/agora`. -## TaskSpawners +## Spawners -| TaskSpawner | Trigger | Agent | Description | +| Spawner | Trigger | Agent | Description | |---|---|---|---| -| **agora-workers** | Webhook: issue comment `/kelos pick-up` | Codex | Picks up issues, creates or updates PRs, self-reviews, and ensures CI passes | +| **agora-workers** | Webhook: issue comment `/kelos pick-up` | Codex | Creates durable Sessions for issue work, including PR creation or updates | | **agora-planner** | Webhook: issue comment `/kelos plan` | Codex | Investigates an issue and posts a structured implementation plan — advisory only, no code changes | | **agora-reviewer** | Webhook: PR comment `/kelos review` | Codex | Reviews PRs on demand — analyzes code, checks conventions, and updates a sticky review comment | -| **agora-pr-responder** | Webhook: PR review/comment with `/kelos pick-up` | Codex | Re-engages on PR review feedback and updates the existing branch incrementally | +| **agora-pr-responder** | Webhook: PR review/comment with `/kelos pick-up` | Codex | Creates durable Sessions for PR review feedback on the existing branch | | **agora-triage** | Webhook: issue opened/reopened (untriaged) | Codex | Classifies issues by kind/priority, detects duplicates, and recommends an actor | | **agora-fake-user** | Cron (daily 09:00 UTC) | Codex | Tests DX as a new user and maintains one unassigned issue slot for the highest-impact problem found | | **agora-fake-strategist** | Cron (every 12 hours) | Codex | Explores new use cases, integrations, and API/UI/deployment capabilities while maintaining one unassigned strategic issue slot | @@ -56,8 +56,7 @@ maintain (`self-development/agora/*`) live in *this* repository, so they use the Apply the root `base-agent` first, then the whole directory. The directory includes `agentconfig.yaml`, which defines the `agora-dev-agent` role -instructions referenced by the pr-responder, triage, and squash-commits -spawners: +instructions referenced by the triage and squash-commits spawners: ```bash kubectl apply -f self-development/base-agent.yaml @@ -69,22 +68,24 @@ individual spawner after `base-agent` is installed. ### agora-workers.yaml -Picks up open GitHub issues when a maintainer posts `/kelos pick-up` and creates autonomous agent tasks to fix them. +Picks up open GitHub issues when a maintainer posts `/kelos pick-up` and +creates a durable Session to fix them. | | | |---|---| | **Trigger** | GitHub `issue_comment` webhook with `/kelos pick-up` | | **Agent** | Codex | -| **Concurrency** | 8 | +| **Storage** | 10 Gi PVC | **Key features:** - Automatically checks for existing PRs and updates them incrementally - Self-reviews PRs before requesting human review - Ensures CI passes before completion - Requires a `/kelos pick-up` comment to pick up an issue (maintainer approval gate) -- Hands off PR review feedback to `agora-pr-responder` +- Keeps the workspace across Session follow-ups and pod restarts +- Supports routine follow-ups through the Session's web or terminal clients - May create separate follow-up issues for out-of-scope discoveries; those - follow-ups are exempt from the per-TaskSpawner issue slot cap + follow-ups are exempt from autonomous discovery issue slot caps **Deploy:** ```bash @@ -147,15 +148,17 @@ Picks up open GitHub pull requests when a reviewer requests changes with `/kelos |---|---| | **Trigger** | GitHub PR comment with `/kelos pick-up`, or a PR review whose body contains `/kelos pick-up` | | **Agent** | Codex | -| **Concurrency** | 8 | +| **Storage** | 10 Gi PVC | **Key features:** - Reuses the existing PR branch instead of starting over - Reads review comments and PR conversation before making incremental changes - Lets the maintainer stay on the PR page for the common review-feedback loop - Requires a `/kelos pick-up` PR comment or review body to be picked up +- Keeps the workspace across Session follow-ups and pod restarts +- Supports routine follow-ups through the Session's web or terminal clients - May create separate follow-up issues for out-of-scope discoveries; those - follow-ups are exempt from the per-TaskSpawner issue slot cap + follow-ups are exempt from autonomous discovery issue slot caps **Deploy:** ```bash @@ -372,17 +375,16 @@ The token needs `repo` (full control) and `workflow` (if your repo uses GitHub A ### 4. GitHub Webhook Secret and Delivery -The issue and pull request TaskSpawners are webhook-driven. Reuse the -`github-webhook-secret` from your existing deployment, then configure a -repository webhook on `kelos-dev/agora`: +The issue and PR pick-up SessionSpawners and the remaining webhook +TaskSpawners are event-driven. Reuse the `github-webhook-secret` from your +existing deployment, then configure a repository webhook on `kelos-dev/agora`: - Point it at the same `https:///webhook/github` endpoint - Use the same shared secret - Subscribe to `issues`, `issue_comment`, and `pull_request_review` -Webhook TaskSpawners only react to **new** events after deployment. Retrigger an -existing issue or PR with a fresh comment or relabel if it was already in a -matching state. +Webhook spawners only react to **new** events after deployment. Retrigger an +existing issue or PR with a fresh matching event if needed. ### 5. Agent Credentials Secret @@ -394,32 +396,33 @@ kubectl create secret generic kelos-credentials \ --from-file=CODEX_AUTH_JSON=$HOME/.codex/auth.json ``` -For API-key auth, change the task template credential type to `api-key` and use +For API-key auth, change the worker credential type to `api-key` and use `--from-literal=CODEX_API_KEY=`. ## Customizing -The `TaskSpawner.spec.when.githubWebhook` filters and template variables work -exactly as in `self-development/`. See +The `spec.when.githubWebhook` filters and template variables work the same for +TaskSpawner and SessionSpawner resources. See [`self-development/README.md`](../README.md#customizing-for-your-repository) for the webhook filter field reference and the full [template variable table](../README.md), and -[docs/reference.md](../../docs/reference.md#taskspawner) for the authoritative -`TaskSpawner` field reference. +[docs/reference.md](../../docs/reference.md) for the authoritative field +references. ## Troubleshooting -**TaskSpawner not creating tasks:** -- Check the TaskSpawner status: `kubectl get taskspawner -o yaml` +**Webhook spawner not creating work:** +- For pick-up, check the SessionSpawner status: `kubectl get sessionspawner -o yaml` +- For other automation, check the TaskSpawner status: `kubectl get taskspawner -o yaml` - Verify the Workspaces exist: `kubectl get workspace agora-agent kelos-agent` - Ensure credentials are configured: `kubectl get secret kelos-credentials` - Ensure the GitHub webhook server is enabled and the `github-webhook-secret` exists - Review the `kelos-dev/agora` repository webhook's recent deliveries in GitHub -**Tasks failing immediately:** +**Sessions or tasks failing immediately:** - Verify the agent credentials are valid - Check the Workspace repository is accessible and the token has push access to it -- Review task logs: `kubectl logs -l job-name=` +- Review the corresponding Session or Task status and pod logs **Triage or PR/issue creation failing on labels:** - Confirm the labels from [Repository labels](#2-repository-labels) exist on `kelos-dev/agora` — `gh` errors when adding or creating with a label that does not exist diff --git a/self-development/agora/agora-pr-responder.yaml b/self-development/agora/agora-pr-responder.yaml index df0dee9b..20f033e2 100644 --- a/self-development/agora/agora-pr-responder.yaml +++ b/self-development/agora/agora-pr-responder.yaml @@ -1,5 +1,5 @@ apiVersion: kelos.dev/v1alpha2 -kind: TaskSpawner +kind: SessionSpawner metadata: name: agora-pr-responder spec: @@ -14,21 +14,23 @@ spec: filters: - event: issue_comment action: created - bodyPattern: '(?m)^[ \t]*/kelos pick-up[ \t]*\r?$' + bodyPattern: '(?m)^/kelos pick-up[ \t]*\r?$' commentOn: PullRequest state: open author: gjkim42 - event: pull_request_review action: submitted - bodyPattern: '(?m)^[ \t]*/kelos pick-up[ \t]*\r?$' + bodyPattern: '(?m)^/kelos pick-up[ \t]*\r?$' state: open draft: false author: gjkim42 - reporting: - enabled: true - checks: {} - maxConcurrency: 8 - taskTemplate: + sessionTemplate: + volumeClaimTemplate: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 10Gi worker: workspaceRef: name: agora-agent @@ -58,22 +60,14 @@ spec: value: "gjkim042@gmail.com" agentConfigRefs: - name: base-agent - - name: agora-dev-agent - ttlSecondsAfterFinished: 864000 - podFailurePolicy: - rules: - - action: Ignore - onPodConditions: - - type: DisruptionTarget - status: "True" - - action: FailJob - onExitCodes: - operator: NotIn - values: [0] - branch: "{{.Branch}}" - promptTemplate: | + initialBranch: "{{.Branch}}" + initialPrompt: | You are a coding agent updating an existing PR in response to review feedback. + Your workspace is backed by a PVC. Files remain available across follow-ups + and pod restarts while the Session and its PVC exist. Commit and push work + that must outlive the Session. + Webhook: - Event: {{.Event}} - Action: {{.Action}} @@ -101,9 +95,9 @@ spec: - 9. After pushing, actively check CI status with `gh pr checks {{.Number}}`. If any checks fail, investigate the failures, fix them, commit and push, then check again. Do not request review until CI passes. Post-checklist: - - If the PR is ready for human review, you need more information, or you cannot make progress, post a plain-English PR comment explaining the current state. Maintainers can resume the PR later with `/kelos pick-up`. When all review feedback was already addressed in previous commits and no new comments require action, keep the explanation brief (e.g. "All review feedback was addressed in previous commits. Ready for re-review.") instead of repeating a full status breakdown. Never post duplicate or near-identical status messages. + - If the PR is ready for human review, you need more information, or you cannot make progress, post a plain-English PR comment explaining the current state. Continue routine follow-ups through the Session's web or terminal clients. When all review feedback was already addressed in previous commits and no new comments require action, keep the explanation brief (e.g. "All review feedback was addressed in previous commits. Ready for re-review.") instead of repeating a full status breakdown. Never post duplicate or near-identical status messages. - If you find follow-up work outside the current PR scope, you may create a separate GitHub issue: - First check existing issues with `gh issue list` to avoid duplicates. - Keep the follow-up narrowly scoped and label it `generated-by-kelos` and `triage-accepted`. - - Follow-up issues are exempt from the per-TaskSpawner issue slot cap. + - Follow-up issues are exempt from autonomous discovery issue slot caps. diff --git a/self-development/agora/agora-workers.yaml b/self-development/agora/agora-workers.yaml index 4069e880..c3ff9ca1 100644 --- a/self-development/agora/agora-workers.yaml +++ b/self-development/agora/agora-workers.yaml @@ -1,39 +1,5 @@ apiVersion: kelos.dev/v1alpha2 -kind: AgentConfig -metadata: - name: agora-workers-agent -spec: - agentsMD: | - # Agora Worker Agent - - Agora (github.com/kelos-dev/agora) is a local coordination server for - humans and coding agents. It serves a browser UI and JSON API for timeline - events, targeted inboxes, instructions, decisions, and status updates. - - ## Environment - You are running in an ephemeral container environment. Your file system - changes disappear after the task completes. Persist work by creating PRs, - issues, or comments. - - ## Identity - - When commenting on issues or PRs, always start with "🤖 **Kelos Worker Agent** @gjkim42\n\n" - so it is clearly distinguishable from human comments and triggers a notification - - ## Standards - - Do not create duplicate issues — check existing issues first with `gh issue list` - - Keep changes minimal and focused - - ## Project Conventions - - Use Makefile targets instead of discovering build/test commands yourself: - - `make verify` — verify formatting, modules, vet, and tests - - `make update` — run formatters and update module metadata - - `make test` — run all unit tests - - `make build` — build the agora binary - - `make run` — run the server locally - - `make image` — build the Agora container image ---- -apiVersion: kelos.dev/v1alpha2 -kind: TaskSpawner +kind: SessionSpawner metadata: name: agora-workers spec: @@ -47,14 +13,17 @@ spec: filters: - event: issue_comment action: created - bodyPattern: '(?m)^[ \t]*/kelos pick-up[ \t]*\r?$' + bodyPattern: '(?m)^/kelos pick-up[ \t]*\r?$' commentOn: Issue state: open author: gjkim42 - reporting: - enabled: true - maxConcurrency: 8 - taskTemplate: + sessionTemplate: + volumeClaimTemplate: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 10Gi worker: workspaceRef: name: agora-agent @@ -84,25 +53,17 @@ spec: value: "gjkim042@gmail.com" agentConfigRefs: - name: base-agent - - name: agora-workers-agent - ttlSecondsAfterFinished: 864000 - podFailurePolicy: - rules: - - action: Ignore - onPodConditions: - - type: DisruptionTarget - status: "True" - - action: FailJob - onExitCodes: - operator: NotIn - values: [0] - branch: "agora-task-{{.Number}}" - promptTemplate: | + initialBranch: "agora-task-{{.Number}}" + initialPrompt: | You are a coding agent. You either - create a PR to fix the issue - update an existing PR to fix the issue - comment on the issue or the PR if you cannot fix it + Your workspace is backed by a PVC. Files remain available across follow-ups + and pod restarts while the Session and its PVC exist. Commit and push work + that must outlive the Session. + Webhook: - Event: {{.Event}} - Action: {{.Action}} @@ -154,4 +115,4 @@ spec: create a separate GitHub issue: - First check existing issues with `gh issue list` to avoid duplicates. - Keep the follow-up narrowly scoped and label it `generated-by-kelos` and `triage-accepted`. - - Follow-up issues are exempt from the per-TaskSpawner issue slot cap. + - Follow-up issues are exempt from autonomous discovery issue slot caps. diff --git a/self-development/kanon/README.md b/self-development/kanon/README.md index d91fdcad..a60b4d26 100644 --- a/self-development/kanon/README.md +++ b/self-development/kanon/README.md @@ -12,12 +12,12 @@ the agents they spawn operate on the Kanon repository. ## How It Works -Each TaskSpawner references the root [`base-agent`](../base-agent.yaml) first -for shared instructions and skills. It then references an AgentConfig with -repository- or role-specific instructions: pr-responder, triage, and -squash-commits share `agentconfig.yaml` (`kanon-dev-agent`), while workers, -planner, reviewer, fake-user, and fake-strategist define their own AgentConfig -inline. +Every spawner references the root [`base-agent`](../base-agent.yaml) for shared +instructions and skills. The issue and PR pick-up SessionSpawners reference +only `base-agent`. The remaining TaskSpawners add repository- or role-specific +instructions where needed: triage and squash-commits share `agentconfig.yaml` +(`kanon-dev-agent`), while planner, reviewer, fake-user, and fake-strategist +define their own AgentConfig inline. Autonomous discovery agents that publish GitHub issues maintain at most one open `generated-by-kelos` issue slot per TaskSpawner. The issue body includes a @@ -36,14 +36,14 @@ maintain (`self-development/kanon/*`) live in *this* repository, so they use the `self-development/`, and they read Kanon's activity cross-repo with `gh ... --repo kelos-dev/kanon`. -## TaskSpawners +## Spawners -| TaskSpawner | Trigger | Agent | Description | +| Spawner | Trigger | Agent | Description | |---|---|---|---| -| **kanon-workers** | Webhook: issue comment `/kelos pick-up` | Codex | Picks up issues, creates or updates PRs, self-reviews, and ensures CI passes | +| **kanon-workers** | Webhook: issue comment `/kelos pick-up` | Codex | Creates durable Sessions for issue work, including PR creation or updates | | **kanon-planner** | Webhook: issue comment `/kelos plan` | Codex | Investigates an issue and posts a structured implementation plan — advisory only, no code changes | | **kanon-reviewer** | Webhook: PR comment `/kelos review` | Codex | Reviews PRs on demand — analyzes code, checks conventions, and updates a sticky review comment | -| **kanon-pr-responder** | Webhook: PR review/comment with `/kelos pick-up` | Codex | Re-engages on PR review feedback and updates the existing branch incrementally | +| **kanon-pr-responder** | Webhook: PR review/comment with `/kelos pick-up` | Codex | Creates durable Sessions for PR review feedback on the existing branch | | **kanon-triage** | Webhook: issue opened/reopened (untriaged) | Codex | Classifies issues by kind/priority, detects duplicates, and recommends an actor | | **kanon-fake-user** | Cron (daily 09:00 UTC) | Codex | Tests DX as a new user and maintains one unassigned issue slot for the highest-impact problem found | | **kanon-fake-strategist** | Cron (every 12 hours) | Codex | Explores new use cases, integrations, and managed-settings types while maintaining one unassigned strategic issue slot | @@ -57,8 +57,7 @@ maintain (`self-development/kanon/*`) live in *this* repository, so they use the Apply the root `base-agent` first, then the whole directory. The directory includes `agentconfig.yaml`, which defines the `kanon-dev-agent` role -instructions referenced by the pr-responder, triage, and squash-commits -spawners: +instructions referenced by the triage and squash-commits spawners: ```bash kubectl apply -f self-development/base-agent.yaml @@ -70,22 +69,24 @@ individual spawner after `base-agent` is installed. ### kanon-workers.yaml -Picks up open GitHub issues when a maintainer posts `/kelos pick-up` and creates autonomous agent tasks to fix them. +Picks up open GitHub issues when a maintainer posts `/kelos pick-up` and +creates a durable Session to fix them. | | | |---|---| | **Trigger** | GitHub `issue_comment` webhook with `/kelos pick-up` | | **Agent** | Codex | -| **Concurrency** | 8 | +| **Storage** | 10 Gi PVC | **Key features:** - Automatically checks for existing PRs and updates them incrementally - Self-reviews PRs before requesting human review - Ensures CI passes before completion - Requires a `/kelos pick-up` comment to pick up an issue (maintainer approval gate) -- Hands off PR review feedback to `kanon-pr-responder` +- Keeps the workspace across Session follow-ups and pod restarts +- Supports routine follow-ups through the Session's web or terminal clients - May create separate follow-up issues for out-of-scope discoveries; those - follow-ups are exempt from the per-TaskSpawner issue slot cap + follow-ups are exempt from autonomous discovery issue slot caps **Deploy:** ```bash @@ -148,15 +149,17 @@ Picks up open GitHub pull requests when a reviewer requests changes with `/kelos |---|---| | **Trigger** | GitHub PR comment with `/kelos pick-up`, or a PR review whose body contains `/kelos pick-up` | | **Agent** | Codex | -| **Concurrency** | 8 | +| **Storage** | 10 Gi PVC | **Key features:** - Reuses the existing PR branch instead of starting over - Reads review comments and PR conversation before making incremental changes - Lets the maintainer stay on the PR page for the common review-feedback loop - Requires a `/kelos pick-up` PR comment or review body to be picked up +- Keeps the workspace across Session follow-ups and pod restarts +- Supports routine follow-ups through the Session's web or terminal clients - May create separate follow-up issues for out-of-scope discoveries; those - follow-ups are exempt from the per-TaskSpawner issue slot cap + follow-ups are exempt from autonomous discovery issue slot caps **Deploy:** ```bash @@ -373,17 +376,16 @@ The token needs `repo` (full control) and `workflow` (if your repo uses GitHub A ### 4. GitHub Webhook Secret and Delivery -The issue and pull request TaskSpawners are webhook-driven. Reuse the -`github-webhook-secret` from your existing deployment, then configure a -repository webhook on `kelos-dev/kanon`: +The issue and PR pick-up SessionSpawners and the remaining webhook +TaskSpawners are event-driven. Reuse the `github-webhook-secret` from your +existing deployment, then configure a repository webhook on `kelos-dev/kanon`: - Point it at the same `https:///webhook/github` endpoint - Use the same shared secret - Subscribe to `issues`, `issue_comment`, and `pull_request_review` -Webhook TaskSpawners only react to **new** events after deployment. Retrigger an -existing issue or PR with a fresh comment or relabel if it was already in a -matching state. +Webhook spawners only react to **new** events after deployment. Retrigger an +existing issue or PR with a fresh matching event if needed. ### 5. Agent Credentials Secret @@ -395,32 +397,33 @@ kubectl create secret generic kelos-credentials \ --from-file=CODEX_AUTH_JSON=$HOME/.codex/auth.json ``` -For API-key auth, change the task template credential type to `api-key` and use +For API-key auth, change the worker credential type to `api-key` and use `--from-literal=CODEX_API_KEY=`. ## Customizing -The `TaskSpawner.spec.when.githubWebhook` filters and template variables work -exactly as in `self-development/`. See +The `spec.when.githubWebhook` filters and template variables work the same for +TaskSpawner and SessionSpawner resources. See [`self-development/README.md`](../README.md#customizing-for-your-repository) for the webhook filter field reference and the full [template variable table](../README.md), and -[docs/reference.md](../../docs/reference.md#taskspawner) for the authoritative -`TaskSpawner` field reference. +[docs/reference.md](../../docs/reference.md) for the authoritative field +references. ## Troubleshooting -**TaskSpawner not creating tasks:** -- Check the TaskSpawner status: `kubectl get taskspawner -o yaml` +**Webhook spawner not creating work:** +- For pick-up, check the SessionSpawner status: `kubectl get sessionspawner -o yaml` +- For other automation, check the TaskSpawner status: `kubectl get taskspawner -o yaml` - Verify the Workspaces exist: `kubectl get workspace kanon-agent kelos-agent` - Ensure credentials are configured: `kubectl get secret kelos-credentials` - Ensure the GitHub webhook server is enabled and the `github-webhook-secret` exists - Review the `kelos-dev/kanon` repository webhook's recent deliveries in GitHub -**Tasks failing immediately:** +**Sessions or tasks failing immediately:** - Verify the agent credentials are valid - Check the Workspace repository is accessible and the token has push access to it -- Review task logs: `kubectl logs -l job-name=` +- Review the corresponding Session or Task status and pod logs **Triage or PR/issue creation failing on labels:** - Confirm the labels from [Repository labels](#2-repository-labels) exist on `kelos-dev/kanon` — `gh` errors when adding or creating with a label that does not exist diff --git a/self-development/kanon/kanon-pr-responder.yaml b/self-development/kanon/kanon-pr-responder.yaml index cdc77d89..6a161e7f 100644 --- a/self-development/kanon/kanon-pr-responder.yaml +++ b/self-development/kanon/kanon-pr-responder.yaml @@ -1,5 +1,5 @@ apiVersion: kelos.dev/v1alpha2 -kind: TaskSpawner +kind: SessionSpawner metadata: name: kanon-pr-responder spec: @@ -24,11 +24,13 @@ spec: state: open draft: false author: gjkim42 - reporting: - enabled: true - checks: {} - maxConcurrency: 8 - taskTemplate: + sessionTemplate: + volumeClaimTemplate: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 10Gi worker: workspaceRef: name: kanon-agent @@ -58,22 +60,14 @@ spec: value: "gjkim042@gmail.com" agentConfigRefs: - name: base-agent - - name: kanon-dev-agent - ttlSecondsAfterFinished: 864000 - podFailurePolicy: - rules: - - action: Ignore - onPodConditions: - - type: DisruptionTarget - status: "True" - - action: FailJob - onExitCodes: - operator: NotIn - values: [0] - branch: "{{.Branch}}" - promptTemplate: | + initialBranch: "{{.Branch}}" + initialPrompt: | You are a coding agent updating an existing PR in response to review feedback. + Your workspace is backed by a PVC. Files remain available across follow-ups + and pod restarts while the Session and its PVC exist. Commit and push work + that must outlive the Session. + Webhook: - Event: {{.Event}} - Action: {{.Action}} @@ -101,9 +95,9 @@ spec: - 9. After pushing, actively check CI status with `gh pr checks {{.Number}}`. If any checks fail, investigate the failures, fix them, commit and push, then check again. Do not request review until CI passes. Post-checklist: - - If the PR is ready for human review, you need more information, or you cannot make progress, post a plain-English PR comment explaining the current state. Maintainers can resume the PR later with `/kelos pick-up`. When all review feedback was already addressed in previous commits and no new comments require action, keep the explanation brief (e.g. "All review feedback was addressed in previous commits. Ready for re-review.") instead of repeating a full status breakdown. Never post duplicate or near-identical status messages. + - If the PR is ready for human review, you need more information, or you cannot make progress, post a plain-English PR comment explaining the current state. Continue routine follow-ups through the Session's web or terminal clients. When all review feedback was already addressed in previous commits and no new comments require action, keep the explanation brief (e.g. "All review feedback was addressed in previous commits. Ready for re-review.") instead of repeating a full status breakdown. Never post duplicate or near-identical status messages. - If you find follow-up work outside the current PR scope, you may create a separate GitHub issue: - First check existing issues with `gh issue list` to avoid duplicates. - Keep the follow-up narrowly scoped and label it `generated-by-kelos`. - - Follow-up issues are exempt from the per-TaskSpawner issue slot cap. + - Follow-up issues are exempt from autonomous discovery issue slot caps. diff --git a/self-development/kanon/kanon-workers.yaml b/self-development/kanon/kanon-workers.yaml index 63b761f2..4ea6a36a 100644 --- a/self-development/kanon/kanon-workers.yaml +++ b/self-development/kanon/kanon-workers.yaml @@ -1,37 +1,5 @@ apiVersion: kelos.dev/v1alpha2 -kind: AgentConfig -metadata: - name: kanon-workers-agent -spec: - agentsMD: | - # Kanon Worker Agent - - Kanon (github.com/kelos-dev/kanon) is a Go CLI that manages coding-agent - settings (instructions, skills, MCP servers, hooks, permissions) across - multiple machines. - - ## Environment - You are running in an ephemeral container environment. Your file system - changes disappear after the task completes. Persist work by creating PRs, - issues, or comments. - - ## Identity - - When commenting on issues or PRs, always start with "🤖 **Kelos Worker Agent** @gjkim42\n\n" - so it is clearly distinguishable from human comments and triggers a notification - - ## Standards - - Do not create duplicate issues — check existing issues first with `gh issue list` - - Keep changes minimal and focused - - ## Project Conventions - - Use Makefile targets instead of discovering build/test commands yourself: - - `make verify` — verify formatting, modules, vet, and tests - - `make update` — run formatters and update module metadata - - `make test` — run all unit tests - - `make build` — build the kanon binary ---- -apiVersion: kelos.dev/v1alpha2 -kind: TaskSpawner +kind: SessionSpawner metadata: name: kanon-workers spec: @@ -49,10 +17,13 @@ spec: commentOn: Issue state: open author: gjkim42 - reporting: - enabled: true - maxConcurrency: 8 - taskTemplate: + sessionTemplate: + volumeClaimTemplate: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 10Gi worker: workspaceRef: name: kanon-agent @@ -82,25 +53,17 @@ spec: value: "gjkim042@gmail.com" agentConfigRefs: - name: base-agent - - name: kanon-workers-agent - ttlSecondsAfterFinished: 864000 - podFailurePolicy: - rules: - - action: Ignore - onPodConditions: - - type: DisruptionTarget - status: "True" - - action: FailJob - onExitCodes: - operator: NotIn - values: [0] - branch: "kanon-task-{{.Number}}" - promptTemplate: | + initialBranch: "kanon-task-{{.Number}}" + initialPrompt: | You are a coding agent. You either - create a PR to fix the issue - update an existing PR to fix the issue - comment on the issue or the PR if you cannot fix it + Your workspace is backed by a PVC. Files remain available across follow-ups + and pod restarts while the Session and its PVC exist. Commit and push work + that must outlive the Session. + Webhook: - Event: {{.Event}} - Action: {{.Action}} @@ -152,4 +115,4 @@ spec: create a separate GitHub issue: - First check existing issues with `gh issue list` to avoid duplicates. - Keep the follow-up narrowly scoped and label it `generated-by-kelos`. - - Follow-up issues are exempt from the per-TaskSpawner issue slot cap. + - Follow-up issues are exempt from autonomous discovery issue slot caps. diff --git a/self-development/kelos-pr-responder.yaml b/self-development/kelos-pr-responder.yaml index 9bc9c692..2fc138d8 100644 --- a/self-development/kelos-pr-responder.yaml +++ b/self-development/kelos-pr-responder.yaml @@ -1,5 +1,5 @@ apiVersion: kelos.dev/v1alpha2 -kind: TaskSpawner +kind: SessionSpawner metadata: name: kelos-pr-responder spec: @@ -24,11 +24,13 @@ spec: state: open draft: false author: gjkim42 - reporting: - enabled: true - checks: {} - maxConcurrency: 8 - taskTemplate: + sessionTemplate: + volumeClaimTemplate: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 10Gi worker: workspaceRef: name: kelos-agent @@ -58,22 +60,14 @@ spec: value: "gjkim042@gmail.com" agentConfigRefs: - name: base-agent - - name: kelos-dev-agent - ttlSecondsAfterFinished: 864000 - podFailurePolicy: - rules: - - action: Ignore - onPodConditions: - - type: DisruptionTarget - status: "True" - - action: FailJob - onExitCodes: - operator: NotIn - values: [0] - branch: "{{.Branch}}" - promptTemplate: | + initialBranch: "{{.Branch}}" + initialPrompt: | You are a coding agent updating an existing PR in response to review feedback. + Your workspace is backed by a PVC. Files remain available across follow-ups + and pod restarts while the Session and its PVC exist. Commit and push work + that must outlive the Session. + Webhook: - Event: {{.Event}} - Action: {{.Action}} @@ -106,9 +100,9 @@ spec: - 9. After pushing, actively check CI status with `gh pr checks {{.Number}}` (if `KELOS_UPSTREAM_REPO` is set, add `--repo "$KELOS_UPSTREAM_REPO"`). If any checks fail, investigate the failures, fix them, commit and push, then check again. Do not request review until CI passes. Post-checklist: - - If the PR is ready for human review, you need more information, or you cannot make progress, post a plain-English PR comment explaining the current state. Maintainers can resume the PR later with `/kelos pick-up`. When all review feedback was already addressed in previous commits and no new comments require action, keep the explanation brief (e.g. "All review feedback was addressed in previous commits. Ready for re-review.") instead of repeating a full status breakdown. Never post duplicate or near-identical status messages. + - If the PR is ready for human review, you need more information, or you cannot make progress, post a plain-English PR comment explaining the current state. Continue routine follow-ups through the Session's web or terminal clients. When all review feedback was already addressed in previous commits and no new comments require action, keep the explanation brief (e.g. "All review feedback was addressed in previous commits. Ready for re-review.") instead of repeating a full status breakdown. Never post duplicate or near-identical status messages. - If you find follow-up work outside the current PR scope, you may create a separate GitHub issue: - First check existing issues with `gh issue list` to avoid duplicates. - Keep the follow-up narrowly scoped and label it `generated-by-kelos`. - - Follow-up issues are exempt from the per-TaskSpawner issue slot cap. + - Follow-up issues are exempt from autonomous discovery issue slot caps. diff --git a/self-development/kelos-workers.yaml b/self-development/kelos-workers.yaml index 496efe8a..c91a12fd 100644 --- a/self-development/kelos-workers.yaml +++ b/self-development/kelos-workers.yaml @@ -13,13 +13,17 @@ spec: filters: - event: issue_comment action: created + bodyPattern: '(?m)^/kelos pick-up[ \t]*\r?$' + commentOn: Issue + state: open + author: gjkim42 sessionTemplate: - initialBranch: '{{with index . "Branch"}}{{.}}{{else}}kelos-task-{{.Number}}{{end}}' + initialBranch: "kelos-task-{{.Number}}" initialPrompt: | - You are a coding agent responding to a new GitHub conversation comment. You either - - create a PR to address an issue - - update an existing PR to address the comment - - comment on the issue or PR if no code change is appropriate or you cannot proceed + You are a coding agent. You either + - create a PR to fix the issue + - update an existing PR to fix the issue + - comment on the issue or the PR if you cannot fix it Your workspace is backed by a PVC. Files remain available across follow-ups and pod restarts while the Session and its PVC exist. Commit and push work @@ -30,46 +34,29 @@ spec: - Action: {{.Action}} - Sender: {{.Sender}} - URL: {{.URL}} - - Number: {{.Number}} - - Title: {{.Title}} - - Branch: {{with index . "Branch"}}{{.}}{{else}}(issue; use kelos-task-{{.Number}}){{end}} - - Comment URL: {{.CommentURL}} - - Comment body: - - {{.CommentBody}} - - - Treat the triggering comment as the immediate request in the context of - the full issue or PR conversation. `/kelos pick-up` means to take ownership - of the complete issue or PR. Other comments may request an incremental - change, ask a question, or provide information; do not invent code work - when a plain-English response is the appropriate outcome. Task: - - 0. Determine whether #{{.Number}} is an issue or a pull request and refresh all context: - - First try `gh pr view {{.Number}} --comments`. If it succeeds, the triggering subject is that PR. Read all reviews and inline review comments with `gh api` as well. - - Otherwise, re-read the issue and all comments with `gh issue view {{.Number}} --comments`, including linked issues and PRs. + - 0. Refresh the latest issue state: + - Re-read the issue and all comments: `gh issue view {{.Number}} --comments` - 1. Fetch and rebase on the latest main branch: ``` git fetch --unshallow || true; git fetch origin main; git rebase origin/main ``` - - 2. Identify the work PR: - - For a triggering PR comment, use PR #{{.Number}}; Kelos already checked out its head branch. - - For a triggering issue comment, check whether a PR already exists for branch kelos-task-{{.Number}}. + - 2. Check if a PR already exists for branch kelos-task-{{.Number}}. - If a work PR exists: + If a PR already exists: - 3a. Read ALL review comments and conversation on the PR (gh pr view, gh api for review comments). - - Also read linked issues and their comments, if any, rather than relying only on the PR conversation. + - Also read ALL comments on the linked issue (gh issue view {{.Number}} --comments), not just the PR. - 4a. Read the existing diff (`git diff origin/main...HEAD`) to understand what has already been done. Do NOT start over or rewrite from scratch. - - 5a. Address the triggering comment and any valid unresolved feedback with the smallest incremental change. Preserve existing work. - - 6a. Commit and push your changes to the current PR branch. + - 5a. Make only the incremental changes needed to address review feedback or remaining issues. Preserve existing work. + - 6a. Commit and push your changes to origin kelos-task-{{.Number}}. - 7a. Run a local review of the PR diff against `origin/main` using the review tool available in the current environment. If no dedicated review tool is available, manually inspect the diff for correctness. If issues are found, fix them, run `make verify` and `make test`, commit and push, then repeat the local review. Continue until the local review passes. - - 8a. Update the PR title and description to reflect the final diff. For work originating from an issue, the PR body MUST contain a standard closing keyword reference to that issue on its own line (e.g., `Fixes #{{.Number}}` or `Closes #{{.Number}}`). For a triggering PR comment, preserve its valid issue references and never add a self-closing reference such as `Fixes #{{.Number}}`. Ensure the PR has labels "generated-by-kelos" and "ok-to-test" (use `gh pr edit --add-label generated-by-kelos --add-label ok-to-test` if missing). + - 8a. Update the PR title and description to reflect the final diff. The PR body MUST contain a standard closing keyword reference on its own line (e.g., `Fixes #{{.Number}}` or `Closes #{{.Number}}`). Do not embed the issue number in natural language. Ensure the PR has labels "generated-by-kelos" and "ok-to-test" (use `gh pr edit kelos-task-{{.Number}} --add-label generated-by-kelos --add-label ok-to-test` if missing). - 9a. After pushing, actively check CI status with `gh pr checks`. If any checks fail, investigate the failures, fix them, commit and push, then check again. - - 10a. If the branch has more than one commit (`[ "$(git rev-list --count origin/main..HEAD)" -gt 1 ]`), squash all commits on the branch into a single commit before requesting external review. Use `git reset --soft $(git merge-base HEAD origin/main)` followed by `git commit` and `git push --force-with-lease origin HEAD:$(git branch --show-current)`. Do not use `git rebase -i` — interactive commands are not supported. Skip the squash and force-push entirely if the branch is already a single commit, to avoid an unnecessary CI re-run. - - 11a. After the squash + force-push (if any), actively check CI status with `gh pr checks` again. If any checks fail, investigate the failures, fix them, commit and push, then check again. Do not request external review until CI passes. Then request a review by posting a comment on the PR — `/kelos api-review` if `git diff origin/main...HEAD --name-only` includes any path under `api/` (CRD types, kubebuilder markers, generated CRD YAML), otherwise `/kelos review`. Use `gh pr comment --body "/kelos api-review"` (or `--body "/kelos review"`). The dedicated reviewer agents will post their findings asynchronously as a sticky PR comment. + - 10a. If the branch has more than one commit (`[ "$(git rev-list --count origin/main..HEAD)" -gt 1 ]`), squash all commits on the branch into a single commit before requesting external review. Use `git reset --soft $(git merge-base HEAD origin/main)` followed by `git commit` and `git push --force-with-lease origin kelos-task-{{.Number}}`. Do not use `git rebase -i` — interactive commands are not supported. Skip the squash and force-push entirely if the branch is already a single commit, to avoid an unnecessary CI re-run. + - 11a. After the squash + force-push (if any), actively check CI status with `gh pr checks` again. If any checks fail, investigate the failures, fix them, commit and push, then check again. Do not request external review until CI passes. Then request a review by posting a comment on the PR — `/kelos api-review` if `git diff origin/main...HEAD --name-only` includes any path under `api/` (CRD types, kubebuilder markers, generated CRD YAML), otherwise `/kelos review`. Use `gh pr comment kelos-task-{{.Number}} --body "/kelos api-review"` (or `--body "/kelos review"`). The dedicated reviewer agents will post their findings asynchronously as a sticky PR comment. - If no work PR exists (issue comments only): + If no PR exists: - 3b. Investigate the issue #{{.Number}}. - Read ALL comments on the issue (gh issue view {{.Number}} --comments), including maintainer feedback and linked issues/PRs. - If previous PRs for this issue were closed, read their reviews to understand why before choosing your approach. @@ -85,7 +72,7 @@ spec: - 11b. After the squash + force-push (if any), actively check CI status with `gh pr checks` again. If any checks fail, investigate the failures, fix them, commit and push, then check again. Do not request external review until CI passes. Then request a review by posting a comment on the PR — `/kelos api-review` if `git diff origin/main...HEAD --name-only` includes any path under `api/` (CRD types, kubebuilder markers, generated CRD YAML), otherwise `/kelos review`. Use `gh pr comment kelos-task-{{.Number}} --body "/kelos api-review"` (or `--body "/kelos review"`). The dedicated reviewer agents will post their findings asynchronously as a sticky PR comment. Post-checklist: - - Post a plain-English status comment on the source issue or PR (`gh issue comment {{.Number}} --body "..."`) for any of the following situations, explaining what happened: + - Post a plain-English status comment on the issue (`gh issue comment {{.Number}} --body "..."`) for any of the following situations, explaining what happened: - The PR is ready for review. - You commented on the issue or the PR for more information. - You cannot make any progress on the issue, explain why. @@ -93,7 +80,7 @@ spec: create a separate GitHub issue: - First check existing issues with `gh issue list` to avoid duplicates. - Keep the follow-up narrowly scoped and label it `generated-by-kelos`. - - Follow-up issues are exempt from the per-TaskSpawner issue slot cap. + - Follow-up issues are exempt from autonomous discovery issue slot caps. volumeClaimTemplate: accessModes: - ReadWriteOnce