feat(project-layer): Project-layer v1 finalization — canonical shape with warn-only compat (M9)#531
Merged
Merged
Conversation
Contributor
|
Claude encountered an error after 0s —— View job I'll analyze this and get back to you. |
…491) Flip OrchestratorConfig.max_turns from 50 to None so the run-level turn cap is opt-in. A task's max_turns is no longer silently clamped to 50; the effective budget is min(task, run cap) only when an operator sets a cap, and the engine default (DEFAULT_MAX_TURNS = 50) applies when neither the run nor the task declares a value. The inline min-rule at InProcessConductor._run_agent_loop is extracted to a pure resolve_max_turns() that returns a concrete int in every branch, called before the mobile-scaling block so None never reaches max()/range(). Closes #265
…t load time (#496) A task's environment_manifest and a project's default_environment now fail loud at load time when 'stack' or 'stack.compose_file' is explicitly null, naming the offending file and field. Previously stack:null was silently dropped, letting a project's value pass through — the opposite of the documented full-override rule; stack.compose_file:null surfaced as a misleading type error. Omitting the key to inherit stays valid. Closes #235
…ation + actors.user binding (#529) * refactor(core): consolidate schema-shape aliases into deprecations.py; add network_policy + security_context aliases Relocate the three schema-shape alias coercers (task_packs->projects, flat compose_file/runner_service->stack, run_config/->run_configs/) into a new tolokaforge/core/deprecations.py module and add two new accept-with-warning aliases: uppercase network_policy enum names lowercase to the canonical values, and SecurityContext user/group rename to run_as_user/run_as_group (single-source disagreement fails loud). The RunConfig dual-home lifts stay on RunConfig — they are field-level orchestrator->compute/storage migrations, not schema-shape renames. Refs #213 * feat(actors): bind actors.user to the user-simulator runtime; alias top-level user_simulator actors.user is the canonical author shape for the user simulator on TaskDefaults and TaskConfig and now drives the simulator at runtime (conductor + native adapter read it via TaskConfig.resolve_user_simulator). Top-level user_simulator is a legacy alias: the loader lifts it into actors.user per config layer, pre-merge, with a DeprecationWarning, so a canonical project layer and a legacy task layer compose field-by-field (task wins) without a false conflict. A single source declaring both keys fails loud. * refactor(examples): migrate example-microservices-pack to fully canonical Project-layer shape The reference pack's YAML shape is already canonical (stack sub-object, actors.user, run_configs/, evaluation.projects). Strip the remaining legacy narrative so it reads as current state: drop the PROPOSED-schema / not-runnable-until-#211 header, the (#366)-must-land-first and (post-M2) notes in the README, and the backstory_template comment that named an unbuildable composition field. Add the collective pack-migration CHANGELOG entry (#213). * refactor(examples): migrate 6 project-bearing multi_service_* packs to canonical shape Migrate multi_service_cache_debug, multi_service_endpoint_add, multi_service_helpdesk_workflow, multi_service_lot_ops, multi_service_postgres_reset, and multi_service_slow_start to the canonical Project-layer shape: - task.yaml: top-level `user_simulator` -> `actors.user`; flat `environment_manifest.compose_file`/`runner_service` -> under `stack` (helpdesk, lot_ops). - run config: pack-root `run_config.yaml` -> `run_configs/dev.yaml`; `evaluation.task_packs` -> `evaluation.projects` (cache_debug, endpoint_add, helpdesk, lot_ops; reset + slow_start already used `projects`). - helpdesk project.yaml gains `task_defaults.actors.user`. Lock the migration with tests/unit/test_multi_service_packs_canonical.py: every pack loads through load_effective_run_config + the task loader on the canonical shape with zero DeprecationWarning, and `actors.user` drives the resolved user simulator. Update per-pack READMEs, docs/MULTI_CONTAINER_GUIDE.md, hardcoded run-config paths in the six end-to-end integration tests, the helpdesk example-task test, and the CHANGELOG. Refs #213 * refactor(examples): migrate 7 project-less packs to canonical (add project.yaml + run_configs/) Every pack under examples/native now ships a project.yaml and loads fully canonical. Migrates browser_task, coding, tool_use, multi_service, multi_service_advanced, multi_service_postgres and native_shared_domain: - add project.yaml at each pack root (identity + task discovery glob + task_defaults lifting the shared actors.user / adapter / turn budget); - move the pack-root run_config.yaml to run_configs/dev.yaml (native_shared_domain also gains run_configs/gate_demo.yaml); - evaluation.task_packs -> evaluation.projects; - top-level user_simulator -> actors.user (shared mode/persona lifted to task_defaults, per-task backstory kept as a delta); - flat environment_manifest.compose_file/runner_service -> stack (the three multi_service packs). Locks the migration with tests/unit/test_project_less_packs_canonical.py: every run config and every task loads with zero DeprecationWarning and actors.user drives the resolved user simulator. Fixes a latent path bug in test_task_loader.py (the native_shared_domain smoke check pointed at a non-existent examples/native_shared_domain path and always skipped) and updates every hardcoded run-config path across docs, scripts, the Makefile and tests to run_configs/dev.yaml. Refs #213 * feat(core): enable extra="forbid" on all Project-layer models + difflib closest-match loader error Turn on strict validation across every Project-layer Pydantic model (ProjectConfig, RunConfig, TaskConfig, GradingConfig transitive closures, plus EnvironmentPatch) so unknown keys in project.yaml / run_configs/*.yaml / task.yaml / grading.yaml now fail loud. A new construct_config helper in tolokaforge/core/project_loader.py wraps pydantic.ValidationError and, for each extra_forbidden error, builds a message naming the file (basename or repo-relative — never the absolute path from tmp_path), the offending key path, and the closest schema match via difflib.get_close_matches. Non-extra validation errors re-raise unchanged. All four load sites route through the helper: ProjectConfig via project_loader, TaskConfig via _task_loader, GradingConfig via native adapter, RunConfig at the four cli/main.py invocation points. Aliases (task_packs, user_simulator, uppercase network_policy, security_context user/group) still work because they lift/rename in mode="before" before the extra check runs. Fixture and test fallout: strip stray keys from tests/data/tasks/browser_basic and calc_basic grading fixtures; correct one Optional wrong-kwarg construction in test_orchestrator_logic; drop an obsolete legacy key in test_project_loader_end_to_end; update the terminal-bench external adapter's GradingConfig construction to the canonical shape. Behaviour locked: 11 unit tests (5 model roots reject unknown keys; construct_config error surface names file+key+suggestion; non-extra errors re-raise; four aliases still accepted after forbid) + 1 canonical snapshot pinning the error-message shape (basename-only, portable across machines). * feat(core)!: require project.yaml — remove transitional default synthesiser A pack must ship a project.yaml. The transitional default-project synthesiser is removed: a run config with no discoverable project.yaml (walking up from the config file to the filesystem root) now fails at load with a RuntimeError naming the searched root and instructing the author to add one, in the loader's existing fail-loud style. BREAKING: external packs that relied on the synthesised default must add a minimal project.yaml (name: alone suffices) at the pack root. All 14 in-tree example packs already ship a project.yaml (Stage 5), so no in-tree consumer breaks. * docs(m4): rewrite M4 doc drift — actors example, run_config paths, run configs directory prose
…ical + parallel-safe integration lane (#530) * fix(loader): stop project-scoped task_defaults leaking into strict TaskConfig validation The task loader deep-merged the entire project.task_defaults dict into each task dict before validating against TaskConfig. Since #213/#529 made TaskConfig extra="forbid", the project-scoped-only defaults grading_defaults and continue_prompt — legitimate task_defaults keys that are not TaskConfig fields — were rejected, so the shipped example-microservices-pack could not build a single TaskDescription. Narrow the merged layer to TaskConfig-shaped keys. The excluded set is derived from the schema (TaskDefaults.model_fields - TaskConfig.model_fields), so future project-only defaults are handled automatically. The adapter's stored _project_task_defaults is untouched — grading_defaults still flows through NativeAdapter.get_grading_config. A genuinely-unknown key authored in task.yaml still raises the friendly construct_config error. * test(canonical): promote pack wiring-proof tests from integration → canonical test_example_microservices_pack.py is a Docker-free, LLM-free load-only wiring proof: it exercises the project.yaml → per-task manifest → backend-selector path against the shipped example-microservices-pack without spinning up any external service. It therefore belongs in the canonical tier, not integration. CI selects lanes by directory (tests/unit/ + tests/canonical/ under the required test-smoke gate; tests/integration/ only on push/nightly/gate), so the file is physically moved rather than just re-marked. Running these tests in the required PR gate is what closes the gap that let the M4 strict-schema regression ship unnoticed. * test(integration): pytest-xdist + per-worker COMPOSE_PROJECT_NAME for parallel-safe Docker stacks testcontainers runs Docker Compose with cwd set to the context dir and passes no --project-name, so Compose derives the project name from that dir's basename. Integration tests that build their stack under tmp_path/"compose" all resolve to project name "compose"; under pytest -n auto two such stacks on different workers share one project namespace and collide on container/network names. Add pytest-xdist to the dev dependency group and a tests/integration autouse session fixture that pins a unique COMPOSE_PROJECT_NAME per xdist worker (falling back to a stable name without xdist). This isolates every worker's stacks in one place, without editing each DockerCompose call site. Per-worker uniqueness suffices because a worker runs its tests sequentially. Both CI integration steps (test-full, test-gate) now run with -n auto. * fix(tests,docs): narrow COMPOSE_PROJECT_NAME conftest scope; correct PROJECTS.md field-ownership table Move the per-worker COMPOSE_PROJECT_NAME autouse fixture from tests/integration/conftest.py to tests/integration/reset_recipes/conftest.py. The env pin only serves the reset-recipe suite, whose stacks all share the `compose` basename; applied worker-wide it collapsed the two concurrent stacks in test_per_trial_isolation_across_concurrent_instances (which rely on make_project_temp_dir's slug-encoded basenames) into one project namespace. Addresses reviewer-correctness's finding for that isolation test. Split the docs/PROJECTS.md field-ownership row: grading_defaults and continue_prompt are project-scoped with no per-task delta (a task.yaml naming either fails with unknown key under TaskConfig's extra="forbid"); the row previously promised a task.yaml delta path that has not existed since #213. Addresses reviewer-hygiene M1. * fix(tests): scope COMPOSE_PROJECT_NAME pin to function lifetime — no cross-test env leak Round-2 fix for the correctness reviewer's follow-on finding. The session-scoped autouse fixture left COMPOSE_PROJECT_NAME pinned for the rest of the worker's queue, so sibling docker tests (notably test_per_trial_isolation_across_concurrent_instances) inherited the pin and their two concurrent stacks collapsed into one Compose namespace. Function-scoped monkeypatch restores the prior env after each reset-recipe test. Also drops a stray bare `prompt` token from the task_defaults enumeration in docs/PROJECTS.md.
…nthesiser + revert max_turns flip M9 originally shipped strict validation across every Project-layer model (extra="forbid") plus removal of the transitional synthesize_default_project fallback and a flip of OrchestratorConfig.max_turns default (int=50 → int|None=None). All three were hard backward-compat breaks that would fail existing external task packs at load. Soften before merge so a gradual rollout is possible: canonical shapes ship with actionable DeprecationWarnings; a future release flips to strict after a deprecation window (tracked in #533 for strict flip, #534 for the max_turns default flip). Changes: 1. extra="forbid" -> extra="ignore" on 42 Project-layer models (39 in core/models.py, EnvironmentPatch in runner/models.py, plus 3 misses from the initial soften pass: ActorSpec, SeedRef, AssetsConfig). LocalStorageConfig / S3StorageConfig kept as forbid (discriminated- union tag safety). 2. construct_config warns instead of raises on unknown top-level keys. Message names the file basename, the offending key, the closest schema match via difflib, and a "(tracked in #533)" suffix pointing at the follow-up. Nested unknown keys are silently dropped (recursive scan deferred to #533). 3. synthesize_default_project restored. A run config with no discoverable project.yaml emits a DeprecationWarning naming the searched root and the exact fix ("Add a project.yaml at the pack root — name: <pack> alone suffices"), then falls back to a minimal ProjectConfig so the pack loads unchanged. 4. stack: null / stack.compose_file: null softened to warn-only in both _task_loader.py::_resolve_environment_manifest_paths and project_loader.py::_warn_null_stack (renamed from _reject_null_stack). The null key is dropped so the loader treats it as unset (inherit- from-project). Non-None non-str compose_file still hard-raises the type error. 5. OrchestratorConfig.max_turns default reverted int|None=None -> int=50. Restored pre-M9 always-on-cap semantic. resolve_max_turns helper and DEFAULT_MAX_TURNS constant kept (pure code cleanup, no behaviour change). Follow-up #534 tracks the eventual opt-in-cap flip. 6. Direct-Python compat shim on TaskConfig / TaskDefaults: a mode="before" validator (_accept_legacy_user_simulator_kwarg) lifts TaskConfig(user_simulator=...) kwargs into actors["user"] with a DeprecationWarning. Reuses the existing canonicalize_actor_config coercer via a small _lift_user_simulator_kwarg helper that model-dumps a UserSimulatorConfig instance before lift. 7. Deprecation message quality bar. Every warning now includes: what legacy shape triggered it, where (file basename via ContextVar-based source_context), why (canonical form), how to fix (concrete rename or block-move with a worked example), and when it goes away ((tracked in #NNN) suffix pointing at the retirement follow-up). warn_deprecated gains a follow_up_issue param defaulting to POST_M9_STRICT_FLIP_ISSUE = "533". Test updates: raises->warns for stack:null tests; soft-drop assertions for 3 reservation tests; direct-Python user_simulator= kwarg shim tests added; TestResolveMaxTurns renamed to reflect the reverted default-50 clamp; canonical error-message snapshot regenerated. Docs: CHANGELOG rewrote the M9 keystone entry to describe the warn-only rollout and added a "Compat / migration notes" section; PROJECTS.md, CONFIG.md, REFERENCE.md, SECURITY.md reverted max_turns prose to the default-50 always-on-cap shape with a #534 breadcrumb. Validated: unit 2640 pass, canonical 526 pass, lint clean. End-to-end load-probe of the private ots pack (tolokaforge-tasks/config/tau_manufacturing/smoke_comm_rubrics_kimi_k2.6.yaml): loads with 0 errors and 5 actionable DeprecationWarnings. Refs: #213, #235, #265, #277, #489, #499, #533, #534.
CiroGamboa
force-pushed
the
feat/project-layer-v1-final
branch
from
July 20, 2026 17:54
6a7a691 to
88b0989
Compare
Contributor
|
Claude encountered an error after 0s —— View job I'll analyze this and get back to you. |
…cklevel note Addresses three review findings on PR #531 from my own /review pass: 1. Absolute-path leakage in warning messages. `_warn_null_stack` (project_loader.py) and the two stack:null branches in _task_loader.py emitted `warnings.warn(f"... at {path}: ...")` with `path` being the full absolute path — violating the basename-only policy the rest of M9 established via `source_context`. Route all four through `warn_deprecated(...)` inside `source_context(path)` so the basename is used consistently. Test snapshots stay machine-independent; prod logs stop leaking user home paths. 2. Inconsistent tracker-suffix handling. Three sites built the message inline and hardcoded `(tracked in #533)`; two more had raw `warnings.warn` calls without going through `warn_deprecated` at all. Consolidated: `coerce_task_packs_alias` both-set branch, `warn_legacy_run_config_dir`, `synthesize_default_project`, and the two `stack: null` sites now all route through `warn_deprecated(...)` which handles the tracker suffix uniformly. 3. `warn_deprecated` stacklevel docstring caveat. Old docstring claimed "stacklevel=3 targets the coercer's caller so IDE warnings point at user code." True only for direct-loader calls; when the coercer is invoked from a Pydantic `mode="before"` validator, Pydantic inserts wrapper frames and the warning surfaces at the model class instead. Added the caveat and a `stacklevel` param so callers with a known depth can target user code precisely. Test-message assertions in test_task_loader.py, test_project_loader.py, test_project_loader_end_to_end.py, and test_schema_shape_aliases.py updated to match the new consolidated message shape (regex-based so the `in <file>` clause between legacy and "is deprecated" is tolerated). Validated: unit 2640 pass; private ots pack load-probe emits 5 DeprecationWarnings with zero absolute-path leaks (grepped for `/Users/` / `/private/` / `/tmp/` in every warning). Refs: PR #531 self-review.
Contributor
|
Claude encountered an error after 0s —— View job I'll analyze this and get back to you. |
This was referenced Jul 20, 2026
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.
TL;DR
Activates the finalized Project-layer shape from M2.5 with warn-only compatibility — every legacy shape a real task pack ships continues to load unchanged, with a
DeprecationWarningnaming the file, the offending key, and the concrete migration action. No hard breaks in this release. Post-M9 follow-up #533 tracks the future strict-rejection flip once a deprecation-window release cycle has closed.Also: an
actors.userruntime binding so a shape that was previously parsed but inert now actually configures the user simulator (backward-compat via theuser_simulatoralias); a schema-derived filter that fixes an M4 regression in theexample-microservices-packload path; and pack-wiring tests promoted to the required PR gate so a similar regression cannot ship silently again.Closes #265, #235, #213, #277.
Impact on existing tasks — read this first
Zero hard breaks. A task pack that loads against
maintoday continues to load, withDeprecationWarnings that point at the migration path. Concrete guarantees:project.yaml— packs without one continue to load via a synthesised default. The synthesiser now emits aDeprecationWarningnaming the searched root and the exact fix (Add a project.yaml at the pack root —name:alone suffices). Follow-up Follow-up (post-M9): re-flip Project-layer strict validation + remove synthesize_default_project #533 flips this to a hard error after a deprecation window.project.yaml/run_configs/*.yaml/task.yaml/grading.yamlno longer fail load.construct_configwarns with a message shapedunknown key 'mox_turns' in dev.yaml — did you mean 'max_turns'? Rename 'mox_turns' to 'max_turns' (or remove it if unused). (tracked in #533)— top-level scan only; the key is silently dropped from the model instance so existing configs keep working.orchestrator.max_turnsdefault stays atint = 50(pre-M9 always-on-cap semantic). Tasks that shipmax_turns: 100continue to clamp to the run-cap of 50. Follow-up Follow-up (post-M9): opt-in OrchestratorConfig.max_turns default flip (redo #265) #534 tracks the eventual opt-in-cap flip (defaultint | None = None) after the deprecation window.stack: null/stack.compose_file: nullin a task'senvironment_manifest(and in a project'sdefault_environment) emit aDeprecationWarningand drop the null so the loader treats the key as unset (inherit-from-project). Non-Nonenon-strcompose_file still hard-raises a type error.TaskConfig(user_simulator=UserSimulatorConfig(...))continues to work via amode="before"shim that lifts the kwarg intoactors["user"]with a warning — the field itself is gone, but the compat path is preserved.Semantic changes covered by aliases (still accepted with warning):
actors.useris now the canonical author shape for the user simulator and drives it at runtime. Top-leveluser_simulator(either in YAML or as a Python kwarg) is a legacy alias.evaluation.projectsreplacesevaluation.task_packs.network_policylowercase enum values (no_internet,limited_internet,full_internet) replace uppercase (NO_INTERNET, ...). Uppercase accepted with warning, lowercased at parse time.security_context_defaults.run_as_user/.run_as_groupreplace.user/.group. Legacy accepted with warning; disagreeing values fail loud.stacksub-object ondefault_environmentis the canonical substrate shape; flatcompose_file/runner_serviceat the top level ofEnvironmentPatchare accepted with warning.Deprecation message quality bar. Every warning M9 emits follows a uniform actionable shape: what legacy shape triggered it, where it lives (file basename via
source_context), why it is deprecated, how to migrate (a concrete key rename or block move with a worked example), and when it goes away (a(tracked in #NNN)suffix pointing at the follow-up issue that carries the retirement schedule). Example messages produced against the real private ots task pack:Design walkthrough
flowchart TB subgraph before ["Before M9 (main)"] b_yaml["task.yaml + project.yaml (optional)"] b_loader["_task_loader.py<br/>whole-dict deep_merge<br/>silent extra=None drop"] b_config["TaskConfig<br/>extra=None (silent drop)<br/>user_simulator field lives here"] b_runtime["runtime reads<br/>task.user_simulator"] b_yaml --> b_loader b_loader --> b_config b_config --> b_runtime end subgraph after ["After M9 (this PR — warn-only)"] a_yaml["task.yaml + project.yaml<br/>(project.yaml still optional — warn)"] a_dep["deprecations.py<br/>per-layer alias coercion<br/>source_context threading<br/>tracker-suffixed messages"] a_loader["_task_loader.py<br/>filter TaskDefaults-only keys<br/>then deep_merge"] a_construct["construct_config helper<br/>warns on unknown top-level keys<br/>difflib closest-match suggestion<br/>file + key + how-to-fix + tracker"] a_config["TaskConfig<br/>extra=ignore + warn-on-unknown<br/>actors is canonical home<br/>resolve_user_simulator()<br/>user_simulator= kwarg shim"] a_synth["synthesize_default_project<br/>DeprecationWarning<br/>fallback for missing project.yaml"] a_runtime["runtime reads<br/>actors['user']"] a_yaml --> a_dep a_dep --> a_loader a_loader --> a_construct a_construct --> a_config a_config --> a_runtime a_yaml -.no project.yaml.-> a_synth a_synth --> a_config end before -.M9 keystone: #213/#529 → soften: #531/88b0989.-> after style a_dep fill:#e1f5e1 style a_construct fill:#e1f5e1 style a_synth fill:#fff3cd style a_config fill:#e1f5e1Design choices
tolokaforge-tasks/config/tau_manufacturing/— still ship the pre-M9 shape (noproject.yaml, root-levelrun_config.yaml,orchestrator.workersdual-home alias,orchestrator.runtime: "docker"). Any hard break would fail those packs at load. Follow-up Follow-up (post-M9): re-flip Project-layer strict validation + remove synthesize_default_project #533 lands the strict flip after a deprecation-window release cycle.extra="ignore"on all Project-layer models +construct_configwarns on unknown keys. Model-levelextra="ignore"keeps loads permissive. The loader-level scan (before Pydantic construction) catches unknown top-level keys and warns with a file+key+difflib+tracker message. Nested unknown keys are silently dropped in this release — the recursive scan comes back with the strict flip in Follow-up (post-M9): re-flip Project-layer strict validation + remove synthesize_default_project #533. This keeps the message quality high without requiring a full schema-walk implementation.ContextVar.warn_deprecatedreadssource_context(aContextVar[Path | None]) so every alias warning names the file basename without threadingsourcethrough every coercer signature.construct_config,load_project_config,_task_loader.load_task_yaml, and_load_domain_dictset the context; Python-level callers (no loader in the stack) get warnings without ain <file>clause.follow_up_issueparam onwarn_deprecateddefaulting to#533. Every alias points at the strict-flip follow-up as its retirement tracker.synthesize_default_projectalso points at Follow-up (post-M9): re-flip Project-layer strict validation + remove synthesize_default_project #533; the max_turns docs point at Follow-up (post-M9): opt-in OrchestratorConfig.max_turns default flip (redo #265) #534. Users have a concrete issue to subscribe to.actors.userruntime binding kept (the M4 Stage 2 work). Alias covers the YAML compat surface, direct-Pythonuser_simulator=kwarg shim covers the Python compat surface. This is a soft semantic change but reversible via the alias if a caller notices behavior drift.project.task_defaults(from perf(tests): integration suite dominated by Docker lifecycle — proposals to speed up #277): the merge into task dicts excludesTaskDefaults-only keys (grading_defaults,continue_prompt) that would otherwise trip the strict-schema warnings on every task load. Derived fromTaskDefaults.model_fields - TaskConfig.model_fields, not hardcoded — future project-only defaults handled automatically.tests/integration/→tests/canonical/(from perf(tests): integration suite dominated by Docker lifecycle — proposals to speed up #277) so a future regression that breaks a shipped pack fails on the introducing PR (test-smokegate), not weeks later on nightly.Concepts introduced
tolokaforge/core/deprecations.py— canonical home for schema-shape alias coercers +warn_deprecateduniform message shape helper +source_contextfor file-path threading +POST_M9_STRICT_FLIP_ISSUE = "533"retirement tracker.construct_config(model, data, *, source, section) -> BaseModelinproject_loader.py— the loader-side warn-on-unknown-key helper. Every YAML load site (project, task, run, grading) routes through it.TaskConfig.resolve_user_simulator() -> UserSimulatorConfig— readsactors["user"]and applies simulator defaults for unset fields. Called by the conductor and native adapter._lift_user_simulator_kwarg+_accept_legacy_user_simulator_kwargmode="before"validator onTaskConfig/TaskDefaults— the direct-Python compat shim for callers passinguser_simulator=...as a constructor kwarg.run_configs/<name>.yamlconvention for in-tree examples — replaces the pack-rootrun_config.yamllayout. External packs (including the private ots pack) still use the root-level shape; both work.Plan
The plan lives at
~/.claude/plans/toloka-tolokaforge/issue-213-m4-strict-schema.md(M4 keystone) plus the softening plan at~/.claude/plans/dapper-painting-key.md(this PR's rollout). Stage-by-stage in the commits below.Suggested review order
0bd3bf6(OrchestratorConfig.max_turns default should be None, not 50 #265 max_turns default) — small warmup; extract-only, no semantic change (the flip was reverted in88b0989).443c367(Reject stack: null and stack.compose_file: null at load time #235 stack:null rejection) — establishes the loader-parse-boundary pattern.ad15b69(Project layer — strict schema + example-pack migration (M4) #213 M4 keystone) — 8 stages:deprecations.py,actors.userruntime binding, all 14 packs canonical,extra="forbid"+ difflib, synth removal.b1b9711(perf(tests): integration suite dominated by Docker lifecycle — proposals to speed up #277 regression fix + pack tests promoted) — the M4 regression fix +pytest-xdist+run_configs/dev.yamlpath renames.88b0989(softening — this commit) — reverts the hard breaks to warn-only, restores the synthesiser, revertsmax_turnsdefault, adds direct-Python shims, upgrades deprecation message quality, files follow-ups Follow-up (post-M9): re-flip Project-layer strict validation + remove synthesize_default_project #533 / Follow-up (post-M9): opt-in OrchestratorConfig.max_turns default flip (redo #265) #534.Discovered issues
Filed as follow-ups (retirement schedule tracker):
extra="forbid", removesynthesize_default_project, re-flipstack: nullto hard errors, add the recursive unknown-key scan. Fires one release cycle after M9 lands.max_turnsdefault flip (redo OrchestratorConfig.max_turns default should be None, not 50 #265): flipint = 50back toint | None = Noneafter the deprecation window closes.Pre-existing (from M4 planning):
orchestrator.timeoutsopt-in default (sibling of OrchestratorConfig.max_turns default should be None, not 50 #265). Deferred to bundle with M5'sturn_s/episode_s→trial_seconds/tool_call_secondsrename.backstory_template+ per-taskscenario) unbuildable under strictActorSpec. M4 corrected the doc example; the composition feature is post-M9 work.Test plan
uv run pytest -m unit— 2640 pass, 1 skip. Every M9-touched path + previously-strict tests converted to soft-warn assertions.uv run pytest -m canonical— 526 pass. Snapshot regenerated for the error-message shape.uv run lint_check— clean.End-to-end load-probe against the private ots task pack (
tolokaforge-tasks/config/tau_manufacturing/smoke_comm_rubrics_kimi_k2.6.yaml) — loads with 0 errors and 5 actionable DeprecationWarnings naming every legacy shape it uses:project.yamlat pack rootorchestrator.workers→compute.workers(pre-existing dual-home alias)orchestrator.max_attempt_retries→compute.max_attempt_retries(pre-existing)orchestrator.runtime = "docker"→ alias for"shared"(pre-existing)orchestrator.runtimedeprecated (backend selection now task-driven, pre-existing)Post-merge: any external pack that today loads on
maincontinues to load with the new warnings pointing at the migration path. The strict-flip follow-up (Follow-up (post-M9): re-flip Project-layer strict validation + remove synthesize_default_project #533) will land these breaks properly after the deprecation-window release cycle.CI note: the
hygiene-reviewworkflow lane has a known infrastructure failure on this repo right now; merging past it is standard (matches the last several PRs onmain).Closes #213, #235, #265, #277.