Repository navigation
feat(slop): the Slop-O-Meter's scoring as a settings section - #384
Merged
Merged
Conversation
sudoTomas
force-pushed
the
feat/slop-meter-settings
branch
from
November 6, 2026 14:30
f58357e to
1a15a37
Compare
The meter kept all of its policy in code: the file weights and which directories are vendored or tests, the pace and cap constants, the small-repository rule, the level bounds and names and the judge's prompt. An organization could not say what counts as code, how fast its team works or where its levels fall, and changing a constant silently rescored every repository. Its reading pace was three variables, and an owner's own bots sat on its organization row. Add a `slop_meter` section to the settings document. Every constant is a key whose default is the value the code held, so nothing written scores bit for bit as before: a fixture history reproduces the pinned pre-change numbers, and 86,000 paths weigh what the old rules gave them. An organization's layer may set every key but `enabled` and `interval_hours`; a repository's what counts as code in its layout. `weights` maps a glob to a weight, merged glob by glob through every layer, the longest glob winning. Each score row records `config_version` (migration 144), a digest of the keys its repository's score is computed from. The loop now looks at the section every five minutes: a change that gives a repository it read another version rescores at once, `enabled` and `interval_hours` apply within the look, and turning the meter on reads at the next one. The forge budget is read per organization; organizations sharing one credential get the fewest calls and the largest reserve. SLOP_METER_INTERVAL_SECONDS (0 is `enabled: false`), SLOP_METER_API_CALLS_PER_TICK and SLOP_METER_RATE_RESERVE are imported once and then ignored. The organization setting `slop_meter_bots` moves into its layer as `bots`, copied at the first start and still taken by PATCH /orgs until 0.2.0. The process limits stay variables. The unread LEAN_LEVEL constant is deleted: triage's lean belongs to the routing section a later pull request adds. Multi-line texts in the settings YAML are now written as literal blocks, so the judge's prompt reads as written. Refs #169
The meter kept all of its policy in code: the file weights and which directories are vendored or tests, the pace and cap constants, the small-repository rule, the level bounds and names and the judge's prompt. An organization could not say what counts as code, how fast its team works or where its levels fall, and changing a constant silently rescored every repository. Its reading pace was three variables, and an owner's own bots sat on its organization row. Add a `slop_meter` section to the settings document. Every constant is a key whose default is the value the code held, so nothing written scores bit for bit as before: a fixture history reproduces the pinned pre-change numbers, and 86,000 paths weigh what the old rules gave them. An organization's layer may set every key but `enabled` and `interval_hours`; a repository's what counts as code in its layout. `weights` maps a glob to a weight, merged glob by glob through every layer. Weigh paths by specificity: a folder glob beats any extension glob, then more literal segments, then more literal characters. Globs are indexed by extension and first directory and each path is weighed once a run, in a thread. Each score row records `config_version` (migration 144), a digest of the keys its repository's score is computed from. The loop looks at the section every five minutes: a settings change rescores only the repositories it touches, from the rows and mirrors already read, with no fetch, forge call or judgment, and the full run keeps its interval. `enabled` and `interval_hours` apply within the look. A credential's forge budget is the deployment's; an organization's values only narrow its own share. `bots` and `ai_agents` add to what each layer inherits (a new `added_keys` for list fields), and an AI agent is an address or the part of one up to its `@`. SLOP_METER_INTERVAL_SECONDS (0 is `enabled: false`), SLOP_METER_API_CALLS_PER_TICK and SLOP_METER_RATE_RESERVE are imported once and read as before until the import succeeds. The organization setting `slop_meter_bots` moves into its layer as `bots`, cleaned (each bot once, unholdable entries left out) at the first start and on PATCH or POST /orgs, which still take it until 0.2.0. The process limits stay variables. The unread LEAN_LEVEL constant is deleted: triage's lean belongs to the routing section a later pull request adds. Multi-line texts in the settings YAML are written as literal blocks; progression packs keep their own writer. Refs #169
The meter kept all of its policy in code: the file weights and which directories are vendored or tests, the pace and cap constants, the small-repository rule, the level bounds and names and the judge's prompt. An organization could not say what counts as code, how fast its team works or where its levels fall, and changing a constant silently rescored every repository. Its reading pace was three variables, and an owner's own bots sat on its organization row. Add a `slop_meter` section to the settings document. Every constant is a key whose default is the value the code held, so nothing written scores bit for bit as before: a fixture history reproduces the pinned pre-change numbers, and 86,000 paths weigh what the old rules gave them. An organization's layer may set every key but `enabled` and `interval_hours`; a repository's what counts as code in its layout. `weights` maps a glob to a weight, merged glob by glob through every layer. Weigh paths by specificity: a folder glob beats any extension glob, then more literal segments, fewer `**` and wildcard segments, more segments, more literal characters. Globs are indexed by name, extension, literal directory or segment and three-character runs; at most 16 per list may be checked against every path. Each path is weighed once a run, and the history, outliers and scores are built in a thread. Each score row records `config_version` (migration 144), a digest of the keys its repository's score is computed from. The loop looks at the section every five minutes: a settings change rescores only the repositories it touches, from the rows and mirrors already read, with no fetch, forge call or judgment, and the full run keeps its interval. `enabled` and `interval_hours` apply within the look. A credential's forge budget is the deployment's; an organization's values only narrow its own share, and a repository stopped by that share is scored with what it read instead of keeping the meter on catch-up. `bots` and `ai_agents` add to what each layer inherits (a new `added_keys` for list fields, each item once without regard to case), and an AI agent is an address or the part of one up to its `@`. A write counts merged and added keys with what they inherit; a read past the cap keeps the layer's own entries and the inherited ones that fit. SLOP_METER_INTERVAL_SECONDS (0 is `enabled: false`), SLOP_METER_API_CALLS_PER_TICK and SLOP_METER_RATE_RESERVE are imported once and read as before until the import succeeds. The organization setting `slop_meter_bots` moves into its layer as `bots`, cleaned (each bot once, unholdable entries left out) at the first start and on PATCH or POST /orgs, which still take it until 0.2.0. The process limits stay variables. The unread LEAN_LEVEL constant is deleted: triage's lean belongs to the routing section a later pull request adds. Multi-line texts in the settings YAML are written as literal blocks; progression packs keep their own writer. Refs #169
The meter kept all of its policy in code: the file weights and which directories are vendored or tests, the pace and cap constants, the small-repository rule, the level bounds and names and the judge's prompt. An organization could not say what counts as code, how fast its team works or where its levels fall, and changing a constant silently rescored every repository. Its reading pace was three variables, and an owner's own bots sat on its organization row. Add a `slop_meter` section to the settings document. Every constant is a key whose default is the value the code held, so nothing written scores bit for bit as before: a fixture history reproduces the pinned pre-change numbers, and 86,000 paths weigh what the old rules gave them. An organization's layer may set every key but `enabled` and `interval_hours`; a repository's what counts as code in its layout. `weights` maps a glob to a weight, merged glob by glob through every layer. Weigh paths by specificity: a folder glob beats any extension glob, then more literal segments, fewer `**` and wildcard segments, more segments, more literal characters. Globs are indexed by name, extension, literal directory or segment and three-character runs; at most 16 per list may be checked against every path. Each path is weighed once a run, and the history, outliers and scores are built in a thread. Each score row records `config_version` (migration 144), a digest of the keys its repository's score is computed from. The loop looks at the section every five minutes: a settings change rescores only the repositories it touches, from the rows and mirrors already read, with no fetch, forge call or judgment, and the full run keeps its interval. `enabled` and `interval_hours` apply within the look. A credential's forge budget is the deployment's; an organization's values only narrow its own share. A repository that share stops before its events are all read keeps its previous score, since a run starts its reading over, and waits for the next run without keeping the meter on catch-up. `bots` and `ai_agents` add to what each layer inherits (a new `added_keys` for list fields, each item once without regard to case), and an AI agent is an address or the part of one up to its `@`. A write counts merged and added keys with what they inherit and is refused only when it makes an over-bound total larger; a read past the bound keeps the most specific layers' entries first. SLOP_METER_INTERVAL_SECONDS (0 is `enabled: false`), SLOP_METER_API_CALLS_PER_TICK and SLOP_METER_RATE_RESERVE are imported once and read as before until the import succeeds. The organization setting `slop_meter_bots` moves into its layer as `bots`, cleaned (each bot once, unholdable entries left out) at the first start and on PATCH or POST /orgs, which still take it until 0.2.0. The process limits stay variables. The unread LEAN_LEVEL constant is deleted: triage's lean belongs to the routing section a later pull request adds. Multi-line texts in the settings YAML are written as literal blocks; progression packs keep their own writer. Refs #169
sudoTomas
force-pushed
the
feat/slop-meter-settings
branch
from
November 6, 2026 16:42
0cfcb0a to
f63f580
Compare
8 tasks done
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
The Slop-O-Meter's scoring policy moves into a
slop_metersection of the settings document: file weights, vendored and test directories, the pace and cap constants, the small-repository rule, the outlier thresholds, the judge's guidance, the five levels' bounds and names, the owner's bots and AI agents, and how often and how hard it reads. Nothing written scores bit for bit as before. Each score row records the version of the settings it was computed with, and a settings change rescores only the repositories it touches, within five minutes. This is the first of two pull requests for #169 (the meter); slop routing (slop_policy) comes in the second, which finishes the issue.Motivation — Intent + Problem
The meter kept all of its policy in code (#169, part of the settings-in-the-database rule, #159):
How it works
The section (
embry0/settings/sections/slop_meter.py, reference indocs/configuration.md#slop_meter, prose indocs/slop-meter.md):enabled,interval_hours(deployment only);api_calls_per_tick,rate_reserveweights(glob → weight, merged glob by glob),excluded_dirs,test_dirs,test_files,test_marks,test_weightbootstrap_lines,complexity_weightcommit_hours,subcommit_hours,coauthor_shares,ai_coauthor_share,event_hours,lines_per_hour,weekly_hours_capcore_min_commits,core_full_commits,small_repository_in_full(embry0's departure from the published algorithm, now data)outlier_min_lines,outliers_per_year,judge_promptlevel_bounds(four),level_names(five)bots,ai_agents(each layer adds to the list it inherits)Defaults are the old constants. The section is the only home of the defaults;
weights.py,score.pyandjudge.pyread them from it. The judge's answer form stays in code and is said after the guidance, so the default system prompt is the old one character for character.Layers. An organization may set every key but
enabledandinterval_hours; a repository may set what counts as code in its layout (weights,excluded_dirs,test_dirs,test_files,test_marks).weightsmerges glob by glob.botsandai_agentsadd to what each layer inherits, through a newSection.added_keysfor list fields (union, each item once without regard to case, no layer takes one out).Counts across layers. A write counts a merged or added key with what it inherits (the defaults, the deployment's, the organization's): one over the key's bound is a 422 naming the key and the count.
weightsover its 79 defaults counts too.settings_layer_entries_left_out): its own, then its organization's, then the deployment's, then the default's, each in its order. Two repositories of one organization therefore agree on the organization's globs, and the read never falls back to the inherited list alone.excluded_dirsand thetest_*lists replace what they inherit, so a repository can take back a directory the default excludes.Globs. A glob without
/matches a file's name in any directory, one with/its path from the root;*,?and**as usual; matching ignores case, so globs are written in lowercase (a leaf module,embry0/services/slop_meter/globs.py, checks, compiles and indexes them).Weigh paths by specificity. Where several globs match, the most specific wins (
globs.specificity):/over any without one, sogen/**: 0weighs.py,.protoand.graphqlundergen/alike;/: more segments free of wildcards, then fewer**segments, then fewer other segments with a wildcard, then more segments, then more literal characters, sosrc/main.pybeatssrc/**/main.pyanda/b/*.tsbeatsa/**/b/*.ts;*.min.jsover*.js);The generated files (
*.min.js,*.d.ts, …) areweightsentries at 0, which is how the defaults stay exact.Weighing cost.
GlobIndexfiles each glob once (globs.bucket) and checks a path only against the globs that could match it. A path is weighed once a run (FileRuleskeeps the weights), and the history, the outliers to judge and the scores are each built inasyncio.to_thread.*.py,test_?*.py)*fixture*)src/**)**/vendored/**,*/gen/*.py)*/*.py)**/*_gen*)Anything else is broad (
*/*,*x*) and checked against every path. A write may hold at most 16 broad globs inweightsand 16 intest_files, counted with what it inherits. That is checked on writes only, so a document below that grows later costs time, never the key. A write that leaves no more broad globs than readers read before is taken.On 100,000 paths with 900 extra globs: about 117,000 checks and 0.3 s for literal-head, wildcard-head-with-segment and wildcard-head-with-extension globs, and 0.6 s for name globs with inner text. Before this round these took 90 million checks and 7 to 11 s.
Validation. 422 names every refused key (a glob the matcher cannot read, a weight outside 0 to 10, a duplicate directory, bounds not rising, five names not distinct, a ramp that does not rise, …), through
PUT/PATCH /config/settings,embry0 settings apply, MCPsettings_set*and the Policy tab. History and undo are the document's. The Policy tab lists sections from the API, so the section appears there with no frontend change.The loop (
embry0/services/slop_meter/loop.py):config_versionis a 16-hex digest of every key butenabled,interval_hours,api_calls_per_tick,rate_reserveandjudge_prompt(keys sorted, values as dumped to JSON). It is stored on each score row (migration 144) and shown inGET /repos/{repo}/-/slop-meterasreading.config_version.SlopMeterRepository.versions()) with the version it reads now. Only the ones that differ are rescored (rescore_changed): from their rows and mirrors as already read, with no fetch, no forge call and no judgment. The full run keeps its time.enabled: falsea run only removes what left the scope, once an interval (before,SLOP_METER_INTERVAL_SECONDS=0pruned once at start and stopped).api_calls_per_tickandrate_reserve, oneApiBudgetper credential (fix(slop): keep the Slop-O-Meter's forge API budget per credential #185). Each organization reads through aShareBudgetof it: at mostmin(its calls, the deployment's), stopping atmax(its reserve, the deployment's). An organization's values narrow only its own share and never lower another organization's on the same credential. Only the credential's own budget running out keeps the meter on its catch-up cadence, as before.main: many repositories on one credential) is related. Keeping the reader's place between runs, as fix(slop): the meter never scores repositories when many share one credential #396 needs, would also let a small share finish over several runs.botsare matched whole (as fix(slop): count embry0's own commits as a bot in the Slop-O-Meter #184's).ai_agentsare an address up to and including its@(aider@, any domain) or a whole address; any other prefix is refused, sodevcannot make everydevon@a bot. Both are applied when scoring, so history read before they were listed is corrected without reading it again.SLOP_METER_INTERVAL_SECONDS,_API_CALLS_PER_TICKand_RATE_RESERVEas the release before did (meter_reader(store, config), as the budget's readers do, feat(budget): unattended-job caps and continue policy as settings #171). A meter that was off stays off. The budget policy retries the import while the orchestrator runs (the import is one transaction for every section).slop_meter_botsmoves into the organization's layer asslop_meter.bots(aMovedSetting, as #170 and #171 moved theirs):PATCH/POST /orgs, which still take it until 0.2.0 with adeprecatednote, through one cleaning (MovedSetting.write).slop_meter_bots_entries_dropped) instead of refusing the list. So a valuemainaccepted, such as["release-bot", "Release-Bot"], is never refused.POST /orgschecks each moved value against its section before registering anything, so it never registers an owner and then answers 422.BudgetView.moved_valuesuntil the copy succeeds.Smaller pieces.
dump_yamlwrites a text of several lines as a literal block (|-), so the example settings file and the env import write the judge's prompt as it reads. No other section has a multi-line value, and the round trip is tested for awkward texts. Progression packs keep their own writer (progression_pack_store.dump_yaml), so their stored texts and versions are unaffected; a test pins it.LEAN_LEVEL(slop_meter/context.py) was never read, so it is deleted rather than wired here. Triage's lean is routing policy: the routing pull request addsslop_policy.triage_lean_min_level.Choices I made that a reviewer may want to revisit:
METER_DUNGEON_LEVEL = 2) and the routing PR'striage_lean_min_levelall read level numbers. Allowing another count later is an additive schema change.pace:andcaps:objects. A layer replaces a key whole (spec 2026-10-29, L2), so an organization can changelines_per_hourwithout restating six other numbers.weightsis the one mapping, merged entry by entry.test_marksbesidetest_files. The old rule fora.test.tsallowed exactly one extension after the mark, which no glob can say:*.test.*would also counta.test.helper.ts. A list of words kept the defaults exact.judge_promptis not inconfig_version. Judgments are kept per commit and never asked again, so new guidance rescores nothing until another commit is judged.interval_hoursis at least a quarter hour. An importedSLOP_METER_INTERVAL_SECONDSbelow 900 becomes 0.25.botsandai_agentsadd up across layers;excluded_dirsand thetest_*lists replace. A deployment-wide CI bot should always count, and the first-start copy writes every organization's old list into its layer, so replace semantics would have cut the deployment's list off from those organizations. A directory list has to be able to take an entry back (a repository whose own code lives indeps/), so those replace, as their descriptions say.config_version's docstring no longer claims stability across releases: a release that adds a scored key changes every digest, and its first run records the new ones.Fate of each
SLOP_METER_*variableSLOP_METER_INTERVAL_SECONDSslop_meter.interval_hours(seconds / 3600, at least 0.25), and0ofslop_meter.enabled: false. Read once at this release's first start, then ignored; remove after the deploy is verifiedSLOP_METER_API_CALLS_PER_TICKslop_meter.api_calls_per_tick. Read once, then ignored; remove after deploySLOP_METER_RATE_RESERVEslop_meter.rate_reserve. Read once, then ignored; remove after deploySLOP_METER_REPOS,SLOP_METER_EXCLUDEbackground.slop_meter(#165)SLOP_METER_CATCHUP_SECONDSSLOP_METER_DATA_PATHSLOP_METER_GIT_TIMEOUT_SECONDSSLOP_METER_COMMITS_PER_TICK,SLOP_METER_EVENT_PAGES,SLOP_METER_REVIEWS_PER_TICK,SLOP_METER_LOGINS_PER_TICK,SLOP_METER_JUDGMENTS_PER_TICKapi_calls_per_tick,rate_reserveand thebudgetsectionThe three imported variables are gone from
.env.example.organizations.settings.slop_meter_botsis copied into each organization's layer and then read by nothing;PATCH /orgstakes it until 0.2.0.Pattern provenance
The scoring is still the clean-room reimplementation of Slop-O-Meter's published slop score algorithm (
docs/slop-score-algorithm.md), credited indocs/slop-meter.md; no code was read or copied. The section, layering, env import and moved-setting mechanics follow embry0's owncontext_audit(#170) andbudget(#171) sections. Everything else originated in this PR.Verification
Automated
-n 4 --fail-on-warnings,DOCKER_HOST=unix:///nonexistent.sock, on the branch rebased ontomainafter refactor(agents): behaviour from agent and template rows #385 (f63f5802):18941 passed, 21 skipped, 0 failed. CI on it:18956 passed, 6 skipped, every job green. Before refactor(agents): behaviour from agent and template rows #385 landed, the only failures were the 35 checks that the migration list has no gap, from the missing 143.ruff format --check .andruff check .(whole tree) clean;mypy embry0: no issues in 654 filescd frontend && npm run lint(0 errors, the 5 existing warnings),npx tsc -bclean,npm run test: 326 files, 2666 tests passed. The only frontend change is thereading.config_versionfield on the API type--checkboth clean); name denylist clean; gitleaks on the changed files cleanNothing written scores exactly as before
test_the_fixture_scores_exactly_as_the_code_before_the_section_did: a 240-commit fixture (core and drive-by authors, a bot, squashes, human and AI co-authors, judged outliers, ignore lists, two areas, forge events) gives the pinned score, level, name and summed cost, capacity and slop per scope. The numbers were computed on65cf0eafwith the same builder and match to the last bit.test_the_default_document_scores_the_fixture_identically_to_the_old_constants: the same with an empty stored document, and with a document that writes every key at its default, through the snapshot read a tick makes.test_every_path_weighs_what_the_rules_before_the_section_weighed_it: 86,363 paths, tricky names included (a.test.helper.ts,test_.py,x.d.ts,Button.stories.module.css, case variants, vendored and test directories), weigh exactly what the pre-change rules (copied verbatim into the test) gave them.tests/unit/services/slop_meter/test_score.py) pass unchanged.The acceptance criterion, on a real database (
tests/unit/api/test_slop_meter_settings_live.py, real lifespan, store and settings documents): the meter scoresacme/widgetsand the row records the version the repository reads.PATCH /config/settings/slop_meter?owner=acme {"weights": {"*.sql": 0.1}}makes the next look rescore at once; the row'sconfig_versionchanges to the new effective version, the score changes, and the API shows it. Undoing the revision rescores it back to the first score and version.Fail-first. The five new test files, copied into a clean
origin/maintree: 37 failed, 3 errors, 2 passed. Every failure carries its own message:The two that pass there are the golden fixture and the path corpus, which pin behaviour that must not change. They fail when a default moves:
"*.sql": 0.81fails both, with "276 of 86363 paths weigh otherwise" and "a repository read with no settings scores as it did before #169".Third review round (each new or changed test, run against the tree before it,
77e0cd23; 6 failed there, each with its own message):test_a_share_too_small_for_a_whole_read_scores_nothing(1, 2 and 3 calls)test_a_repository_whose_share_cuts_its_events_short_keeps_its_previous_scoretest_a_share_large_enough_for_a_whole_read_scorestest_an_organization_that_reads_less_never_holds_the_meter_on_catch_up(0, 3, reserve 100,000)test_a_read_past_the_bound_keeps_the_most_specific_layers_entries_firsttest_a_layer_over_the_bound_because_a_document_below_grew_can_still_writeThe reviewer's probes on the pushed tree:
narrow_stages: shares of 1 to 3 are unscored, 4 is scored.narrow_org: the organization at 10 calls is unscored, with no backlog.narrow_score: no longer prints, since the narrowed repository has no score to read.trim_layers: both repositories keep acme's 200 globs and weighorg199/x.py0.0.broad_cap: the unrelated write is accepted.Second review round (each new or changed test, run against the tree before it,
1a15a379; 13 failed there, each with its own message):test_an_organization_that_reads_less_never_holds_the_meter_on_catch_up(api_calls_per_tick0 and 3,rate_reserve100,000)test_the_credential_s_own_budget_still_holds_the_meter_on_catch_uptest_a_list_added_to_is_counted_with_what_it_inherits_when_written,test_globs_are_counted_with_the_defaults_and_what_they_inherit_when_writtentest_a_layer_keeps_its_own_entries_when_the_document_below_grows_past_the_captest_an_exact_path_wins_over_one_that_reaches_it_through_wildcardstest_a_path_is_checked_against_the_few_globs_that_could_match_it(five glob sets)test_globs_checked_against_every_path_are_bounded_when_writtentest_the_judge_hears_the_repository_s_guidance_and_the_weighing_runs_off_the_event_loopThe reviewer's probes, rerun on the pushed tree:
probe_loop2andnarrow_share: one full run in six hours, globex fetched once (acme was scored then; since the third round a share that cuts its read short holds its score).cap_probe: the 210-bot write refused, naming 210; a grown deployment leaves acme its 60 bots.spec_probe: 0 mismatches in 663,000, and the three cases resolved.perf_wild: 0.3 to 0.6 s.three_orgs,scope_probe,version_cost: as before.many_repos(fix(slop): the meter never scores repositories when many share one credential #396): the same asmain.First review round (each new or changed test, run against the tree before it,
398abab4; 19 failed there, each with its own message):test_a_changed_weight_rescores_only_the_repository_it_touches_with_no_fetch_and_no_forge_calltest_a_repository_whose_rescore_fails_is_not_tried_again_every_looktest_a_weight_changed_in_the_document_rescores_on_the_next_run_and_records_the_versiontest_a_folder_glob_weighs_every_file_under_it_whatever_its_extension,test_the_most_specific_glob_winstest_a_path_is_checked_against_the_few_globs_that_could_match_ittest_the_judge_hears_the_repository_s_guidance_and_the_weighing_runs_off_the_event_looptest_an_organization_s_budget_narrows_only_its_own_share_of_a_credentialbots/ai_agentsadd uptest_bots_and_ai_agents_add_to_what_each_layer_inherits,test_an_added_key_holds_what_every_document_below_lists_and_its_own,test_only_a_list_a_layer_may_set_can_be_added_toadded_keys): "no layer takes out a bot the layer below lists"test_an_ai_agent_is_an_address_or_the_start_of_one_up_to_its_attest_an_organization_s_bots_move_into_its_layertest_a_moved_setting_the_section_refuses_is_refused_before_the_owner_is_registeredtest_the_first_start_copies_each_organization_s_bots_into_its_layertest_until_the_import_succeeds_the_meter_reads_the_variables_it_read_beforemeter_reader()took no config; mutation (no fallback): "not imported yet: the variable's 0 still turns it off"test_a_text_of_several_lines_is_a_literal_block_in_the_settings_document_onlyOne mutation passed: building a share from the organization's raw values instead of
min/maxwith the deployment's. It is equivalent, since every share also draws on the credential's own budget, which holds the deployment's values. Themin/maxonly make each share's own numbers honest in the logs.First round's mutations, each caught: no early rescore, version not stored, section bots ignored, deployment values instead of the repository's, no turn-on run, small-repository rule always on, default judge guidance, a repository may set the pace,
slop_meter_botsnot moved.Deploy notes
slop_meter_scores.config_version(NOT NULL DEFAULT ''). It follows refactor(agents): behaviour from agent and template rows #385's 143 onmain; the branch is rebased onto it, andtests/unit/storage/test_migration_144.pynow holds the "is the last entry" assertion, handed over fromtest_migration_143.py. The number lives inembry0/storage/migrations/runner.py,tests/unit/storage/test_migration_144.py(its name andVERSION), the hand-over docstring intests/unit/storage/test_migration_143.py,embry0/storage/repositories/slop_meter.py(two docstrings),docs/architecture.mdanddocs/slop-meter.md.SLOP_METER_INTERVAL_SECONDS,_API_CALLS_PER_TICKand_RATE_RESERVEwhen they are set and differ from the defaults, and copies each organization'sslop_meter_botsinto its layer. Every existing score row hasconfig_version ''; the meter's first run, which scores every caught-up repository, does so with the same numbers and records a version.SLOP_METER_INTERVAL_SECONDS,SLOP_METER_API_CALLS_PER_TICKandSLOP_METER_RATE_RESERVEfrom.envif set; startup logssettings_env_ignoredwhile they are.SLOP_METER_INTERVAL_SECONDS=0pruned once at start and stopped. Turning it on needs no restart.embry0/sandbox/,safety/,agents/,execution/changed. No new route;reading.config_versionis an additive field.Out of scope / follow-ups
slop_policy: kinds, guides, thresholds, the ticket heading, the route template,triage_lean_min_level,enabled) is the second pull request, which finishes feat(slop): Slop-O-Meter scoring and slop routing as settings #169.METER_DUNGEON_LEVEL(embry0/services/slop.py) is routing's threshold and moves with it.Design + Plan
docs/superpowers/specs/2026-10-29-settings-layers-design.md, section 9:organizations.settings.slop_meter_bots→slop_meter.bots)Anti-goals respected
Refs #169