Add optional staged task dependencies - #100
Open
Guidance78 wants to merge 1 commit into
Open
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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"]}depends_onbehave exactly as they do today.max_parallelslot, never whileholding one, so a two-stage manifest at
max_parallel: 1can't deadlock.fails attempt 1 and passes attempt 2 has passed.
skipped: no workerlaunched, no attempt consumed, and no eval row for a model that never ran.
Skips propagate down the chain.
skippedandblocked_byshow up truthfully in run state, theprogress bar, the briefings and the summary.
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, sopreparation 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_onisa 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
tests/test_dependencies.py, all observed failing before theimplementation existed.
ordering test — they detect the feature's absence rather than asserting on end
status.
RINGER_NO_SELF_UPDATE=1 python3 -m unittest discover -s tests—280 pass on Python 3.12.
git diff --checkclean.dependent
skippedwith 0 attempts, 0 tokens and no eval row, while aseparate passing producer released its reviewer normally.