Resolves three issues caught in self-review of PR #1886:
1. **Legacy `allSucceeded`/`anySucceeded` consulted verdict** —
`predicate.ts:fromLegacyRoutePredicate`. v1.1's evaluator checked only
`status === "succeeded"` and ignored `verdict`. The new bridge mapped
`allSucceeded → all_pass`, and `all_pass`'s `isStagePassing` prefers
`verdict` over `status`. A stage with `status=succeeded, verdict=neutral`
passed v1.1 routes but fails v1.3 routes — silent backward-compat break.
Fix: attach a non-enumerable `LEGACY_TAG` symbol to bridged `all_pass`
nodes; the evaluator routes through `isStageSucceededByStatus` (status
only) for tagged predicates and `isStagePassing` (verdict-aware) for
direct DSL callers. The tag is a Symbol-keyed prop so it doesn't leak
through JSON serialization (legacy routes are re-bridged on every
evaluation anyway, so the tag is regenerated rather than persisted).
2. **Unscoped DSL leaves inside `routes.when` immediately skipped the
stage at trigger time** — `predicate.ts` + `config-schema.ts`. A leaf
like `{ kind: "no_open_findings" }` (no `stages`) defaults to "all
stages in the run." At trigger time `arePreconditionsTerminal` is
trivially true (`collectReferencedStages` returns nothing to await),
so the scheduler evaluates the predicate against still-pending stages
— including the host stage itself — and cascade-skips it before it can
run. Schema accepted it but operators got a silently-dead stage.
Fix: new `validateRoutePredicateScope` walks the predicate tree and
reports every DSL leaf lacking an explicit, non-empty `stages` scope.
Wired into `ConfiguredPipelineSchema.superRefine` so config load
rejects the shape with a clear message. Legacy shapes are exempt (the
discriminated-union schema already requires `min(1)` stages on those).
Unscoped leaves remain legal in `exitPredicates`, where "all stages"
is a well-defined default at terminal time.
3. **`predicateDepth` returned `-Infinity` on empty combinators** —
`predicate.ts:243-245`. `Math.max(...[])` is `-Infinity`, and the
subsequent `depth > MAX_PREDICATE_DEPTH` check silently passes. Zod
rejects `predicates: []` at parse time, but the TS type permits it,
so programmatically-constructed Predicates (engine validation, tests)
could hit this path.
Fix: short-circuit empty combinators to depth `0`.
## Tests
11 new tests across the existing `pipeline-predicate.test.ts`:
- `allSucceeded` ignores verdict (v1.1 status-only contract)
- `anySucceeded` ignores verdict (v1.1 status-only contract)
- `predicateDepth` returns 0 on empty combinator
- `validateRoutePredicateScope` accept/reject coverage (5 tests)
- `ConfiguredPipelineSchema` route-scope enforcement (4 tests)
All 1386 / 1386 tests passing (previously 1374; +12 new).
## Public API
- New named export: `validateRoutePredicateScope` from `pipeline/index.ts`.
Pure function, no breaking change.