Skip to content

The orchestrator that drafts its own backlog

Most checks in this repo are gates. If lint, test, lint-ascii, or one of the bug-fix gates fails, the ticket does not merge. That is the normal contract: red means stop and fix the branch.

The repo also has checks whose job is not to stop the branch. They record a separate problem as a ticket.

That distinction matters. A pull request can introduce or expose code-health drift that is real but not urgent enough to block the original change. It can also reveal that a monitoring producer has gone silent. In both cases the project wants a durable artifact, not a line in a log that nobody reads later. The orchestrator handles that by filing backlog tickets.

There are two current shapes:

  • Drift soft-checks file state: draft remediation tickets. These are suggestion-class refactors. They do not run until someone promotes them.
  • Watchdogs file state: confirmed repair tickets when advisory machinery is unhealthy. These are not product work, but they are scheduled work because a broken signal should not rot silently.

The useful idea is not "the system fixes itself." It does not. The useful idea is narrower: when the system observes a problem outside the current ticket's scope, it writes down the next unit of work in the same backlog format as every human-authored ticket.

Soft checks are advisory, not toothless

The review configuration in workflow/project.config.json splits review checks into buckets. review.always_checks is the hard wall. review.soft_checks is the advisory bucket.

The soft-check bucket currently includes several freshness and drift checks, including these three code-health tripwires:

  • lint-blast-radius-drift
  • lint-dedup-drift
  • lint-library-replacement-drift

The review stage runs those commands through the normal preflight runner, but a non-zero soft-check result is recorded as an advisory warning rather than a merge failure. The soft-check watchdog reads those records and files a confirmed repair ticket only after a configured streak of unhealthy runs.

The drift tripwires themselves have a more specific behavior. When they see a new candidate in touched code, they draft a remediation ticket and let the original ticket continue.

Example: blast-radius drift

scripts/lint_blast_radius_drift.py looks only at changed source files. For each changed file that is also in the blast-radius candidate roots, it asks scripts/blast_radius_candidates.py for candidates and filters that list back to the touched file.

When a candidate survives, the script builds a stable slug from the path and smell. It checks tickets/backlog/ and tickets/archive/ for that exact stable ticket name. If one already exists, it skips. If not, it emits a ticket like:

state: draft
source: lint-blast-radius-drift <date>
priority: P3
group: blast-radius-review
tags: [refactor]
bug_heuristic: false
authoring_engine: generator
authoring_mode: single

The generated body names the path, the measured signal, the smell, and the suggested decomposition. For wide refactors such as "God file," "God class," or "High fan-in hub," the ticket also widens ## Touches to include bounded importers when the script can enumerate them. If it cannot enumerate them safely, it falls back to a broader touch surface such as tests/** or the source package.

That ## Touches detail is not decoration. It tells the scheduler which future tickets must serialize with the refactor. The soft-check is not just leaving a note. It is writing a schedulable artifact with enough shape to keep later work from colliding.

Example: dedup and library replacement

The dedup and library-replacement checks follow the same draft-ticket pattern, but they are more conservative about what they file.

scripts/lint_dedup_drift.py scans touched Python and JS/JSX roots for clone, dead-code, and orphan-file candidates. It only auto-drafts when the candidate's codemap maturity is stable. Experimental, active, unknown, and unannotated code is skipped for ticket emission. That avoids turning ahead-of-time work into an automatic deletion request just because a static scanner cannot see its future caller yet.

The generated dedup ticket points the implementer back to bash scripts/dev.sh dedup-candidates and the code-health review protocol: write a golden or characterization test first, prove both copies behave the same, then collapse to one source of truth with no compatibility shim.

scripts/lint_library_replacement_drift.py uses the same maturity rule for backend Python that may be replaceable by a trusted library. The generated ticket names the hand-rolled smell, the suggested library, and the candidate path. Its notes tell the future agent to run bash scripts/dev.sh library-replacement-candidates, write a golden test on real server-shaped fixtures, and treat the change as a refactor unless the golden proves the old code wrong.

Those details are why the check drafts instead of fixing inline. Replacing a manual tally with collections.Counter, deleting an orphan symbol, or splitting a god file can be correct, but it is not a safe side quest inside an unrelated ticket. The safe unit of work is a separate ticket with its own tests.

The ticket emitter owns the boring parts

The Python emitters do not hand-roll backlog writes. The drift checks and the producer-health watchdog route ticket creation through workflow/scripts/lib_ticket_emit.py.

That helper owns the repeated mechanics:

  • validate ASCII-only content
  • require the core ticket shape
  • avoid re-emitting a stem that already exists in the backlog or archive
  • write under the configured backlog directory
  • run lint-tickets on the emitted file
  • commit the emitted ticket when the emitter is running in a branch context

That last point is important for review. A soft-check result is useful only if the generated ticket lands as part of the branch history. Otherwise the signal dies in the worker clone.

Tempo has equivalent review-stage behavior for its Go-side watchdog tickets: format the ticket, leave it under tickets/backlog/, then queue and push the watchdog ticket from the review stage. The implementation details differ, but the artifact contract is the same: the output is a real backlog ticket, not an ephemeral warning.

Watchdogs file repairs, not cleanup suggestions

The producer-health watchdog is the clearest example of the second shape. It is configured by workflow/producer_health_manifest.json. Each producer declares a kind, table or artifact, predicate, sample window, and any plausibility bounds. Examples include run-record token fields, history-index tables, sidecar artifacts, and JSON artifacts.

workflow/scripts/producer_health_watchdog.py evaluates those predicates against .state/history.duckdb or the configured artifact. If there are not enough samples, it skips. If a producer is unhealthy and no open matching ticket exists, it files:

state: confirmed
source: "Auto-filed by producer-health watchdog for <producer>"
priority: P1
group: producer-health
doc_mode: atomic
bug_heuristic: false

That ticket includes the producer id, predicate, sample count, observed count, observed rate, required rate, plausibility bounds, and dedup key. Its acceptance criteria explicitly say the fix must not make producer health a blocking always_checks or soft_checks gate.

That is the project rule in code form: metrics and sidecars never block a merge, but a broken producer must self-report as scheduled repair work.

The soft-check watchdog uses the same principle for advisory checks. Soft checks can fail without blocking a ticket. If the same check stays unhealthy for the configured threshold, Tempo writes a confirmed P0 soft-check-watchdog-* ticket. That ticket's goal is to restore the advisory check or remove it through the normal process-contract path.

What this buys

This setup gives the project a memory for problems that are real but not part of the current ticket.

The drift checks catch code-health candidates near the change that exposed them, but they do not turn every warning into immediate work. Draft tickets keep cleanup suggestions visible and deduplicated until they are promoted.

The watchdogs do the opposite for broken instrumentation. They avoid blocking product work, then file confirmed repair tickets when the signal itself is unhealthy. That keeps advisory checks and metrics from becoming theater.

Both paths use the same backlog shape as human-authored work: frontmatter, dependencies, touch surfaces, context, and acceptance criteria. That is the part that scales. The orchestrator does not need a separate place for "things the system noticed." It writes the same kind of ticket it already knows how to schedule, review, ship, and archive.

The boundary

This is still not autonomy in the strong sense.

Draft drift tickets are suggestions. They preserve evidence, but they are not a decision to spend engineering time. A person or a later policy still has to promote them.

Confirmed watchdog tickets are decisions to repair the control plane, but they are narrow decisions. They say "this signal is broken enough to fix," not "the system has improved itself."

The honest claim is smaller and more useful: the orchestrator can turn certain observations into backlog artifacts without waiting for a human to notice the log. That makes drift and broken advisory machinery harder to lose. It does not make the system wise about which product work matters.

The orchestrator drafts part of its own backlog. The backlog is still a set of claims to review, not a substitute for judgment.