Skip to content

Let Slack TaskSpawners create TaskPipelines - #1854

Open
knechtionscoding wants to merge 2 commits into
kelos-dev:mainfrom
datagravity-ai:feat/taskspawner-pipeline-template
Open

knechtionscoding wants to merge 2 commits into
kelos-dev:mainfrom
datagravity-ai:feat/taskspawner-pipeline-template

Conversation

@knechtionscoding

@knechtionscoding knechtionscoding commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

What type of PR is this?

/kind api

What this PR does / why we need it:

Stacked on #1853. Only the top commit is new; review that one. It lets a
Slack TaskSpawner start a TaskPipeline. Combined with #1853's conditional
stages, one Slack front door can route each request to the right agent without
a new CRD, as discussed on #1761.

kind: TaskSpawner
spec:
  when:
    slack:
      channels: ["C0123456789"]
  pipelineTemplate:
    stages:
      - name: route
        taskTemplate:
          workerPoolRef: {name: router-pool}
          prompt: "Reply docs or none: {{.Params.Body}}"
      - name: docs
        conditions:
          - input: '{{index .Stages "route" 0 "Results" "response" | b64dec}}'
            operator: In
            values: [docs]
        taskTemplate: {...}
  • TaskSpawner.spec.pipelineTemplate.stages creates a TaskPipeline for each
    matching message instead of a Task.
    • The pipeline gets the same name and dedupe behavior a Task would.
    • It carries the spawner label, a controller owner reference, and the Slack
      reporting annotations.
    • It counts toward status.activeTasks and maxConcurrency. The
      TaskSpawner controller watches TaskPipelines so the count updates.
  • TaskPipeline.spec.params is an immutable map[string]string, exposed
    as .Params to stage prompt, branch, and condition templates. The spawner
    fills it from the work item's string template variables (for Slack: ID,
    Title, Body, URL, Kind). This sidesteps a template clash: the
    spawner can't fill in request fields at creation time while leaving
    {{.Stages...}} for the pipeline to fill in later.
  • taskTemplate becomes optional. CEL requires exactly one of
    taskTemplate or pipelineTemplate, so every existing manifest still
    applies unchanged.

This is the second of three stacked PRs:

  1. Add conditional stages to TaskPipeline #1853: conditional stages.
  2. This PR: TaskSpawner pipelineTemplate.
  3. Propagate the initiator's Slack reporting onto stage Tasks, so results return to the thread.

Which issue(s) this PR is related to:

Part of #1760. Depends on #1853.

Special notes for your reviewer:

  • Scope is Slack only, by CEL. pipelineTemplate is rejected with any
    other source and with spec.credentials. Each stage sets its own
    worker/workerPoolRef.
    • The polling spawner, webhook handlers, and manual runs each have their own
      Task-creation and dedupe logic. Wiring them is follow-up work, and lifting
      the restriction later only loosens validation.
    • kelos run --from and Console manual runs return an error naming the
      spawner instead of building a broken Task.
  • How taskTemplate became optional. I used json:"taskTemplate,omitzero"
    with +optional, keeping the value type. Converting it to a pointer would
    touch about 98 references. With omitzero, a typed Go client omits a zero
    taskTemplate, which the integration specs exercise with the typed client.
    The three existing spec-level rules that read self.taskTemplate are now
    guarded with !has(self.taskTemplate) || .... One spec checks that the
    inline-credentials rule still fires for a taskTemplate spawner.
  • v1alpha1 round-trip. pipelineTemplate is kept in a
    kelos.dev/v1alpha2-pipeline-template annotation, following the existing
    preservation pattern. v1alpha1 always serializes taskTemplate, including
    its non-pointer credentials. On the way back up, that empty template is
    dropped so the exactly-one rule still holds.
  • CRD size. PipelineStage embeds the full WorkerSpec schema, so the
    TaskSpawner CRD grows from about 844 KB to about 1.09 MB of compact JSON.
    That is under etcd's 1.5 MiB object limit, but worth knowing.
    TestHelmReleaseSecretFitsKubernetesLimit passes.
  • Slack server RBAC gains create/get on taskpipelines.

Testing:

  • make verify, make test, and make test-integration are clean
    (179/179 specs, plus 12/12 install specs).
  • New unit tests:
    • Slack happy path: a TaskPipeline with params, stages, owner, and reporting
      annotations is created, and no Task is created;
    • duplicate delivery dedupe;
    • a name collision with an unowned pipeline;
    • BuildTaskPipeline, including a deep-copy check;
    • ownership checks against a recreated spawner;
    • manual-run rejection;
    • active-pipeline counting and watch mapping;
    • .Params in prompts and conditions;
    • conversion round-trip, malformed annotation, and no-annotation cases.
  • New integration specs:
    • exactly-one-template in both directions;
    • the Slack-only source rule;
    • the credentials rule;
    • duplicate stage names;
    • the existing rule still enforced;
    • params immutability.

Does this PR introduce a user-facing change?

TaskSpawner accepts `spec.pipelineTemplate` for the Slack source, creating a
TaskPipeline for each matching message instead of a Task. TaskPipeline accepts
`spec.params`, exposed to stage templates as `.Params`; pipeline spawners fill
it from the message. `spec.taskTemplate` is now optional, with exactly one of
`taskTemplate` or `pipelineTemplate` required.

🤖 Generated with Claude Code


Summary by cubic

Lets Slack TaskSpawners create TaskPipelines instead of Tasks, so a single Slack front door can route each message to the right agent using conditional stages.

  • spec.pipelineTemplate.stages defines the pipeline for each matching message; the pipeline keeps Task naming, dedupe, spawner label, owner reference, and Slack reporting annotations, and counts toward maxConcurrency and status.activeTasks.
  • TaskPipeline gains spec.params, an immutable string map exposed to stage templates as .Params; the spawner fills it from the message's string template variables.
  • spec.taskTemplate is now optional — exactly one of taskTemplate or pipelineTemplate is required via CEL, so existing manifests still apply unchanged.
  • pipelineTemplate is Slack-only (rejected with other sources and with spec.credentials); manual runs (kelos run --from, Console) return an error naming the spawner.
  • Stages can declare conditions that render an earlier result and compare with In/NotIn; templates gain b64dec to decode base64 results.

Notes

  • pipelineTemplate survives a v1alpha1 round-trip via a preservation annotation; v1alpha1 always serializes a taskTemplate, which is dropped on conversion back so the exactly-one rule holds.
  • The TaskSpawner CRD grows to about 1.09 MB of compact JSON, still within etcd's 1.5 MiB object limit; Slack server RBAC gains create/get on taskpipelines.

Written for commit ee7d947. 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.

@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.

12 issues found across 24 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/slack/pipeline_test.go">

<violation number="1" location="internal/slack/pipeline_test.go:88">
P3: This Slack-to-TaskPipeline integration test leaves `ID`, `Title`, and `Kind` unverified even though they are part of the message parameter contract. Assert those fields here so regressions in the creation path are caught.</violation>
</file>

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

<violation number="1" location="internal/taskbuilder/pipeline_test.go:64">
P2: This stage assertion does not verify the copied `TaskTemplate`, so a regression that loses a prompt or `WorkerPoolRef` still passes. Assert representative prompt and worker-pool values for both stages before mutating the condition slice.</violation>

<violation number="2" location="internal/taskbuilder/pipeline_test.go:74">
P2: This assertion can pass when `BuildTaskPipeline` omits the controller owner reference because `ObjectBelongsToSpawner` falls back to the label. Assert the owner reference’s UID, name, kind, API version, and controller bit directly so ownership regressions fail the test.

(Based on your team's feedback about adding tests for new behavior.)</violation>
</file>

<file name="internal/controller/taskpipeline_conditions_test.go">

<violation number="1" location="internal/controller/taskpipeline_conditions_test.go:93">
P2: Assert that `StageStatuses` contains every expected stage before checking phases; otherwise this test can pass without verifying that skipped stages were recorded at all.</violation>

<violation number="2" location="internal/controller/taskpipeline_conditions_test.go:319">
P2: Scope this helper to the pipeline as well as the stage; otherwise tests with multiple pipelines can update or inspect an unrelated Task and produce misleading results. Include the `taskPipelineLabel` selector using `pipelineLabelValue(pipeline.Name)`.</violation>
</file>

<file name="internal/slack/handler.go">

<violation number="1" location="internal/slack/handler.go:441">
P1: This records Slack reporting metadata on the `TaskPipeline`, but the Slack reporter lists only `Task` objects and pipeline-generated Tasks do not inherit this metadata. Pipeline-template requests therefore create work without sending Slack status or completion replies; propagate the reporting metadata to child Tasks or make the reporter handle TaskPipelines.</violation>
</file>

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

<violation number="1" location="internal/conversion/taskspawner.go:143">
P2: Pipeline spawners are not writable through v1alpha1: conversion returns empty required legacy fields, which the v1alpha1 schema rejects on write-back. Provide a schema-valid fallback that is removed on restoration, or explicitly reject this conversion path.</violation>

<violation number="2" location="internal/conversion/taskspawner.go:164">
P2: This stores the entire pipeline template in one annotation, so valid large v1alpha2 templates can exceed Kubernetes' 256 KiB annotation limit and fail v1alpha1 conversion. Bound the template to the annotation budget or use a preservation mechanism that is not limited by annotation size.</violation>

<violation number="3" location="internal/conversion/taskspawner.go:177">
P2: This restore path accepts semantically invalid pipeline templates. Validate the complete `TaskSpawnerPipelineTemplate` before assigning it, or ignore the annotation when any stage violates the v1alpha2 constraints.</violation>

<violation number="4" location="internal/conversion/taskspawner.go:187">
P2: A v1alpha1 edit to the placeholder task template leaves both templates after restoration, so the v1alpha2 exactly-one validation rejects the write-back. Clear the legacy task template unconditionally when restoring a preserved pipeline template.</violation>
</file>

<file name="examples/07-task-pipeline/README.md">

<violation number="1" location="examples/07-task-pipeline/README.md:23">
P3: The README now documents three pipeline examples but still says “Delete both examples.” Change that wording to “Delete all examples” so the cleanup instructions match the directory contents.</violation>
</file>

<file name="examples/07-task-pipeline/conditional-pipeline.yaml">

<violation number="1" location="examples/07-task-pipeline/conditional-pipeline.yaml:18">
P3: The classifier asks for the default branch, but this example's Workspace is pinned to `main`; repositories using another default branch will classify and review the wrong commit. Refer to the checked-out branch here, or document that users must change `workspace.yaml.spec.ref` to their default branch.</violation>
</file>

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

View guided diff | Re-trigger cubic

Comment thread internal/slack/handler.go
spawnerRef *taskbuilder.SpawnerRef,
) error {
pipeline := h.taskBuilder.BuildTaskPipeline(name, spawner.Namespace, spawner.Spec.PipelineTemplate, templateVars, spawnerRef)
setSlackReporting(pipeline, msg)

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: This records Slack reporting metadata on the TaskPipeline, but the Slack reporter lists only Task objects and pipeline-generated Tasks do not inherit this metadata. Pipeline-template requests therefore create work without sending Slack status or completion replies; propagate the reporting metadata to child Tasks or make the reporter handle TaskPipelines.

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/slack/handler.go, line 441:

<comment>This records Slack reporting metadata on the `TaskPipeline`, but the Slack reporter lists only `Task` objects and pipeline-generated Tasks do not inherit this metadata. Pipeline-template requests therefore create work without sending Slack status or completion replies; propagate the reporting metadata to child Tasks or make the reporter handle TaskPipelines.</comment>

<file context>
@@ -441,6 +425,70 @@ func (h *SlackHandler) createTask(ctx context.Context, spawner *kelos.TaskSpawne
+	spawnerRef *taskbuilder.SpawnerRef,
+) error {
+	pipeline := h.taskBuilder.BuildTaskPipeline(name, spawner.Namespace, spawner.Spec.PipelineTemplate, templateVars, spawnerRef)
+	setSlackReporting(pipeline, msg)
+
+	if err := h.client.Create(ctx, pipeline); err != nil {
</file context>

Comment on lines +64 to +66
if len(pipeline.Spec.Stages) != 2 || pipeline.Spec.Stages[1].Conditions[0].Values[0] != "docs" {
t.Fatalf("stages = %#v, want the template stages", pipeline.Spec.Stages)
}

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 stage assertion does not verify the copied TaskTemplate, so a regression that loses a prompt or WorkerPoolRef still passes. Assert representative prompt and worker-pool values for both stages before mutating the condition slice.

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 64:

<comment>This stage assertion does not verify the copied `TaskTemplate`, so a regression that loses a prompt or `WorkerPoolRef` still passes. Assert representative prompt and worker-pool values for both stages before mutating the condition slice.</comment>

<file context>
@@ -0,0 +1,104 @@
+			t.Errorf("params[%q] = %q, want %q", key, pipeline.Spec.Params[key], value)
+		}
+	}
+	if len(pipeline.Spec.Stages) != 2 || pipeline.Spec.Stages[1].Conditions[0].Values[0] != "docs" {
+		t.Fatalf("stages = %#v, want the template stages", pipeline.Spec.Stages)
+	}
</file context>
Suggested change
if len(pipeline.Spec.Stages) != 2 || pipeline.Spec.Stages[1].Conditions[0].Values[0] != "docs" {
t.Fatalf("stages = %#v, want the template stages", pipeline.Spec.Stages)
}
+\tif len(pipeline.Spec.Stages) != 2 {
+\t\tt.Fatalf("stages = %#v, want the template stages", pipeline.Spec.Stages)
+\t}
+\tif got := pipeline.Spec.Stages[0].TaskTemplate.Prompt; got != "Route {{.Params.Body}}" {
+\t\tt.Errorf("stage 0 prompt = %q, want %q", got, "Route {{.Params.Body}}")
+\t}
+\tif ref := pipeline.Spec.Stages[0].TaskTemplate.WorkerPoolRef; ref == nil || ref.Name != "router" {
+\t\tt.Errorf("stage 0 worker pool = %#v, want router", ref)
+\t}
+\tif got := pipeline.Spec.Stages[1].TaskTemplate.Prompt; got != "{{.Params.Body}}" {
+\t\tt.Errorf("stage 1 prompt = %q, want %q", got, "{{.Params.Body}}")
+\t}
+\tif ref := pipeline.Spec.Stages[1].TaskTemplate.WorkerPoolRef; ref == nil || ref.Name != "docs" {
+\t\tt.Errorf("stage 1 worker pool = %#v, want docs", ref)
+\t}
+\tif got := pipeline.Spec.Stages[1].Conditions[0].Values[0]; got != "docs" {
+\t\tt.Fatalf("stage 1 condition value = %q, want docs", got)
+\t}

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 can pass when BuildTaskPipeline omits the controller owner reference because ObjectBelongsToSpawner falls back to the label. Assert the owner reference’s UID, name, kind, API version, and controller bit directly so ownership regressions fail the test.

(Based on your team's feedback about adding tests for new behavior.)

View Feedback

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 can pass when `BuildTaskPipeline` omits the controller owner reference because `ObjectBelongsToSpawner` falls back to the label. Assert the owner reference’s UID, name, kind, API version, and controller bit directly so ownership regressions fail the test.

(Based on your team's feedback about adding tests for new behavior.) </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>

"incident": kelos.PipelineStagePhaseSkipped,
"fallback": kelos.PipelineStagePhaseSkipped,
}
for _, status := range updated.Status.StageStatuses {

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: Assert that StageStatuses contains every expected stage before checking phases; otherwise this test can pass without verifying that skipped stages were recorded at all.

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/controller/taskpipeline_conditions_test.go, line 93:

<comment>Assert that `StageStatuses` contains every expected stage before checking phases; otherwise this test can pass without verifying that skipped stages were recorded at all.</comment>

<file context>
@@ -0,0 +1,357 @@
+		"incident": kelos.PipelineStagePhaseSkipped,
+		"fallback": kelos.PipelineStagePhaseSkipped,
+	}
+	for _, status := range updated.Status.StageStatuses {
+		if status.Phase != wantPhases[status.Name] {
+			t.Errorf("stage %q phase = %q, want %q", status.Name, status.Phase, wantPhases[status.Name])
</file context>

var tasks kelos.TaskList
if err := k8sClient.List(context.Background(), &tasks,
client.InNamespace(pipeline.Namespace),
client.MatchingLabels{taskPipelineStageLabel: stage},

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: Scope this helper to the pipeline as well as the stage; otherwise tests with multiple pipelines can update or inspect an unrelated Task and produce misleading results. Include the taskPipelineLabel selector using pipelineLabelValue(pipeline.Name).

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/controller/taskpipeline_conditions_test.go, line 319:

<comment>Scope this helper to the pipeline as well as the stage; otherwise tests with multiple pipelines can update or inspect an unrelated Task and produce misleading results. Include the `taskPipelineLabel` selector using `pipelineLabelValue(pipeline.Name)`.</comment>

<file context>
@@ -0,0 +1,357 @@
+	var tasks kelos.TaskList
+	if err := k8sClient.List(context.Background(), &tasks,
+		client.InNamespace(pipeline.Namespace),
+		client.MatchingLabels{taskPipelineStageLabel: stage},
+	); err != nil {
+		t.Fatal(err)
</file context>
Suggested change
client.MatchingLabels{taskPipelineStageLabel: stage},
client.MatchingLabels{taskPipelineLabel: pipelineLabelValue(pipeline.Name), taskPipelineStageLabel: stage},

Comment on lines +187 to +189
if emptyTaskTemplate(dst.TaskTemplate) {
dst.TaskTemplate = v1alpha2.TaskTemplate{}
}

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: A v1alpha1 edit to the placeholder task template leaves both templates after restoration, so the v1alpha2 exactly-one validation rejects the write-back. Clear the legacy task template unconditionally when restoring a preserved pipeline template.

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 187:

<comment>A v1alpha1 edit to the placeholder task template leaves both templates after restoration, so the v1alpha2 exactly-one validation rejects the write-back. Clear the legacy task template unconditionally when restoring a preserved pipeline template.</comment>

<file context>
@@ -133,9 +140,62 @@ func taskSpawnerFromHub(_ context.Context, src *v1alpha2.TaskSpawner, dst *v1alp
+	// v1alpha1 always serializes taskTemplate, including its non-pointer
+	// credentials, so a pipeline spawner comes back with an empty template.
+	// Drop it so the hub object sets exactly one of the two templates.
+	if emptyTaskTemplate(dst.TaskTemplate) {
+		dst.TaskTemplate = v1alpha2.TaskTemplate{}
+	}
</file context>
Suggested change
if emptyTaskTemplate(dst.TaskTemplate) {
dst.TaskTemplate = v1alpha2.TaskTemplate{}
}
dst.TaskTemplate = v1alpha2.TaskTemplate{}

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 stores the entire pipeline template in one annotation, so valid large v1alpha2 templates can exceed Kubernetes' 256 KiB annotation limit and fail v1alpha1 conversion. Bound the template to the annotation budget or use a preservation mechanism that is not limited by annotation size.

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 stores the entire pipeline template in one annotation, so valid large v1alpha2 templates can exceed Kubernetes' 256 KiB annotation limit and fail v1alpha1 conversion. Bound the template to the annotation budget or use a preservation mechanism that is not limited by annotation size.</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>

Comment on lines +88 to +90
if got := pipeline.Spec.Params["URL"]; got != msg.Permalink {
t.Errorf("params.URL = %q, want %q", got, msg.Permalink)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: This Slack-to-TaskPipeline integration test leaves ID, Title, and Kind unverified even though they are part of the message parameter contract. Assert those fields here so regressions in the creation path 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/slack/pipeline_test.go, line 88:

<comment>This Slack-to-TaskPipeline integration test leaves `ID`, `Title`, and `Kind` unverified even though they are part of the message parameter contract. Assert those fields here so regressions in the creation path are caught.</comment>

<file context>
@@ -0,0 +1,154 @@
+	if got := pipeline.Spec.Params["Body"]; got != msg.Body {
+		t.Errorf("params.Body = %q, want %q", got, msg.Body)
+	}
+	if got := pipeline.Spec.Params["URL"]; got != msg.Permalink {
+		t.Errorf("params.URL = %q, want %q", got, msg.Permalink)
+	}
</file context>
Suggested change
if got := pipeline.Spec.Params["URL"]; got != msg.Permalink {
t.Errorf("params.URL = %q, want %q", got, msg.Permalink)
}
if got := pipeline.Spec.Params["URL"]; got != msg.Permalink {
t.Errorf("params.URL = %q, want %q", got, msg.Permalink)
}
if got := pipeline.Spec.Params["ID"]; got != msg.Timestamp {
t.Errorf("params.ID = %q, want %q", got, msg.Timestamp)
}
if got := pipeline.Spec.Params["Title"]; got != msg.Text {
t.Errorf("params.Title = %q, want %q", got, msg.Text)
}
if got := pipeline.Spec.Params["Kind"]; got != "SlackMessage" {
t.Errorf("params.Kind = %q, want %q", got, "SlackMessage")
}

| `workspace.yaml` | Workspace | Git repository to clone |
| `pipeline.yaml` | TaskPipeline | Three-stage feature development workflow |
| `matrix-pipeline.yaml` | TaskPipeline | Matrix fan-out followed by a summary stage |
| `conditional-pipeline.yaml` | TaskPipeline | Classification stage that selects one of two review stages |

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: The README now documents three pipeline examples but still says “Delete both examples.” Change that wording to “Delete all examples” so the cleanup instructions match the directory contents.

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 examples/07-task-pipeline/README.md, line 23:

<comment>The README now documents three pipeline examples but still says “Delete both examples.” Change that wording to “Delete all examples” so the cleanup instructions match the directory contents.</comment>

<file context>
@@ -19,6 +20,7 @@ The pipeline status summarizes progress with bounded per-stage counts.
 | `workspace.yaml` | Workspace | Git repository to clone |
 | `pipeline.yaml` | TaskPipeline | Three-stage feature development workflow |
 | `matrix-pipeline.yaml` | TaskPipeline | Matrix fan-out followed by a summary stage |
+| `conditional-pipeline.yaml` | TaskPipeline | Classification stage that selects one of two review stages |
 
 ## Sequential Pipeline
</file context>

workspaceRef:
name: my-workspace
prompt: |
Read the most recent commit on the default branch. Do not modify the repository.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: The classifier asks for the default branch, but this example's Workspace is pinned to main; repositories using another default branch will classify and review the wrong commit. Refer to the checked-out branch here, or document that users must change workspace.yaml.spec.ref to their default branch.

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 examples/07-task-pipeline/conditional-pipeline.yaml, line 18:

<comment>The classifier asks for the default branch, but this example's Workspace is pinned to `main`; repositories using another default branch will classify and review the wrong commit. Refer to the checked-out branch here, or document that users must change `workspace.yaml.spec.ref` to their default branch.</comment>

<file context>
@@ -0,0 +1,57 @@
+          workspaceRef:
+            name: my-workspace
+        prompt: |
+          Read the most recent commit on the default branch. Do not modify the repository.
+          Reply with exactly one word and no other text:
+          - docs if the commit only changes documentation
</file context>

This branch is waiting to be deployed

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

Labels

kind/api Categorizes issue or PR as related to API changes needs-actor needs-priority needs-triage release-note

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant