feat(pipeline): v1.3 — workspace classes, predicate DSL, exitPredicates
Adds three pieces that round out v1 (issue #1632, sub-task of #1349):
## Typed predicate DSL (pipeline/predicate.ts)
- Leaves: all_pass, no_open_findings, finding_count_below
- Boolean ops: and, or, not
- Recursive Zod schema with nesting depth cap (MAX_PREDICATE_DEPTH=16)
- Pure evaluator over (stages, artifactsByStage) context
- Legacy v1.1 StageRoutePredicate shapes still parse — bridged via
fromLegacyRoutePredicate so existing pipelines need no rewrites
- collectReferencedStages walks both legacy + DSL forms (used by dag.ts
for upstream-ref discovery and by config-schema for ref validation)
## Pipeline.exitPredicates (reducer.ts, dag.ts)
- Replaces hardcoded "allTerminal → done" path in the reducer
- New helper evaluateRunExitOutcome returns succeeded /
failed_exhausted / failed_can_retry based on Pipeline.maxLoopRounds
- Loop state mapping:
* predicates true (or unset) → done (loop_succeeded)
* predicates false, rounds out → stalled (loop_failed)
* predicates false, rounds remain → awaiting_context (re-trigger)
- Emits pipeline.loop.succeeded / pipeline.loop.failed /
pipeline.loop.continuing observations alongside run.terminated
- RunState gains optional runArtifacts buffer so the reducer can
evaluate finding-based predicates without round-tripping the store
## Workspace classes (types.ts, config-schema.ts, stage-prompt.ts, engine.ts)
- Stage.workspaceClass: "independent" (default) | "read-siblings"
- WorkspaceGuard at config load rejects orphan read-siblings stages
(no dependsOn, no route refs, no prior stage in declaration order)
- Engine threads transitive upstream artifacts to read-siblings stages
- buildStagePrompt surfaces them as an Upstream Artifacts JSON block
## CONFIG_CHANGED structural vs tuning (pipeline/config-diff.ts)
- classifyConfigChange returns "none" | "tuning" | "structural"
- Structural: stage list changes, dependsOn / routes / executor edits,
pipeline rename → must terminate the active run
- Tuning: predicate thresholds, retries, budgets, exitPredicates,
maxLoopRounds, workspaceClass → engine can hot-reload
- Short-circuits on first structural diff for cheap classification
## Tests (57 new vitests)
- pipeline-predicate.test.ts (25): schema + evaluator + legacy bridge
- pipeline-workspace.test.ts (10): WorkspaceGuard + prompt injection
- pipeline-exit-predicates.test.ts (5): reducer integration
- pipeline-config-diff.test.ts (17): structural vs tuning classification