Skip to content

feat(pipeline): real backend runner + control-plane protocol + hardening - #207

Merged
saucam merged 2 commits into
mainfrom
feat/sdlc-pipeline-runtime
Jul 20, 2026
Merged

feat(pipeline): real backend runner + control-plane protocol + hardening#207
saucam merged 2 commits into
mainfrom
feat/sdlc-pipeline-runtime

Conversation

@saucam

@saucam saucam commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator

Combined follow-up to the merged SDLC-pipeline foundation (#204/#205/#206): the review fixes + the real backend runner (PR4) + the control-plane protocol (PR5). Still off by default (config.pipeline.enabled=false) — no behavior change until enabled.

1. Hardening (addresses the review findings)

  • Concurrencyadvance/answer are serialized per pipeline id (a promise chain), so awaited work can't interleave and clobber persisted state once phases run async.
  • Resume re-drivedriveResumable() re-drives pipelines interrupted mid-run (draft/running) on boot; halted pipelines stay parked for a human answer.
  • answer/gate semantics — halt requestId is source-qualified (entry:/exit:/kind:) so a phase with both gates can't collide; documented halt = human-overridable decision point vs abort = final.
  • Shared DB connectionPipelineStore reuses the daemon Store's handle (Store.database) instead of opening a second connection.
  • create() validation — unknown kind/gate/entryGate/skill (or empty phase list) fail fast at creation.
  • listActive() — index-friendly status IN (…) via ACTIVE_STATUSES (with a partition-drift guard test).
  • abort() now fails the unresolved active phase; shared errMessage util.

2. PR4 — real backend runner

SessionPhaseRunner drives a phase's prompt/slash skill on a disposable worker session via a new SessionManager.runPhaseTurn (mirrors the dispatcher spawn; honors per-phase provider/model; awaits idle/error; always tears down). Wired through createPipelineManagerFromConfig with a lazy () => this host thunk; fn skills still run natively. PipelineState gains workdir.

Proven end-to-end with a MockSessionProvider: a skill phase runs a worker turn and captures its final text as the phase summary.

3. PR5 — control-plane protocol

Additive (no PROTOCOL_VERSION bump): pipeline.create/list/get/answer/abort (client→daemon) + pipeline.snapshot/pipeline.list.result (daemon→client) + SessionInfo.phase/profile; zod schemas; pipeline:create/read/answer scopes; daemon handlers with scope + tenancy checks and a serializable wire projection. A client drives a pipeline over the wire and observes a halt via pipeline.get.

Verification

  • tsc --noEmit clean across daemon + protocol + core
  • biome check clean (287 files)
  • 1610 tests pass / 0 fail — incl. new pipeline unit tests, a real-backend runner integration, and the handler control-plane tests

Deferred (clean follow-ups)

  • Halt push-broadcast — the daemon has no central broadcast; a live pipeline.blocker would route through the tenant conductor session. Poll pipeline.get for now.
  • Frontend UIs — Web/TUI/Telegram widgets for the pipeline dashboard + answer-a-blocker.
  • Packs — a pack = a named phases[]; create currently takes explicit phases.

🤖 Generated with Claude Code

Hardening (review fixes): per-pipeline serialization of advance/answer (no lost updates once phases run async); driveResumable() re-drives interrupted draft/running pipelines on boot (halted stay parked); source-qualified halt requestId (entry/exit/kind) + documented halt=human-override vs abort=final; create() validates kinds/gates/skills up front; listActive() uses an index-friendly status IN (...) via ACTIVE_STATUSES; abort() fails the active phase; PipelineStore shares the daemon DB connection (Store.database) instead of a second handle; shared errMessage util.

PR4 (real runner): SessionPhaseRunner drives a prompt/slash skill on a disposable worker session via SessionManager.runPhaseTurn (honors per-phase provider/model, awaits idle/error, tears down), wired through createPipelineManagerFromConfig; fn skills still run natively. PipelineState gains workdir.

PR5 (control plane): additive pipeline.create/list/get/answer/abort + pipeline.snapshot/list.result messages + SessionInfo.phase/profile (no PROTOCOL_VERSION bump); zod schemas; pipeline:create/read/answer scopes; daemon handlers with scope + tenancy checks + wire projection. Halt push-broadcast + frontend UIs deferred (poll pipeline.get to observe a halt).

Still off by default (config.pipeline.enabled=false). Verified: tsc (daemon+protocol+core) clean, biome clean (287 files), 1610 tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment thread src/daemon/session-manager.ts
@github-actions

Copy link
Copy Markdown

🤖 Gemini code review

This PR implements a real backend runner, control-plane protocol, and concurrency hardening for SDLC pipelines. The changes look well-structured and highly robust, with comprehensive unit and integration tests. One high-priority issue was identified in the worker session auth initialization within the backend runner.

Findings: 🔴 0 · 🟠 1 · 🟡 0 · 🟢 0


Tokens spent · ⬆️ Input: 20,453 · ⬇️ Output: 310 · Σ Total: 31,659
Total may be higher due to thinking token counts.

@codecov

codecov Bot commented Jul 20, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.79227% with 5 lines in your changes missing coverage. Please review.
✅ Project coverage is 87.91%. Comparing base (f6d1280) to head (be83f3b).
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
src/daemon/session-manager.ts 97.57% 5 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #207      +/-   ##
==========================================
+ Coverage   87.69%   87.91%   +0.22%     
==========================================
  Files         126      128       +2     
  Lines       21482    21839     +357     
==========================================
+ Hits        18838    19200     +362     
+ Misses       2644     2639       -5     
Flag Coverage Δ
daemon 87.91% <98.79%> (+0.22%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
packages/protocol/src/schemas.ts 100.00% <100.00%> (ø)
packages/protocol/src/scopes.ts 100.00% <100.00%> (ø)
packages/protocol/src/types.ts 100.00% <ø> (ø)
src/daemon/pipeline/engine.ts 100.00% <100.00%> (ø)
src/daemon/pipeline/errors.ts 100.00% <100.00%> (ø)
src/daemon/pipeline/interface.ts 100.00% <100.00%> (ø)
src/daemon/pipeline/manager.ts 100.00% <100.00%> (+4.54%) ⬆️
src/daemon/pipeline/runner.ts 100.00% <100.00%> (ø)
src/daemon/pipeline/store.ts 100.00% <100.00%> (ø)
src/daemon/pipeline/wiring.ts 100.00% <100.00%> (ø)
... and 2 more
🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

…safety hardening

Deep audit of the merged foundation + this PR (4 parallel reviews) surfaced real issues; all folded in with tests.

HIGH: the runtime was inert — added a pipeline.advance verb + handler and wired PipelineManager.driveResumable() into daemon boot (server.ts startPipelines), so created/interrupted pipelines actually run. A non-idle worker turn (error / budget-exhausted waiting_approval / timeout) now FAILS the phase instead of silently passing: runPhaseTurn returns the resting status and SessionPhaseRunner throws on anything but idle. Added a per-turn timeout so a clientless worker can't hang forever + leak. Removed PhaseDef.tools (a documented hard-fence that was never enforced = false security) until the canUseTool fence lands. abort() now routes through the per-id serialization chain (was clobberable by an in-flight advance).

MED: MAX_STEPS exhaustion now fails the pipeline terminally (was stuck 'running'); create() rejects kind:skill with no skill id and duplicate phase ids; the engine hands plugins a cloned view (a hostile kind/gate can't corrupt the transition); pipeline.create validates workdir + per-phase provider up front (fail fast, like session.create). Cache evicts terminal pipelines; get()/returns hand out clones.

Tests (+~14): non-idle turn → phase failure (unit + real-backend error-turn integration); pipeline.advance + halt→answer→resume round-trip over the wire; cross-tenant answer/abort → not_found; workdir/provider validation; MAX_STEPS→failed; retry-via-gate exhaustion; plugin-mutation isolation; driveResumable re-driving a crashed running pipeline; get() isolation; skill-id/duplicate-id validation.

Verified: tsc (daemon+protocol+core) clean, biome clean (287 files), 1624 tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@saucam
saucam merged commit 3959acc into main Jul 20, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant