Skip to content

Report TaskPipeline stage results to the originating Slack thread - #1855

Open
knechtionscoding wants to merge 3 commits into
kelos-dev:mainfrom
datagravity-ai:feat/pipeline-slack-reporting
Open

knechtionscoding wants to merge 3 commits into
kelos-dev:mainfrom
datagravity-ai:feat/pipeline-slack-reporting

Conversation

@knechtionscoding

@knechtionscoding knechtionscoding commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

What type of PR is this?

/kind feature

What this PR does / why we need it:

Stacked on #1853 and #1854. Only the top commit is new; review that one.
This closes the loop from #1761: results from a Slack-started TaskPipeline go
back to the thread that started it.

  • Stage Tasks inherit Slack reporting. Each stage Task gets the pipeline's
    kelos.dev/slack-reporting label and the kelos.dev/slack-reporting,
    -channel, -thread-ts and -user-id annotations. Let Slack TaskSpawners create TaskPipelines #1854 sets these on
    pipelines it creates from Slack messages.
  • The reporter is unchanged. internal/reporting already reports any Task
    that carries these keys, so each stage that runs posts its acceptance,
    progress and result into the thread. Skipped stages create no Task and post
    nothing.
  • Routing reads well in the thread. The route stage's one-word answer acts
    as the acknowledgement, and the selected handler posts the real answer.
  • Only those keys are copied. Reporter state (slack-report-phase), the
    spawner label (which would double count activeTasks), and unrelated
    metadata stay on the pipeline.
  • New example. examples/19-slack-pipeline-router is a front door that
    classifies each Slack request and runs one of three handlers, or a NotIn
    fallback.

This is the third of three stacked PRs:

  1. Add conditional stages to TaskPipeline #1853: conditional stages.
  2. Let Slack TaskSpawners create TaskPipelines #1854: TaskSpawner pipelineTemplate.
  3. This PR: Slack reporting for stage Tasks.

Which issue(s) this PR is related to:

Part of #1760. Depends on #1853 and #1854.

Special notes for your reviewer:

  • Every stage that runs posts to the thread. For two-stage routing that is
    the acknowledgement plus the answer. Longer pipelines will be noisier, and a
    per-stage way to opt out of reporting could follow if needed. I kept it out
    so the API stays minimal.
  • Why the keys are duplicated. The controller keeps its own copy of the
    four keys instead of importing internal/reporting, which would pull
    slack-go into the controller binary for the first time.
    TestPipelineSlackReportingKeysMatchReporter pins them to the reporter's
    constants so they can't drift.
  • GitHub reporting is not propagated. Pipeline spawners are Slack-only
    (Let Slack TaskSpawners create TaskPipelines #1854), and GitHub comment and check reporting needs its own thought about
    one comment or check per stage.

Testing:

  • make verify, make test, and make test-integration are clean
    (180/180 specs, plus 12/12 install specs).
  • New unit tests cover:
    • every stage Task, across a conditional branch, carries exactly the Slack
      keys and not reporter state, the spawner label, or unrelated annotations;
    • a pipeline without reporting yields stage Tasks with no annotations or
      reporting label;
    • the key constants match internal/reporting.
  • The example is validated by an integration dry-run spec.
  • Slack delivery itself isn't covered by e2e, which has no Slack workspace.

Does this PR introduce a user-facing change?

TaskPipeline stage Tasks inherit the pipeline's Slack reporting metadata, so a
pipeline started by a Slack TaskSpawner posts each stage's status and result to
the originating thread.

🤖 Generated with Claude Code


Summary by cubic

TaskPipeline stages started from a Slack message now report each stage's status and result back to the thread that started the pipeline.

Stage Tasks inherit the pipeline's Slack reporting label and annotations (slack-reporting, slack-channel, slack-thread-ts, slack-user-id), so any stage that runs posts to the thread exactly like a spawned Task. Skipped stages create no Task and post nothing, and the new example examples/19-slack-pipeline-router routes each request through a classification stage to one of several handlers.

  • Only those four keys are copied from the pipeline; reporter state and the spawner label stay behind.
  • The controller keeps its own key constants instead of importing internal/reporting, pinned by a test so they can't drift.
  • GitHub comment and check reporting is not propagated to stage Tasks.
  • Adds conditional stage support (In/NotIn conditions) and TaskSpawner.spec.pipelineTemplate, which the stacked PRs introduce and this one reports on.

Written for commit 4f62567. Summary will update on new commits.

View guided diff

A stage can now declare conditions that gate it on earlier stages'
results. Each condition renders a Go template input with .Stages, trims
whitespace, and compares it against a value list with In or NotIn. A
stage whose conditions are not met is Skipped: it creates no Tasks and
counts as complete for the stages after it and for pipeline success.

This lets one pipeline pick a branch at runtime, for example a routing
stage whose agent answer selects which of several stages runs, with a
NotIn stage as the fallback.

Pipeline templates also gain b64dec, so prompts and conditions can read
the base64-encoded response result directly.
A TaskSpawner can now set spec.pipelineTemplate instead of
spec.taskTemplate. For each matching Slack message it creates a
TaskPipeline with the template's stages, so a pipeline with conditional
stages can act as a front door that routes each request.

TaskPipeline gains spec.params, an immutable string map exposed to stage
prompt, branch, and condition templates as .Params. The spawner fills it
from the work item's string template variables.

The created TaskPipeline is named and deduplicated the way the Task would
have been, carries the spawner label, owner reference, and Slack
reporting annotations, and counts toward status.activeTasks and
maxConcurrency. taskTemplate becomes optional, with CEL requiring exactly
one of the two templates, so existing manifests are unaffected.

pipelineTemplate is limited to the slack source and rejects
spec.credentials; manual runs return an error for such spawners. A
v1alpha1 round-trip keeps it in a preservation annotation.
Stage Tasks now inherit the TaskPipeline's Slack reporting label and
annotations (kelos.dev/slack-reporting, -channel, -thread-ts, -user-id).
A pipeline started from a Slack message therefore reports each stage
that runs back to the thread, the same way a spawned Task does, with no
change to the reporter. Skipped stages create no Task and post nothing.

Only those keys are copied. The controller keeps its own copy of the
keys rather than importing internal/reporting, and a test pins them to
the reporter's constants.

Adds examples/19-slack-pipeline-router, a Slack front door that
classifies each request and runs one of several handler stages.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

5 issues found across 29 files

You’re at about 99% of the monthly reviewed-line limit. You may want to disable incremental reviews to conserve quota. Reviews will continue until that limit is exceeded. If you need help avoiding interruptions, please contact contact@cubic.dev.

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="internal/taskbuilder/pipeline_test.go">

<violation number="1" location="internal/taskbuilder/pipeline_test.go:74">
P2: This assertion does not verify that `BuildTaskPipeline` sets the controller owner reference: the already-asserted label makes `ObjectBelongsToSpawner` return true through its legacy fallback. Assert the owner reference’s UID, name, kind, API version, and controller flag directly so ownership regressions are caught.</violation>
</file>

<file name="docs/reference.md">

<violation number="1" location="docs/reference.md:1523">
P2: This guarantee is too broad for Slack slash-command work items: slash commands have no originating thread, and `setSlackReporting` intentionally omits the reporting label and thread annotation, so stage Tasks do not report. Limit this statement to message-triggered pipelines or explicitly document that slash-command pipelines do not receive Slack stage reporting.</violation>
</file>

<file name="internal/conversion/taskspawner.go">

<violation number="1" location="internal/conversion/taskspawner.go:143">
P1: Pipeline spawners are down-converted with an empty v1alpha1 `taskTemplate`, but v1alpha1 requires `taskTemplate.type` and `taskTemplate.credentials`. A v1alpha1 read followed by any write is rejected before the annotation can restore `pipelineTemplate`; emit a schema-valid fallback that the restore path recognizes, or do not expose pipeline spawners through v1alpha1.</violation>

<violation number="2" location="internal/conversion/taskspawner.go:164">
P2: This preservation path has no annotation-size guard. A valid v1alpha2 pipeline with large prompts or many stages can exceed Kubernetes’ 256 KiB annotation limit when converted, causing v1alpha1 conversion or write-back to fail; enforce the metadata budget or use a preservation mechanism that is not limited to annotations.</violation>

<violation number="3" location="internal/conversion/taskspawner.go:183">
P2: The restore accepts semantically invalid pipeline templates from the annotation. An edited annotation can therefore produce an invalid v1alpha2 TaskSpawner whose generated TaskPipeline is rejected or repeatedly fails; validate the restored template against its schema constraints or ignore it wholesale.</violation>
</file>

Reply with feedback, questions, or to request a fix.

View guided diff | Re-trigger cubic

if err := setPreservedSlackExcludeFilters(dst, src.Spec.When.Slack); err != nil {
return err
}
if err := setPreservedPipelineTemplate(dst, src.Spec.PipelineTemplate); err != nil {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Pipeline spawners are down-converted with an empty v1alpha1 taskTemplate, but v1alpha1 requires taskTemplate.type and taskTemplate.credentials. A v1alpha1 read followed by any write is rejected before the annotation can restore pipelineTemplate; emit a schema-valid fallback that the restore path recognizes, or do not expose pipeline spawners through v1alpha1.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At internal/conversion/taskspawner.go, line 143:

<comment>Pipeline spawners are down-converted with an empty v1alpha1 `taskTemplate`, but v1alpha1 requires `taskTemplate.type` and `taskTemplate.credentials`. A v1alpha1 read followed by any write is rejected before the annotation can restore `pipelineTemplate`; emit a schema-valid fallback that the restore path recognizes, or do not expose pipeline spawners through v1alpha1.</comment>

<file context>
@@ -133,9 +140,62 @@ func taskSpawnerFromHub(_ context.Context, src *v1alpha2.TaskSpawner, dst *v1alp
 	if err := setPreservedSlackExcludeFilters(dst, src.Spec.When.Slack); err != nil {
 		return err
 	}
+	if err := setPreservedPipelineTemplate(dst, src.Spec.PipelineTemplate); err != nil {
+		return err
+	}
</file context>

if pipeline.Labels[SpawnerLabel] != "chat" {
t.Errorf("spawner label = %q, want chat", pipeline.Labels[SpawnerLabel])
}
if !ObjectBelongsToSpawner(pipeline, "chat", "spawner-uid") {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: This assertion does not verify that BuildTaskPipeline sets the controller owner reference: the already-asserted label makes ObjectBelongsToSpawner return true through its legacy fallback. Assert the owner reference’s UID, name, kind, API version, and controller flag directly so ownership regressions are caught.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At internal/taskbuilder/pipeline_test.go, line 74:

<comment>This assertion does not verify that `BuildTaskPipeline` sets the controller owner reference: the already-asserted label makes `ObjectBelongsToSpawner` return true through its legacy fallback. Assert the owner reference’s UID, name, kind, API version, and controller flag directly so ownership regressions are caught.</comment>

<file context>
@@ -0,0 +1,104 @@
+	if pipeline.Labels[SpawnerLabel] != "chat" {
+		t.Errorf("spawner label = %q, want chat", pipeline.Labels[SpawnerLabel])
+	}
+	if !ObjectBelongsToSpawner(pipeline, "chat", "spawner-uid") {
+		t.Error("built TaskPipeline does not belong to its spawner")
+	}
</file context>

Comment thread docs/reference.md
- The TaskPipeline is named and deduplicated the way the Task would have been,
carries the `kelos.dev/taskspawner` label and the same Slack reporting
annotations, and is owned by the TaskSpawner. Stage Tasks inherit the Slack
reporting annotations, so every stage that runs posts its status and result

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: This guarantee is too broad for Slack slash-command work items: slash commands have no originating thread, and setSlackReporting intentionally omits the reporting label and thread annotation, so stage Tasks do not report. Limit this statement to message-triggered pipelines or explicitly document that slash-command pipelines do not receive Slack stage reporting.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At docs/reference.md, line 1523:

<comment>This guarantee is too broad for Slack slash-command work items: slash commands have no originating thread, and `setSlackReporting` intentionally omits the reporting label and thread annotation, so stage Tasks do not report. Limit this statement to message-triggered pipelines or explicitly document that slash-command pipelines do not receive Slack stage reporting.</comment>

<file context>
@@ -1415,10 +1502,62 @@ bundled skills. Reading an AgentConfig through `kelos.dev/v1alpha1` omits
+- The TaskPipeline is named and deduplicated the way the Task would have been,
+  carries the `kelos.dev/taskspawner` label and the same Slack reporting
+  annotations, and is owned by the TaskSpawner. Stage Tasks inherit the Slack
+  reporting annotations, so every stage that runs posts its status and result
+  to the originating thread. Skipped stages post nothing.
+- The work item's string template variables (for Slack: `ID`, `Title`, `Body`,
</file context>
Suggested change
reporting annotations, so every stage that runs posts its status and result
annotations, so every stage that runs from a Slack message posts its status and result

// API server still rejects a TaskSpawner left without a template.
return
}
dst.PipelineTemplate = &pipelineTemplate

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The restore accepts semantically invalid pipeline templates from the annotation. An edited annotation can therefore produce an invalid v1alpha2 TaskSpawner whose generated TaskPipeline is rejected or repeatedly fails; validate the restored template against its schema constraints or ignore it wholesale.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At internal/conversion/taskspawner.go, line 183:

<comment>The restore accepts semantically invalid pipeline templates from the annotation. An edited annotation can therefore produce an invalid v1alpha2 TaskSpawner whose generated TaskPipeline is rejected or repeatedly fails; validate the restored template against its schema constraints or ignore it wholesale.</comment>

<file context>
@@ -133,9 +140,62 @@ func taskSpawnerFromHub(_ context.Context, src *v1alpha2.TaskSpawner, dst *v1alp
+		// API server still rejects a TaskSpawner left without a template.
+		return
+	}
+	dst.PipelineTemplate = &pipelineTemplate
+	// v1alpha1 always serializes taskTemplate, including its non-pointer
+	// credentials, so a pipeline spawner comes back with an empty template.
</file context>

if dst.Annotations == nil {
dst.Annotations = map[string]string{}
}
dst.Annotations[preservedPipelineTemplateAnnotation] = string(data)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: This preservation path has no annotation-size guard. A valid v1alpha2 pipeline with large prompts or many stages can exceed Kubernetes’ 256 KiB annotation limit when converted, causing v1alpha1 conversion or write-back to fail; enforce the metadata budget or use a preservation mechanism that is not limited to annotations.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At internal/conversion/taskspawner.go, line 164:

<comment>This preservation path has no annotation-size guard. A valid v1alpha2 pipeline with large prompts or many stages can exceed Kubernetes’ 256 KiB annotation limit when converted, causing v1alpha1 conversion or write-back to fail; enforce the metadata budget or use a preservation mechanism that is not limited to annotations.</comment>

<file context>
@@ -133,9 +140,62 @@ func taskSpawnerFromHub(_ context.Context, src *v1alpha2.TaskSpawner, dst *v1alp
+	if dst.Annotations == nil {
+		dst.Annotations = map[string]string{}
+	}
+	dst.Annotations[preservedPipelineTemplateAnnotation] = string(data)
+	return nil
+}
</file context>

This branch is waiting to be deployed

1 waiting deployment
ok-to-test — 4f62567f Waiting Oct 28, 2026 by knechtionscoding via fork-e2e / e2e-with-environment #1981
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

kind/feature Categorizes issue or PR as related to a new feature needs-actor needs-priority needs-triage release-note

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant