Second batch of v1 features. With this, the `provides`/`emits` token
infrastructure that has lived in OrcaAction since v0 finally has
control-flow consequences: tokens emitted by one action gate the
acceptance of subsequent actions in the same run.
Gate semantics:
- `unless: T[]` — declared on actions that should not run again if
a sibling action already produced their precondition (idempotency
guard). When any token in `unless` is present, the action is
SKIPPED with reason `unless-triggered:<token>`.
- `abortOn: T[]` — declared on actions that must halt when an upstream
flagged danger. When any token in `abortOn` is present, the action
is BLOCKED (distinct from skipped) with reason
`abort-on-triggered:<token>`.
- `after: T[]` — declared on actions whose work depends on tokens
emitted by upstream actions. When any required token is missing,
the action is SKIPPED with reason
`after-not-met:<missing1>,<missing2>,…`.
Evaluation order is deliberate: unless first (idempotency), abortOn
second (halt signal), after third (weakest reason to skip). The first
gate that fires short-circuits the action; later gates are not
evaluated. The FINALLY stage bypasses all gates so cleanup work runs
unconditionally.
Engine changes:
- New evaluateGates(action, tokens, now) helper produces a
synthesized OrcaActionRun when a gate fires, or null when the
action should proceed.
- executeRun calls evaluateGates() right after the trace-aborted
short-circuit and before the per-action AbortController is set up.
Gated actions emit either ACTION_SKIPPED or ACTION_BLOCKED
diagnostics and never reach runAction().
- The "run aborted between stages" path now also emits an
ACTION_BLOCKED diagnostic with reason 'run-aborted', so blocked
actions are observable in logs regardless of cause.
- OrcaActionRun.interruptedReason renamed to OrcaActionRun.reason —
the field carries gate, reentry, or authored reasons uniformly
across SKIPPED, BLOCKED, INTERRUPTED. The status determines what
kind of reason it is.
New constants exported from $orca:
- ORCA_GATE_REASON_AFTER_NOT_MET
- ORCA_GATE_REASON_UNLESS_TRIGGERED
- ORCA_GATE_REASON_ABORT_ON_TRIGGERED
- ORCA_GATE_REASON_RUN_ABORTED
- ORCA_DIAGNOSTIC_EVENTS.ACTION_BLOCKED
Tests (+11):
v1 — after gate (3):
- skips an action whose `after` token is missing
- runs the action when every `after` token has been emitted
- reports every missing token in the reason when partially met
v1 — unless gate (2):
- skips an action when any `unless` token is present
- runs the action when no `unless` token is present
v1 — abortOn gate (3):
- blocks an action when an abortOn token is present
- runs the action when no abortOn token is present
- emits orca.action.blocked diagnostic with the abortOn reason
v1 — gate precedence and FINALLY bypass (3):
- unless wins over after when both would short-circuit
- abortOn wins over after when both would short-circuit
- FINALLY stage bypasses gates so cleanup always runs
The legacy "accepts after/unless/abortOn without enforcing" forward-
compat tests from the v0 ignored-fields block are removed; v0 promise
is now v1 reality. The orcaInterrupted-reason test was updated to read
the renamed `reason` field.
Verification: 1381/1381 tests pass (71 in orca, +11 from this commit
on top of 63 from the previous v1 commits).
README updated: gates moved from "Roadmap v1" to "Ya en el motor (de
v1)". Remaining v1 items: compensate, commit/replace queue policies,
transaction groups, parallel, payload-bearing tokens, fan-in, bus
interception (Option B), validate()/commit() static graph validation,
and createActiveOrca() reactive wrapper.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>