π€ Kelos Strategist Agent @gjkim42
Summary
When a Kelos task completes, the only framework-level feedback to the originating source system is a bare status comment ("Task X has succeeded. β
"). All other lifecycle actions β closing issues, adding/removing labels, assigning users β must be performed by the agent itself via CLI commands embedded in the task prompt.
This creates unreliable, inconsistent, and unobservable behavior. This proposal adds a sourceActions field to GitHubReporting (and its Jira/Linear equivalents) that declaratively specifies lifecycle actions to perform on the originating work item when tasks reach terminal phases.
Motivation
The "prompt-embedded lifecycle" problem
Today, virtually every TaskSpawner prompt includes instructions like:
If you successfully create a PR, close the originating issue.
Add the label "agent/completed" to the issue.
Remove the label "actor/kelos" from the issue.
This pattern has five concrete problems:
-
Unreliable: If the agent hits activeDeadlineSeconds, OOMs, or fails mid-execution, lifecycle actions never happen. The controller transitions the Task to Failed (task_controller.go:507-512) and moves on β the issue remains open with stale labels.
-
Inconsistent: Each TaskSpawner prompt handles lifecycle differently. One spawner closes issues on success; another doesn't. One adds labels; another uses different label names. There's no central configuration.
-
Unobservable: The controller has no visibility into whether lifecycle actions were performed. There's no metric, event, or status field tracking label changes or issue state transitions.
-
Duplicated across spawners: Every TaskSpawner prompt repeats the same lifecycle instructions. Changing the label scheme (e.g., renaming agent/completed to kelos/done) requires editing every prompt.
-
Requires credential elevation: For the agent to manage labels and close issues, it needs write access to the repository β even if the core task only needs read access for analysis.
The reporting gap
The current GitHubReporting mechanism (api/v1alpha1/taskspawner_types.go:57-61) is a single boolean:
type GitHubReporting struct {
Enabled bool `json:"enabled,omitempty"`
}
When enabled, the spawner's TaskReporter (internal/reporting/watcher.go:45-127) creates/updates a comment with three fixed templates:
FormatAcceptedComment: "Task X has been accepted and is being processed."
FormatSucceededComment: "Task X has succeeded. β
"
FormatFailedComment: "Task X has failed. β"
These comments contain no task outputs β no PR URL, no branch name, no cost metrics, no error details. And beyond posting this comment, no other actions are taken on the source work item.
Differentiation from existing proposals
Current State Analysis
Source metadata is already propagated
The spawner stamps source metadata onto every task via sourceAnnotations() (cmd/kelos-spawner/main.go:453-473):
annotations := map[string]string{
reporting.AnnotationSourceKind: kind, // "issue" or "pull-request"
reporting.AnnotationSourceNumber: strconv.Itoa(item.Number),
}
The reporting watcher (internal/reporting/watcher.go) already reads these annotations to identify the originating issue/PR when posting comments. This same metadata can drive lifecycle actions.
The spawner already has GitHub API access
The spawner process authenticates to GitHub (via workspace secret or GitHub App token) for source discovery. The same credentials can be reused for lifecycle API calls without requiring additional secrets.
Task outputs are already captured
The controller captures task outputs (internal/controller/output_parser.go) including branch names, PR URLs, and structured results. These values are available in Task.Status.Outputs and Task.Status.Results β but the reporting watcher currently ignores them entirely.
Proposed API Design
Extended GitHubReporting
type GitHubReporting struct {
// Enabled posts standard status comments back to the originating
// GitHub issue or PR. When SourceActions is set, this also controls
// whether actions are performed.
Enabled bool `json:"enabled,omitempty"`
// SourceActions defines lifecycle actions to perform on the originating
// GitHub issue or PR when the spawned task reaches a terminal phase.
// Actions are performed by the spawner process using the workspace's
// GitHub credentials.
// +optional
SourceActions *GitHubSourceActions `json:"sourceActions,omitempty"`
// CommentTemplate overrides the default status comment body.
// Supports Go text/template with variables: {{.TaskName}}, {{.Phase}},
// {{.Outputs}} (map), {{.Results}} (map), {{.Duration}}.
// +optional
CommentTemplate *GitHubCommentTemplate `json:"commentTemplate,omitempty"`
}
// GitHubSourceActions defines per-phase lifecycle actions on GitHub items.
type GitHubSourceActions struct {
// OnSuccess defines actions to perform when the task succeeds.
// +optional
OnSuccess *GitHubActions `json:"onSuccess,omitempty"`
// OnFailure defines actions to perform when the task fails.
// +optional
OnFailure *GitHubActions `json:"onFailure,omitempty"`
}
// GitHubActions defines a set of lifecycle actions on a GitHub issue or PR.
type GitHubActions struct {
// AddLabels adds these labels to the issue/PR.
// +optional
AddLabels []string `json:"addLabels,omitempty"`
// RemoveLabels removes these labels from the issue/PR. Missing labels
// are silently ignored.
// +optional
RemoveLabels []string `json:"removeLabels,omitempty"`
// Close closes the issue or PR. Only applies to open items.
// +optional
Close bool `json:"close,omitempty"`
// Reopen reopens the issue or PR. Only applies to closed items.
// +optional
Reopen bool `json:"reopen,omitempty"`
// Assignees adds these GitHub usernames as assignees.
// +optional
Assignees []string `json:"assignees,omitempty"`
// RemoveAssignees removes these GitHub usernames from assignees.
// +optional
RemoveAssignees []string `json:"removeAssignees,omitempty"`
}
// GitHubCommentTemplate configures templated status comments.
type GitHubCommentTemplate struct {
// Accepted is the template for the comment posted when the task starts.
// +optional
Accepted string `json:"accepted,omitempty"`
// Succeeded is the template for the comment posted when the task succeeds.
// +optional
Succeeded string `json:"succeeded,omitempty"`
// Failed is the template for the comment posted when the task fails.
// +optional
Failed string `json:"failed,omitempty"`
}
Example Configurations
Issue-fixing spawner with full lifecycle management
apiVersion: kelos.dev/v1alpha1
kind: TaskSpawner
metadata:
name: issue-fixer
spec:
when:
githubIssues:
labels: ["actor/kelos"]
reporting:
enabled: true
sourceActions:
onSuccess:
addLabels: ["agent/completed"]
removeLabels: ["actor/kelos", "needs-agent"]
close: true
onFailure:
addLabels: ["agent/failed", "needs-human"]
removeLabels: ["actor/kelos"]
assignees: ["oncall-engineer"]
commentTemplate:
succeeded: |
π€ **Kelos Task Status**
Task `{{.TaskName}}` has **succeeded**. β
{{- if index .Outputs "prURL" }}
**PR**: {{ index .Outputs "prURL" }}
{{- end }}
{{- if index .Results "cost-usd" }}
**Cost**: ${{ index .Results "cost-usd" }}
{{- end }}
failed: |
π€ **Kelos Task Status**
Task `{{.TaskName}}` has **failed**. β
Duration: {{.Duration}}
Manual intervention required.
taskTemplate:
type: claude-code
# ... (prompt no longer needs lifecycle instructions)
PR review spawner with label-based signaling
apiVersion: kelos.dev/v1alpha1
kind: TaskSpawner
metadata:
name: pr-reviewer
spec:
when:
githubPullRequests:
labels: ["needs-review"]
reporting:
enabled: true
sourceActions:
onSuccess:
addLabels: ["review/completed"]
removeLabels: ["needs-review"]
onFailure:
addLabels: ["review/error"]
removeLabels: ["needs-review"]
taskTemplate:
type: claude-code
# ... (agent focuses on review, not label management)
Implementation Notes
Where actions are executed
The reporting watcher (internal/reporting/watcher.go:45) already runs in the spawner process and observes task phase transitions. Source actions would be executed here, immediately after the status comment is posted/updated:
func (tr *TaskReporter) ReportTaskStatus(ctx context.Context, task *Task) error {
// ... existing comment logic ...
// NEW: execute source actions for terminal phases
if desiredPhase == "succeeded" || desiredPhase == "failed" {
if err := tr.executeSourceActions(ctx, task, desiredPhase); err != nil {
log.Error(err, "Failed to execute source actions", "task", task.Name)
// Non-fatal: record event but don't block
}
}
}
GitHub API methods required
All required GitHub API methods are standard REST endpoints:
POST /repos/{owner}/{repo}/issues/{number}/labels β add labels
DELETE /repos/{owner}/{repo}/issues/{number}/labels/{name} β remove labels
PATCH /repos/{owner}/{repo}/issues/{number} β close/reopen (state field)
POST /repos/{owner}/{repo}/issues/{number}/assignees β add assignees
DELETE /repos/{owner}/{repo}/issues/{number}/assignees β remove assignees
The GitHubReporter (internal/reporting/github.go) already implements HTTP requests with token refresh support. Adding these methods follows the same pattern as CreateComment and UpdateComment.
Idempotency
Actions must be idempotent since the watcher may retry:
- Adding a label that already exists: GitHub returns 200 (no-op)
- Removing a label that doesn't exist: GitHub returns 404 (catch and ignore)
- Closing an already-closed issue: GitHub returns 200 (no-op)
- The
AnnotationGitHubReportPhase annotation (watcher.go:34) already prevents duplicate reporting, and the same mechanism prevents duplicate action execution.
Future extension: Jira and Linear
The same SourceActions pattern can extend to Jira and Linear when those sources gain reporting support (a gap noted in #903):
- Jira:
transitions (move to "Done"), addLabels, assignee
- Linear:
state (move to "Done"), addLabels, assignee
The SourceActions type would be source-specific (not generic) to leverage each platform's native lifecycle capabilities.
Backward Compatibility
GitHubReporting.Enabled: true with no sourceActions behaves exactly as today β only posts status comments.
sourceActions is fully optional; existing TaskSpawners require no changes.
- The
commentTemplate extension is additive β when not set, the existing Format*Comment() functions are used.
/kind feature
π€ Kelos Strategist Agent @gjkim42
Summary
When a Kelos task completes, the only framework-level feedback to the originating source system is a bare status comment (
"Task X has succeeded. β "). All other lifecycle actions β closing issues, adding/removing labels, assigning users β must be performed by the agent itself via CLI commands embedded in the task prompt.This creates unreliable, inconsistent, and unobservable behavior. This proposal adds a
sourceActionsfield toGitHubReporting(and its Jira/Linear equivalents) that declaratively specifies lifecycle actions to perform on the originating work item when tasks reach terminal phases.Motivation
The "prompt-embedded lifecycle" problem
Today, virtually every TaskSpawner prompt includes instructions like:
This pattern has five concrete problems:
Unreliable: If the agent hits
activeDeadlineSeconds, OOMs, or fails mid-execution, lifecycle actions never happen. The controller transitions the Task toFailed(task_controller.go:507-512) and moves on β the issue remains open with stale labels.Inconsistent: Each TaskSpawner prompt handles lifecycle differently. One spawner closes issues on success; another doesn't. One adds labels; another uses different label names. There's no central configuration.
Unobservable: The controller has no visibility into whether lifecycle actions were performed. There's no metric, event, or status field tracking label changes or issue state transitions.
Duplicated across spawners: Every TaskSpawner prompt repeats the same lifecycle instructions. Changing the label scheme (e.g., renaming
agent/completedtokelos/done) requires editing every prompt.Requires credential elevation: For the agent to manage labels and close issues, it needs write access to the repository β even if the core task only needs read access for analysis.
The reporting gap
The current
GitHubReportingmechanism (api/v1alpha1/taskspawner_types.go:57-61) is a single boolean:When enabled, the spawner's
TaskReporter(internal/reporting/watcher.go:45-127) creates/updates a comment with three fixed templates:FormatAcceptedComment:"Task X has been accepted and is being processed."FormatSucceededComment:"Task X has succeeded. β "FormatFailedComment:"Task X has failed. β"These comments contain no task outputs β no PR URL, no branch name, no cost metrics, no error details. And beyond posting this comment, no other actions are taken on the source work item.
Differentiation from existing proposals
onCompletionnotification hooks): Proposes sending HTTP POST to arbitrary URLs. This is about outbound notifications to external systems β it doesn't operate on the source work item.GitHubReportingtype, notTaskSpawnerSpec. It leverages the source metadata already tracked via annotations (kelos.dev/source-kind,kelos.dev/source-number).Current State Analysis
Source metadata is already propagated
The spawner stamps source metadata onto every task via
sourceAnnotations()(cmd/kelos-spawner/main.go:453-473):The reporting watcher (
internal/reporting/watcher.go) already reads these annotations to identify the originating issue/PR when posting comments. This same metadata can drive lifecycle actions.The spawner already has GitHub API access
The spawner process authenticates to GitHub (via workspace secret or GitHub App token) for source discovery. The same credentials can be reused for lifecycle API calls without requiring additional secrets.
Task outputs are already captured
The controller captures task outputs (
internal/controller/output_parser.go) including branch names, PR URLs, and structured results. These values are available inTask.Status.OutputsandTask.Status.Resultsβ but the reporting watcher currently ignores them entirely.Proposed API Design
Extended
GitHubReportingExample Configurations
Issue-fixing spawner with full lifecycle management
PR review spawner with label-based signaling
Implementation Notes
Where actions are executed
The reporting watcher (
internal/reporting/watcher.go:45) already runs in the spawner process and observes task phase transitions. Source actions would be executed here, immediately after the status comment is posted/updated:GitHub API methods required
All required GitHub API methods are standard REST endpoints:
POST /repos/{owner}/{repo}/issues/{number}/labelsβ add labelsDELETE /repos/{owner}/{repo}/issues/{number}/labels/{name}β remove labelsPATCH /repos/{owner}/{repo}/issues/{number}β close/reopen (state field)POST /repos/{owner}/{repo}/issues/{number}/assigneesβ add assigneesDELETE /repos/{owner}/{repo}/issues/{number}/assigneesβ remove assigneesThe
GitHubReporter(internal/reporting/github.go) already implements HTTP requests with token refresh support. Adding these methods follows the same pattern asCreateCommentandUpdateComment.Idempotency
Actions must be idempotent since the watcher may retry:
AnnotationGitHubReportPhaseannotation (watcher.go:34) already prevents duplicate reporting, and the same mechanism prevents duplicate action execution.Future extension: Jira and Linear
The same
SourceActionspattern can extend to Jira and Linear when those sources gain reporting support (a gap noted in #903):transitions(move to "Done"),addLabels,assigneestate(move to "Done"),addLabels,assigneeThe
SourceActionstype would be source-specific (not generic) to leverage each platform's native lifecycle capabilities.Backward Compatibility
GitHubReporting.Enabled: truewith nosourceActionsbehaves exactly as today β only posts status comments.sourceActionsis fully optional; existing TaskSpawners require no changes.commentTemplateextension is additive β when not set, the existingFormat*Comment()functions are used./kind feature