Repository navigation
feat(slop): slop routing policy as a settings section - #398
Merged
Merged
Conversation
sudoTomas
force-pushed
the
feat/slop-routing-settings
branch
from
November 6, 2026 18:55
e4356ac to
705b142
Compare
What embry0 did with a code job or a finding judged slop was code. Every code job judged slop ran slop-cleanup, seeded again at each start so deleting it did not stick. The kinds a verdict could name, what the classifiers and the reviewer were told, the heading of a slop finding's ticket and the meter levels triage leaned at and dungeons appeared from were constants, and the lean level was never read. Add a slop_policy section: enabled, route_template, min_score_to_route, kinds, classifier_guide, review_guide, ticket_heading, triage_lean_min_level and dungeon_min_level. Each default is the old constant, so the default triage prompt is the old one to the byte. An organization may set every key, a repository only the three that route its own jobs: the guides go into system prompts, which a key with a repository allowlist must not write. A job reads the section when it starts, the watcher its stack's on each analysis and filing, the world each repository's dungeon level when it draws. Triage's paragraph is built from the policy: the heading it weighs, what a verdict does to the route, the route by name unless it is the built-in cleanup, and the lean, named with the meter's own level names. Its slots are filled in one pass, so an operator's text is never read for a marker. plan_route sends a code job judged slop to the policy's template when routing is on and the score reaches the threshold. A job pinned to the full pipeline keeps review and QA: it takes the slop route only when that route has both. A template that is gone, invalid or unfit is logged and the job takes its kind's default, and never fails. A write that changes the template a document's readers route to, by naming another or by taking the key or the section out so they fall back to what they inherit or the default, must leave one that records code jobs and has a coding step; a section's new effective_check sees the default fallback, which a field validator never does. Dry runs and undo are checked the same way. Only a write that may change slop_policy reads the templates, FOR SHARE in its transaction. Deleting, renaming or unfitting a template a document routes to answers 409 naming the documents; the change locks its row FOR UPDATE before it reads them, so neither side races the other. The built-in templates are still seeded at every start: routing no longer depends on whether the template exists. No variable fed these keys, so nothing is imported. A cleanup's experience stays the progression pack's outcomes.cleaned (#176). METER_DUNGEON_LEVEL moves into the policy beside the lean level: both are where embry0 acts on a meter reading, not how a score is computed. Fixes #169
sudoTomas
force-pushed
the
feat/slop-routing-settings
branch
from
November 6, 2026 19:25
705b142 to
ba34013
Compare
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.
Summary
What embry0 does with a code job or a finding judged slop moves into a
slop_policysection of the settings document: whether and where a code job judged slop is routed, and from which score; the kinds a verdict may name; what the classifiers and the reviewer of a cleanup are told; the heading a slop finding's ticket opens with; and the two Slop-O-Meter levels embry0 acts on (triage's lean and the dungeon). Every default is the old constant, so the default triage prompt is the old one to the byte. This is the second and last pull request for #169; the first (#384) moved the meter's scoring.Motivation — Intent + Problem
Slop routing kept all of its policy in code (#169, part of the settings-in-the-database rule, #159):
slop-cleanup, and the built-in template is seeded again at every start, so deleting it, the only way to stop the routing, did not stick.How it works
The section (
embry0/settings/sections/slop_policy.py, reference indocs/configuration.md#slop_policy, prose in the newdocs/slop-routing.md, linked fromdocs/slop-meter.md):enabledtrueroute_templateslop-cleanupmin_score_to_route0(every slop verdict)kindsclassifier_guideCLASSIFIER_GUIDEreview_guideSLOP_REVIEW_GUIDEticket_headingSlop, not a bugtriage_lean_min_level4(Sloppy Joe)dungeon_min_level2(Spaghetti Bolognese)PUT/PATCH /config/settings,embry0 settings applyandset, MCPsettings_set*, the Policy tab and an undo): at least one kind, at most 20, eacha_wordonce; guides plain text (lines and tabs) of at most 4,000 and 2,000 characters, not blank; a one-line heading of at most 80 characters without#marks; levels 1 to 5 or null; a score 0 to 100; a template name's shape. History and undo are the document's; the Policy tab lists sections from the API, so the section appears there with no frontend change.docs/slop-routing.md#layers): a repository's layer that setsenabled: trueroutes that repository's jobs judged slop under an organization'senabled: false, and a repository's layer that setsroute_templateholds that template against a delete or a rename until that layer's key is changed or taken out.Who reads it, for which repository.
job_settings.JOB_SECTIONS, asdone_whenandsalvage): triage's prompt, plan_route's route and the reviewer's guide come from it. A job that started before this release reads the defaults, which are what it started under.app.pybuilds per stack (_watcher_slop_policy, pinned intest_watcher_wiring.py).dungeon_min_levelwhen it draws the dungeons.parse_work_nature(kinds=None)), so one organization's own kind is never dropped by a reader of another.Triage's prompt. The paragraph after the guide is built from the policy: the heading it weighs, what a verdict does to the route (nothing with
enabled: false; from a score withmin_score_to_route), the route (the Slopmaster's cleanup for the built-inslop-cleanup, whose graph is seeded at every start; the cleanup pipeline by name for any other, whoever codes it), and the lean, fromtriage_lean_min_levelby the meter's own level names (slop_meter.level_namesof the job's repository, read when triage writes its prompt as its reading is). Wrapped at 78 columns, the defaults give the old paragraph byte for byte. The prompt's slots are now filled in one pass, so text an operator wrote (a guide, a heading, a level name) is never read for another slot's marker.Routing (
plan_route). A code job judged slop runsroute_templatewhen the policy is on and itsslop_scorereachesmin_score_to_route; otherwise it takes its normal route (triage's pick, else its kind's default), logged asplan_route_slop_not_routed. A template a person named still wins. A job pinned to the full pipeline (a watcher-origin case, #306) keeps review and QA: it takes the slop route only when that route has a review step and a QA step, asslop-cleanupdoes, else its kind's default; a job that is not pinned may take a lighter route such asquick-fix. The slop route is now its own tier: a template that is gone, invalid, does not take a code job, or lacks the steps a pinned job keeps, is logged (plan_route_template_not_foundwithslop_route,plan_route_template_invalid,plan_route_slop_template_unfit) and falls to the kind default. Unlike a person's invalid template it never fails the job (before, an invalidslop-cleanupwith no code default raisedTemplateInvalidError).The route template and deleting or renaming it.
row_behaviour.code_job_problem, from refactor(agents): behaviour from agent and template rows #385's row columns): its jobs are recorded as code jobs (recorded_kind: not the default of another kind, no otherjob_kind, not a route of specialists only), and its route has a coding step (developerorslopmaster). A write that changes the template a document's readers route to must leave one. That covers a write naming another template and one that takes the key, or the whole section, out of the last document that set it, so its readers fall back to what they inherit or the default: the field's validator checks a value a document of the chain sets, and a newSection.effective_check(run bydocument.checkwith the readers' models before and after the write,document.effective_models) checks the fallback to the default, which no field validator sees. The same check runs forPUT,PATCH, their dry runs, an undo andsettings apply. Only a change is checked, so a write of another key never trips over a template its readers already route to.its defaultwhile it sets none, which namesslop-cleanup) and each layer that sets the key. The operator points those documents elsewhere first, which is recorded and undoable.slop_policyreads the templatesFOR SHAREin its own transaction (a newwrite_readsstep of the store,write_context.deployment_reads, given the sections the write may change; a write of any other section, its dry run included, reads none and locks nothing), and a template's change locks its rowFOR UPDATEbefore it reads the documents (services/slop_routing.py, a guardPipelineTemplatesRepository.updateanddeleterun in their transaction). Whichever comes second waits and sees what the first committed; two tests hold each order open and prove it.Smaller pieces.
docs/examples/settings.yamlwrites a blank line inside a text block as the bare#(the review guide has one).SettingsDocumentsRepository.alltakes the caller's connection.Decisions
quest_template(refactor(agents): behaviour from agent and template rows #385): that one is a column in the same transaction, while a settings document is the operator's text, possibly kept in a file that the nextembry0 settings applywrites back, so an automatic rewrite would be undone or refused by it. A fallback alone would let a deleted template silently stop the cleanups, which is the failure the issue names. The fallback stays behind the refusal for a template edited in the database by hand.enableddoes not release a template. A document that names a template holds it even with routing off: turning the policy on again writes noroute_template, so it would never be checked again.slop-cleanupstays as it is. refactor(agents): behaviour from agent and template rows #385 made what a built-in row says about its jobs the operator's (written on a first insert only), while its graph and description stay embry0's and are written at every start. Keeping deleted built-ins deleted would need a record of deletions for every built-in, which changes them all, beyond this issue. Routing no longer depends on whether the template exists: an operator turns it off withenabledor routes elsewhere withroute_template, and aslop-cleanupseeded again is then unused.slop_policy, asdungeon_min_level. It is where embry0 acts on a meter reading, besidetriage_lean_min_level, not how a score is computed. Inslop_meterit would have had to stay out ofconfig_versionor rescore every repository when it moved.xpkey. The issue predates feat(progression): read game content from a shipped pack #207: a merged cleanup's experience (20,000, plus 300 per net line removed up to 1,000) is already the progression pack'soutcomes.cleaned(feat(progression): game content from a pack, original by default #176), an operator's document with versions and restore. A second home would be one more place for one value.Moved environment variables
None: no variable ever fed slop routing; every key was a constant in the code. Nothing is imported at the first start, nothing is retired, and
.env.exampleis unchanged. There is no.envkey to remove after the deploy.Pattern provenance
N/A — originated in this PR. The section, layers and job snapshot follow embry0's own
done_when,salvageandslop_metersections (#182, #384); the template checks read #385's row columns.Verification
Automated
-n 4 --fail-on-warnings,DOCKER_HOST=unix:///nonexistent.sock:19009 passed, 21 skipped, 0 failed, on705b142e(ba34013badds two test cases only; the touched files and the slop tests: 96 passed) (the 21 skips: the Docker-backed infra tests and the usual six). CI onba34013b:19025 passed, 6 skipped, every job green.ruff format --check .andruff check .(whole tree) clean;mypy embry0: no issues in 658 source filesDefaults reproduce origin/main, pinned from
c80ba42e, not from the new codetest_the_defaults_are_what_the_code_held_before_the_section: the kinds, the template, the two guides (copied as text), the heading, the dungeon level 2 and the lean level 4.test_the_default_prompt_is_the_one_triage_was_given_before_the_policy: the sha256 of the whole triage system prompt asc80ba42ebuilds it, with tiers and with one default model, and the old paragraph as text.test_the_default_ticket_and_review_are_what_they_were: a slop ticket's opening and the reviewer's section, asc80ba42ewrote them.test_a_job_that_read_no_policy_routes_as_before(passes onmainby design) andtest_the_policy_s_defaults_route_as_the_code_did.The acceptance criterion, on a real database (
tests/unit/api/test_slop_policy_settings_live.py, real lifespan, store and seeded templates):PATCH /config/settings/slop_policy?owner=acme {"enabled": false}; a job ofacme/widgetsjudged slop, with the settings it reads when it starts, getsfull-magnum-opusand the developer, whileglobex/api's still getsslop-cleanupand the Slopmaster. Undoing the revision routes acme's again. The same file proves the 422s through the API, the 409s on delete, rename and unfit, and both lock orders.Fail-first. The seven new test files, copied into a clean
origin/maintree: 51 failed, 3 passed, each failure with its own message:unknown_section), "the 422 names each refused key", "the Policy tab lists the sections the API answers", "the template the default policy routes to was deleted", "nothing guards a template a settings document routes slop jobs to" (both lock orders), "triage is told no level names but the defaults";The three that pass there pin behaviour that must not change:
test_a_job_that_read_no_policy_routes_as_before,test_a_watcher_without_a_reader_reads_as_before, andtest_a_reader_that_fails_reads_the_defaults(mainnever reads a reader; the mutation below shows it fails when a failing reader is not caught).Mutations (first round), each against the tree then, each caught by its test with its message:
enabledDELETEruns no guardFOR SHAREFOR UPDATEtest_only_a_template_the_write_changes_is_checked(422 on the unrelated write)classifier_guidereplaceafter anothertest_an_operator_s_text_is_never_read_for_a_slotThe first run of these found one gap: triage given the default level names passed every test, since no test gave the node other names.
test_triage_is_built_from_the_policy_its_job_readnow gives it a repository's own names, andtest_triage_leans_by_the_level_names_its_repository_readsreads them from a real layer.Review round (two defects, five test gaps, three wording and scope fixes). Each new test run against the previous head
e4356ac5:e4356ac5test_a_job_pinned_to_the_full_pipeline_keeps_review_and_qa_when_judged_sloproute_templateout checks the default it falls back totest_taking_route_template_out_checks_the_template_its_readers_then_route_to(key out, sectionnull, section left out)test_a_layer_that_takes_route_template_out_is_checked_for_what_it_inheritstest_taking_route_template_out_never_leaves_a_template_that_is_goneslop_policylocks the templatestest_only_a_write_that_may_change_slop_policy_waits_on_a_templatetest_triage_is_told_the_route_its_policy_namestest_a_write_that_leaves_the_route_as_it_was_is_taken_while_its_template_is_missingThe reviewer's two tests, on this head: both pass (on
e4356ac5both fail, with "a pinned watcher job judged slop ran quick-fix: ['developer', 'reviewer']" and "the deployment's document now routes to its default slop-cleanup, which was deleted"). The default triage prompt's digest test passes unchanged.The five test gaps, and two more from the re-review (N03, N08), each a test that passes on
e4356ac5and its mutation, now caught:SELECTdropsjob_kindtest_a_template_whose_row_records_another_kind_is_refusedtest_a_stored_verdict_is_read_back_with_every_kind_it_kepttest_a_dry_run_checks_the_template_as_a_write_doestest_a_repository_layer_holds_the_template_it_routes_totest_an_undo_that_would_route_to_a_template_that_is_gone_is_refusedtest_a_job_pinned_to_the_full_pipeline_keeps_review_and_qa_when_judged_slop(a route with developer and QA, no review)test_naming_the_default_it_already_routes_to_is_taken_while_that_template_is_missingAnd the fixes' own mutations, each caught: the pinned check removed ("... ran quick-fix: ['developer', 'reviewer']");
effective_checknot wired ("... left the deployment routing to the default slop-cleanup, which is gone"); the hook judging an unchanged default ("{'slop_policy': {'min_score_to_route': 40}} was refused for a template it does not change"); every write reading the templatesFOR SHARE("a write of another section, and its dry run, take no lock on the templates"); every route called the Slopmaster's ("triage is told every slop route is the Slopmaster's cleanup").Deploy notes
embry0/sandbox/,safety/,agents/orexecution/changed.DELETEor a rename ofslop-cleanup(or of a template a document routes to) now answers 409 instead of deleting it; an invalid slop route with no code default now takes the legacy plan instead of failing the job; and a settings write that would leave its readers on a template that is gone answers 422.embry0/services/slop.pyandembry0/workflows/row_behaviour.pyare in the progression rollup'sFOLD_MODULES, so the rollup is rebuilt once at its next read (one read of the history).Out of scope / follow-ups
Design + Plan
docs/superpowers/specs/2026-10-29-settings-layers-design.md), the slop dungeon spec (docs/superpowers/specs/2026-10-25-slop-dungeon-design.md)Anti-goals respected
Fixes #169