Skip to content

Add optional staged task dependencies - #100

Open
Guidance78 wants to merge 1 commit into
NateBJones-Projects:mainfrom
Guidance78:feature/staged-dependencies
Open

Add optional staged task dependencies#100
Guidance78 wants to merge 1 commit into
NateBJones-Projects:mainfrom
Guidance78:feature/staged-dependencies

Conversation

@Guidance78

Copy link
Copy Markdown

The problem I hit

Ringer runs tasks as a flat set — everything is eligible at once. I run producer
tasks followed by an independent QC reviewer on a different model family, and
there's no way to say "review this one after it builds."

So every stage becomes two runs with me in the middle, doing nothing but
watching for the first one to finish and then launching the second. The
expensive part isn't the worker tokens, it's the directing model reloading
context to do dispatch work it could have declared up front.

The change

An optional task field:

{"key": "schema-qc", "depends_on": ["schema-producer"]}
  • Tasks without depends_on behave exactly as they do today.
  • A dependent waits before acquiring a max_parallel slot, never while
    holding one, so a two-stage manifest at max_parallel: 1 can't deadlock.
  • It runs only after every prerequisite reaches a final PASS — a producer that
    fails attempt 1 and passes attempt 2 has passed.
  • If a prerequisite doesn't pass, the dependent is skipped: no worker
    launched, no attempt consumed, and no eval row for a model that never ran.
    Skips propagate down the chain.
  • Waiting, skipped and blocked_by show up truthfully in run state, the
    progress bar, the briefings and the summary.
  • Missing keys, self-dependencies, duplicates, non-string entries, bare strings,
    dicts and cycles are all rejected at parse time, with the cycle path named.

Every task publishes its terminal result exactly once from a finally, so
preparation failures, timeouts, exhausted attempts, cancellation and unexpected
exceptions release waiters instead of hanging the run.

Deliberately not in this PR

No cross-run dependencies, no conditional workflow language, no dynamic task
generation, no automatic artifact routing or patch application. depends_on is
a control dependency only. Because a passing task's worktree can be removed
before its dependent starts, anything the dependent needs has to be exported by
the producer's check to a durable path outside the worktree — documented beside
the field.

Proof

  • 27 new tests in tests/test_dependencies.py, all observed failing before the
    implementation existed.
  • Disabling the dependency wait turns 7 of them red, including the fan-in
    ordering test — they detect the feature's absence rather than asserting on end
    status.
  • Full suite: RINGER_NO_SELF_UPDATE=1 python3 -m unittest discover -s tests
    280 pass on Python 3.12.
  • git diff --check clean.
  • Also exercised end to end on a real run: a producer that failed left its
    dependent skipped with 0 attempts, 0 tokens and no eval row, while a
    separate passing producer released its reviewer normally.

Ringer runs manifest tasks as a flat set: every task is eligible at once.
There is no way to say "review this after it builds", so a producer and
its QC reviewer have to be two separate runs with a human or a directing
model in between, doing nothing but watching for the first to finish.

Tasks can now declare depends_on, a list of task keys that must all reach
a final PASS first:

    {"key": "schema-qc", "depends_on": ["schema-producer"], ...}

Manifests without the field keep their current execution path exactly.

A dependent waits before acquiring a max_parallel slot, never while
holding one, so a two-stage manifest at max_parallel=1 cannot deadlock.
When a prerequisite does not pass, the dependent is recorded skipped: no
worker is launched, no attempt is consumed, and no eval row is written
for a model that never ran. Skips propagate down the chain, and waiting,
skipped and blocked_by are shown truthfully in run state, the progress
bar, the briefings and the summary.

Every task publishes its terminal result exactly once from a finally
block, so preparation failures, timeouts, exhausted attempts, cancellation
and unexpected exceptions all release waiters instead of hanging the run.

Missing keys, self-dependencies, duplicates, non-string entries, bare
strings, dicts and cycles are rejected while the manifest is parsed, with
the cycle path named in the error.

depends_on is a control dependency only. A passing task's worktree can be
removed before its dependent starts, so any output a dependent needs must
be exported by the producer's check to a durable path outside the
worktree. That limitation is documented beside the field.

Proof: 27 new tests in tests/test_dependencies.py, all observed failing
before the implementation existed. Disabling the dependency wait turns 7
of them red, including the fan-in ordering test, so they detect the
feature's absence rather than asserting on end status. Full suite 280
pass on Python 3.12.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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