myxce/.kiro/specs/scheduler-sim-live-allocation/tasks.md
Rongsong Shen c2b3bf6473
chore(specs): mark four finished specs implemented
scheduler-sim-diagnostics, scheduler-sim-live-allocation, xce-test-cli-ux
and eliminate-frontend-makefile had every sub-task checked but their
parent boxes and spec.json phase still read tasks-generated.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 13:00:11 +00:00

30 KiB

Implementation Plan: scheduler-sim-live-allocation

  • 1. Foundation: independent core-safe seams and leaf migrations

  • 1.1 (P) Give the allocation dispatch chokepoint an optional, normal-mode-only performance cache seam

    • Add two NULL-by-default callback fields to the shared core state for consulting and populating the unsatisfiable-resreq cache, mirroring the existing session-reset callback's proven shape (a bare function pointer taking the core state directly, no wrapper struct, no opaque handle)
    • Implement the two adapter functions against the real cache, in the same file the existing session-reset adapter already lives in, registered only during normal-mode startup
    • TDD: add a test asserting that with both callbacks left NULL, the allocation dispatch chokepoint always computes fresh with no crash and no incorrect skip; add a normal-mode test confirming the cache is still consulted/populated exactly as before this change
    • Observable: a simulation-mode session (which never registers these callbacks) never touches the real cache implementation at all, while a normal-mode session's cache behavior is provably unchanged
    • Requirements: 5.2
    • Boundary: Unsatisfiable-Cache Callback Seam
  • 1.2 (P) Make plugin hook dispatch driver-agnostic and give every plugin an honest reference to shared core state

    • Re-point the three plugin-hook dispatch functions (job election, prepare-allocation, commit-decision) to iterate the shared plugin registry directly instead of deriving it from a normal-mode-only driver struct, mirroring the session-begin/session-end dispatchers' own already-completed migration
    • Add a new, honestly-typed core-state reference field to the plugin instance struct, set by BOTH existing plugin-registration entry points (the normal-mode one and the simulation-mode one) — leave the plugin's existing, differently-typed driver-reference field completely untouched, since an unrelated legacy picker plugin still depends on its current meaning
    • TDD: extend the existing plugin-hook dispatch tests to run unchanged against a core-state fixture built either way (embedded in a normal-mode-shaped or simulation-mode-shaped container), and add a test confirming a plugin registered via the simulation-mode entry point has a non-NULL core-state reference (closing today's gap where it silently stays unset)
    • Observable: a plugin registered through either registration path can reach shared core state without ever reading the normal-mode-only field, and the existing dispatch behavior (each registered plugin's hook invoked exactly once, in registration order) is unchanged
    • Requirements: 1.1, 2.1, 2.2, 5.1
    • Boundary: Plugin Hook Dispatchers
  • 1.3 (P) Make the per-host and shared capacity checks driver-agnostic

    • Re-type the host-selection match, shared-capacity check, and per-host capacity check functions to operate on shared core state directly instead of a normal-mode-only driver struct
    • TDD: re-run their existing unit tests unchanged (pure signature migration) plus a new test calling each function with core state drawn from a simulation-mode-shaped container, confirming identical results to the normal-mode-shaped case
    • Observable: these three functions compile and pass identically regardless of which driver's core state they're given
    • Requirements: 1.3, 3.1, 3.2
    • Boundary: Core-Safe Allocation Chain
  • 2. Core: driver-agnostic allocation search and dispatch

  • 2.1 Thread shared core state through the per-job allocation context and the per-host allocation attempt

    • Change the per-job allocation context's driver-reference field to carry shared core state instead of a normal-mode-only pointer, updating its handful of existing cast sites
    • Re-type the per-host allocation-attempt function (which calls the three capacity checks from 1.3 and the prepare-allocation hook from 1.2) to take shared core state directly
    • Thread the same change through the existing multi-host allocation search's entry points, preserving their host-iteration and bucket/plan-construction algorithm exactly as-is
    • TDD: re-run the existing multi-host allocation and per-host-attempt test suites unchanged; add a test driving the per-host attempt function with simulation-mode-shaped core state end to end
    • Observable: the per-host allocation attempt and the multi-host search both run correctly when given either driver's core state, with zero change to which hosts get selected or in what order
    • Depends: 1.2, 1.3
    • Requirements: 1.3, 3.1, 3.2
    • Boundary: Core-Safe Allocation Chain
  • 2.2 Make primary/compound/alter allocation dispatch and decision commit driver-agnostic

    • Re-type the top-level allocation dispatch switch and its primary/compound/alter sub-functions, and the decision-commit function, to take shared core state directly
    • Wire the one genuine normal-mode-only touch point (the unsatisfiable-resreq cache) through the seam built in 1.1 instead of reading it directly
    • Confirm by reading each sub-function's body before migrating it that it touches nothing beyond core state, worker/context fields, and the one cache touch point. If a sub-function is found to touch additional normal-mode-only state, stop and split that state's seam into a new follow-up task (mirroring 1.1's pattern) rather than designing and building it within this task
    • TDD: re-run the existing allocation-dispatch and decision-commit test suites unchanged; add a normal-mode test confirming the cache fast path still activates correctly through the new seam
    • Observable: a job's dispatch decision and resulting resource-accounting state are identical before and after this migration, for every existing allocation-dispatch test
    • Depends: 2.1, 1.1
    • Requirements: 1.1, 1.3, 3.1, 3.2, 5.2
    • Boundary: Core-Safe Allocation Chain, Unsatisfiable-Cache Callback Seam
  • 2.3 (P) Make global and host-scoped resource-limit matching driver-agnostic

    • Change the limit plugin's global-limit and host-scoped-limit matching functions to read the new honest core-state reference from 1.2 instead of deriving anything from the plugin's normal-mode-only field
    • Also address the confirmed-dead, type-confused write at plugins/limit.c:319 (ctx.sched = plugin->sched;, flagged during task 2.1's review — never read back today, but stores an xce_scheduler_t*-shaped value into a field that is now genuinely xce_sched_core_t*-typed): either populate it honestly from the plugin's new core-state reference, or remove the dead write entirely if nothing in the migrated code path needs it
    • Keep the existing plugin-hook-registered entry points' external signature unchanged; have them resolve core state from the new field and delegate to the now-driver-agnostic matching logic
    • TDD: extend the limit plugin's existing test suite with a case that registers the plugin via the simulation-mode entry point and confirms a match still succeeds and still registers the matched limit's usage, where today it would silently no-op
    • Observable: a limit configured on a job registered via the simulation-mode plugin path is matched and its usage instance created, verifiable by reading the plugin's own configured-limits/usage-instances accessors
    • Depends: 1.2
    • Requirements: 2.1, 2.2, 2.3, 2.4
    • Boundary: Limit Plugin Core-Safe Matching
  • 3. Core: unify job election and allocation into one implementation

  • 3.1 Extract the job-picking and allocation loop into a single implementation shared by both drivers

    • Move the job-picking thread body and the allocation thread body out of the normal-mode-only file they live in today, into their own file, re-typed to operate on shared core state directly
    • Confirm every call this pair makes is already driver-agnostic after tasks 1.2/2.1/2.2 — no remaining reference to anything normal-mode-only
    • Update normal mode's own startup to spawn these (now-relocated) threads unchanged in behavior
    • TDD: re-run the full existing normal-mode scheduling test suite unchanged (behavior-preserving relocation); add a test spawning both threads against a simulation-mode-shaped core state fixture and confirming a job flows all the way from being picked to a committed decision
    • Observable: normal mode's own scheduling behavior is bit-for-bit unchanged, and the identical thread pair now also runs correctly when spawned against simulation-mode's own core state in a test fixture
    • Depends: 2.2, 2.3
    • Requirements: 1.1, 1.2, 5.1
    • Boundary: Unified Picker/Allocator Threads
  • 4. Integration: wire simulation mode onto the shared implementation

  • 4.1 Replace simulation mode's placeholder picker with the shared picker/allocator pair

    • Replace simulation mode's own always-pend placeholder thread with the shared job-picking and allocation thread pair from 3.1, spawned against simulation mode's own core state
    • Confirm the existing one-job-at-a-time hand-off guarantee between picking and allocating still holds now that simulation mode genuinely uses it for the first time
    • TDD: write a new fixture using a non-stack-local (static or heap-allocated) simulation driver instance — a prior, still-recurring bug class in this exact area came from a stack-local instance outliving a never-joined thread, and this change doubles the number of such threads, so this must be positively confirmed, not assumed safe by analogy
    • Observable: a simulation-mode session, driven from a real replay with no changes to the replay format, produces a genuine dispatch or pend decision for a job based on its actual resource availability, instead of unconditionally pending it
    • Depends: 3.1
    • Requirements: 1.1, 1.2, 3.3, 4.2, 4.3, 5.1
    • Boundary: Unified Picker/Allocator Threads, Sim Driver
  • 4.2 Narrow recorded-decision reconciliation to the timing-gap safety net

    • Add a call to the now-driver-agnostic job-election dispatch (from 1.2) for a job this mechanism reconstructs from the recording, so a job whose outcome comes from the recording still gets its global-limit usage registered
    • Confirm the existing trigger guard (a job already decided is never reconsidered) needs no change, since it already excludes any job the live path from 4.1 has already handled
    • TDD: extend the existing reconciliation test suite with a case confirming a reconciled job's global limit is registered, and a case confirming a job the live path already decided is never reprocessed by this mechanism
    • Observable: replaying a recorded session in which a job's dispatch was found only via reconciliation still shows that job's limit usage through the existing limit-usage accessors
    • Depends: 4.1, 2.3
    • Requirements: 3.4, 3.5
    • Boundary: Sim-Mode Reconciliation Narrowing
  • 4.3 Close the host-scoped limit fan-out seam gap for live simulation-mode dispatch

    • Found during task 4.1's review, mechanism-traced and documented in design.md's "Known Seam Gap: Host-Scoped Limit Fan-Out During Live Seqjob/Parjob Dispatch": sim_bootstrap_seqjob_allocator()/sim_bootstrap_parjob_allocator()'s throwaway-wrapper registration means the seqjob/parjob allocator plugins' prepare-allocation-hook fan-out iterates the wrapper's own separate plugin registry, never reaching any plugin (the limit plugin included) registered only against simulation mode's real core — so a host-scoped limit's usage instance can never be created via a live seqjob/parjob dispatch attempt today
    • Fix this with an honest, narrow seam (mirroring the unsatisfiable_cache callback pattern from 1.1, or an equally narrow alternative) satisfying seqjob/parjob's own pre-existing init_fn requirement for a real xce_scheduler_t* without substituting a separate plugin registry for the fan-out itself — do not repeat the throwaway-wrapper-with-a-separate-registry shape that created this gap
    • TDD: a test proving a host-scoped limit registered directly against simulation mode's real core is matched and its usage instance created during a genuine seqjob or parjob dispatch attempt, independent of recorded-decision reconciliation and independent of global-limit matching
    • Observable: a host-scoped limit's usage instance is created and visible via the existing configured-limits/usage-instances accessors after a live simulation-mode dispatch that never touched reconciliation
    • Depends: 4.1
    • Requirements: 2.2
    • Boundary: Unified Picker/Allocator Threads, Limit Plugin Core-Safe Matching
  • 5. Validation: prove equivalence and the concrete acceptance test

  • 5.1 Prove identical decisions and accounting state under resource contention across both drivers

    • Build a resource-contention fixture (multiple jobs, capacity insufficient for all of them, including capacity constrained by a matching limit) and drive it through both drivers
    • Observable: both drivers produce the same admission/pend outcome and the same final resource-accounting state for the identical input
    • Depends: 4.1
    • Requirements: 3.1, 3.2
  • 5.2 Prove the concrete acceptance test: real global and host-scoped limit usage visible against a real replay

    • Drive a real simulation session through a replay whose original live session had both a global and a host-scoped limit actively enforced
    • Observable: the existing limit-inspection command displays real, non-zero usage for both limits against this replay, where today it reports no active usage instance for either
    • Depends: 4.1, 4.2, 4.3
    • Requirements: 2.1, 2.2, 2.3, 2.4
  • 5.3 Re-run existing simulation-mode equivalence suites and reconcile any behavior change

    • Re-run the REPL/batch decision-equivalence suite and the existing normal-mode-recording-vs-simulation-replay decision-identity suites in full
    • Investigate and explain, rather than silently update, any assertion whose expected output changes as a result of this feature
    • Observable: all three suites pass, or every changed assertion has a written explanation of why the new behavior is correct
    • Depends: 4.1
    • Requirements: 4.1, 4.2, 4.3
  • 5.4 Validate against real recorded sessions and root-cause any divergence found

    • Run live-vs-recorded verification against a range of real recorded sessions, not only crafted fixtures
    • Treat any reported mismatch between a live-computed decision and its recorded counterpart as a finding to root-cause (either a defect in this feature's own migration, or a genuine issue the old always-pend behavior could never have exposed), never as something to suppress
    • Observable: a written disposition (fixed, or explained as a pre-existing issue) exists for every mismatch found this way
    • Depends: 5.1, 5.2, 5.3
    • Requirements: 3.3, 3.5

Implementation Notes

  • Task 1.1: initial submission implemented before tests were written (TDD-mandate violation, caught in review round 1) — the real RED phase must be captured by reverting production files to HEAD via a saved patch + git checkout --, rebuilding to see the genuine failure, then reapplying. Retroactively-written tests plus a passing GREEN build alone do not satisfy this project's TDD discipline; the reviewer independently re-derives RED by reverting your changes, so skipping this is always caught.
  • Task 4.1: making a driver-safe function GENUINELY reachable for the first time (sim mode's allocator finally calling sched_make_allocation() for real) can expose a pre-existing gap that had zero prior symptoms simply because the code path was previously unreachable (bug-1829: core->seqjob_allocator/parjob_allocator were never bootstrapped for sim mode at all). Fixing such a blocking bug inside the task that exposes it is acceptable (same precedent as bug-1828, task 2.3) — but a fix that works around a plugin's own out-of-boundary dependency (here, seqjob_allocator_init()/parjob_allocator_init() needing a real xce_scheduler_t*) via a throwaway wrapper with its OWN separate plugin registry can silently reintroduce exactly the two-realities-diverge risk this whole feature exists to eliminate — it did here (task 4.3 exists specifically to close it). When a workaround must stand in for a real fix, prefer an honest narrow seam (mirroring 1.1's callback pattern) over substituting an entire parallel registry/struct, and document the exact substitution's blast radius in design.md immediately, not after review catches the gap.
  • Task 3.1: two pre-existing, out-of-boundary gaps blocked a real end-to-end fixture and were correctly routed around rather than fixed: (a) seqjob_allocator_init()/parjob_allocator_init() still read plugin->sched (not plugin->core) — a throwaway xce_scheduler_t wrapper local satisfies that registration precondition in tests, but &(wrapper.core), never &wrapper, must be what's actually passed to the core-safe functions under test; (b) make_sched_job()'s parse path doesn't thread submit.maxtasks/mintasks into the parsed primary resreq, so a test needing a specific seqjob-vs-parjob routing must set job->schedobj->u.primary->resreq->maxTasks/minTasks directly after construction. Whoever eventually fixes either of these two gaps should grep for this pattern across the test suite.
  • Task 2.3: while migrating a plugin's matching functions, writing the mandated TDD test for a code path that had genuinely never been exercised before (zero prior test call sites for match_job_host_limit_()/limit_prepare_allocation()) surfaced a real pre-existing bug (bug-1828) blocking that path entirely — the same defect class as bug-1786, fixed the same way, never applied to this sibling function. Fixing a blocking, in-boundary, minimally-scoped pre-existing bug discovered this way is acceptable within a task rather than escalating, but its buglog entry must state the REAL impact (here: host-scoped limits were never enforced in any mode, not merely a display gap) rather than the narrower symptom that surfaced it — the first-written impact description undersold this until review caught it.
  • Task 1.3: grep for ALL call sites of a function before migrating it, not just the ones named in the task brief — host_iter.c's sched_host_iter_next() and parjob_allocator.c/seqjob_allocator.c's my_alloc() all call the capacity-check functions directly, not only via sched_try_alloc_on_host_(). A private static helper with no other callers besides the functions being migrated should be migrated alongside them (leaving it stale forces an ugly cast, which this codebase's own design explicitly forbids in shared code).
  • Task 1.2: retyping a widely-called function's signature (e.g. plugin_job_election, plugin_prepare_allocation, plugin_commit_decision) breaks ALL of its call sites as hard compile errors in one step (this toolchain treats -Wincompatible-pointer-types as an error, not a warning) — there is no way to migrate a signature and keep the build green without touching every call site in the same commit, even when those call sites live in files nominally owned by a later task. The correct scope is the minimal fix (wrap the existing argument, e.g. sched → &(sched->core); touch nothing else in that function), not a rewrite of the caller. Also: design.md's Implementation Notes said plugin_prepare_allocation() has "one normal-mode call site" — it actually has two (resource.c's sched_try_alloc_on_host_() and schedobj.c's sched_ctx_set_primary()). Grep for ALL call sites before assuming a design doc's count is exhaustive.
  • Task 5.1: the pre-existing test_sim_contention_parity.c fixture could not be reused because it drives reconciliation only (its own white-box stubs hard-code the old always-PEND behavior), so proving live-path contention parity required two new fixtures spawning the real sched_job_picker()/sched_allocator() pair directly against bare normal-mode and sim-mode core state. Confirmed as a genuine, requirement-consistent scope boundary (not a silent narrowing): build_limit_vres_requests()/get_vres_value() (plugins/limit.c) are #if 0'd dead code with zero live callers — no limit, in either mode, currently gates/denies admission anywhere in this codebase, only tracks/displays usage. Any future task attempting real limit-based admission denial must first un-stub these two functions; that is out of boundary for this feature.
  • Task 5.2: this is the feature's concrete bug-1826 acceptance test, and it deliberately goes one level higher-fidelity than every prior task's fixtures — a real capnp-encoded .replay (genuine ReplayHeader/SnapshotEntry/Conf4Sched, real generated encode_Conf4Sched()) driven through the real, unmodified sched_sim_driver_run() entry point, not a bare-&(sim->core) shortcut. repl_info_limit()/repl_dispatch() (the literal functions behind the info limit CLI command) are both static and unreachable from a real-scheduler_srcs-linked test binary, so the test reads limit_get_configured_limits()/limit_get_usage_instances() directly — confirmed (by tracing repl_print_limit_usage()'s own body) to be the exact same accessors info limit itself calls, not a weaker substitute. RED-phase evidence for a task this late in a long dependency chain doesn't need a full feature revert: a targeted revert of just task 4.3's 4 files (the specific commit that closed the host-scoped-limit fan-out gap this test's host-scoped half exercises) reproduced the predicted failure exactly (global limit's usage still populated, host-scoped usage instance missing), then a restore-to-HEAD reproduced GREEN — this is sufficient, targeted regression-detection proof without needing to unwind 8+ interdependent commits. Also closed bug-1826 itself (the bug this whole feature exists to fix) — updated its buglog entry from "NOT FIXED" to "FULLY FIXED," citing this test.
  • Task 5.3: confirmed all three named suites (test_repl_equivalence.c, test_sim_equivalence.c, test_sim_contention_parity.c — the only files matching "REPL/batch decision-equivalence" and "normal-mode-recording-vs-simulation-replay decision-identity"; no fourth file exists despite the task text's plural "suites" wording, confirmed by grepping the whole spec/steering corpus and testsuite/meson.build for any other candidate name) pass, and pass for the right, non-vacuous reason, but NOT the reason a shallow read of their own header comments would suggest. All three exercise the REAL, genuinely-changed production code path in one dimension — sim_loop.c's sched_sim_driver_run() now spawns sched_job_picker()/sched_allocator() via a real two-thread jbuf producer/consumer hand-off instead of the old single fused sim_picker_thread_fn() thread, exactly the decision-timing/ordering risk requirements.md/design.md flagged for this migration — but all three still exercise a STUB, not the real allocation chain, in the other dimension: each file (being a white-box #include of sim_loop.c without linking sched_election.c/scheduler.c/resource.c) supplies its own link-satisfying sched_job_picker()/sched_allocator() definitions (added by task 4.1, unmodified since) that faithfully reproduce the OLD always-PEND contract. Verified via git show across this feature's commits that the only changes made to any of these three files were adding those stubs (task 4.1, commit 08df6493) and, for test_sim_contention_parity.c alone, one earlier mechanical call-site signature migration (task 1.3, commit 55173fca, e.g. sched_check_host_satisfy(sched, ...) → sched_check_host_satisfy(core, ...) — an argument-type change, not a value/logic change) — zero pre-existing assertions were touched in any of them, so there is no "changed expected value" to explain for this task's own AC. This means these three suites correctly prove "the new two-thread timing didn't disturb decision ordering," which is genuinely what they were built to prove and is not vacuous — but they do NOT prove "sim mode's real live allocator now matches normal mode under contention," which is a materially different, and separately covered, claim. That second claim already has its own dedicated, non-duplicative proof: sched_sim_live_allocation_test (task 4.1), sched_sim_alloc_limit_fanout_test (task 4.3), sched_sim_contention_resource_driver_parity_test / sched_sim_contention_limit_driver_parity_test (task 5.1), and sched_sim_e2e_limit_visibility_test (task 5.2) — all five link the real scheduler_srcs and drive the real sched_job_picker()/sched_allocator() pair, several explicitly under resource contention and active limits. So this task's own job for these three suites is exactly what its Observable text implies when read carefully ("all three suites pass, or every changed assertion has a written explanation") — reconciliation and documentation, not a new proof, since nothing changed and the missing proof already exists elsewhere. One genuine documentation defect found and fixed: all three files' header comments (and one inline comment in test_repl_equivalence.c) still asserted, in the present tense, that "the simulation driver structurally CANNOT emit a DISPATCH decision" and attributed this to sim_picker_thread_fn() — a function this same feature removed in task 3.1/4.1. That claim is now false of production sim mode (proved false by the five tests above) and was only ever true of these three files' own local stub choice — a reader skimming the header without noticing the file never links sched_election.c could easily conclude sim mode itself still can't dispatch. Corrected with a RECONCILIATION addendum in each of the three files' own header comments (plus the one inline comment) rather than rewriting the original historical rationale, which is still accurate as a record of why the fixture was scoped to PEND-only at the time it was written. No production code was touched; no assertion values changed; full suite re-run at 85/85 pass after the documentation fix, identical to the pre-task baseline.
  • Task 5.4: no genuine production corpus of real recorded sessions existed in this checkout to satisfy Scope item 9/AC 3.3-3.5 directly (Option 1). Searched exhaustively first: grepped .wolf/buglog.json/.wolf/cerebrum.md for bug-1611's "real 108-session replay" and a separate bug-1734 investigation's preserved /tmp/scheduler-replay-20260825-090251.replay — both were named-path references to prior sessions' /tmp files, neither still present on disk. A full-filesystem find for *.replay turned up 486 real, genuinely-produced-by-code files under /tmp/sched_adminreq_start_auto_*/ (from test_sched_admin_dispatch.c's own record start admin-request fixture, mkdtemp-named), but every single one was exactly 448 bytes -- an empty session header with zero jobs/decisions, worthless as a state-equivalence check. So this task took Option 2: generated genuine real recordings from actual live runs of the current codebase, per the task's own explicit allowance ("a session recorded from a real run of the current codebase counts as real even if the workload itself is synthetic"). Deployed a real single-node cluster (controller + scheduler + two xcecd_virtual workers) via deploy-ha.sh, reusing xce-test-multihost's proven deploy/start/confirm pattern, then used the REAL production CLI path end to end: xceadmin scheduler record start (confirmed exact mechanism: SchedAdminRequest{action=recordStart} over the real controller-to-scheduler admin channel, src/xceadmin/src/xceadmin/scheduler.py/cli.py -- xceadmin scheduler record start|stop|status, NOT a startup-only --record-dir flag) before submitting real jobs via plain jobsub (never xce_test's manual-allocation bypass), then xceadmin scheduler record stop to finalize a genuine capnp-encoded .replay file written by the real, unmodified recording.c/recording_session.c capture path -- not hand-authored, not test-harness-stubbed. Confirmed the verification mechanism's exact current names against source (unchanged from design.md): CLI flag --sim-verify (main.c), reporting function sim_log_mismatch() (sim_verify.c), which logs "%s decision verification MISMATCH ..." via ERROR() -- greppable verbatim. Captured TWO real sessions, deliberately different in shape: (1) 110 real rounds / 3 real jobs -- two 22-task jobs that PENDed for the entire session (root-caused via the replay's own dump and the scheduler's live DEBUG3 trace to a real, organically-configured GLOBAL limit, limit1, capped at total=3, reached through the real limit_job_election()/match_job_global_limit_() path with wanted=22.000000 shortfall=19.000000 -- genuine live limit enforcement, not a bug, and not something this feature's own migration could have caused since the same limit config applies identically to normal mode) plus one 1-task job that DISPATCHed and completed immediately; (2) 28 real rounds / 5 real 1-task jobs cycling through the same limit1 cap (15 real PEND decisions, 5 real DISPATCH decisions, genuine PEND-then-DISPATCH transitions as the limit's cumulative usage freed up) -- exercising Requirement 3.3's "live decision authoritative" rule under real, organically-timed multi-round contention, not a single crafted scenario. Ran xce_scheduler -s <file> --sim-verify (batch/non-interactive, ASAN_OPTIONS=detect_leaks=0 for a clean exit code -- the ASAN debug build's own known, already-documented "sim-mode threads are never joined" leak-at-exit artifact, design.md's own Non-Goals, unrelated to decision correctness) against both real files: zero mismatches reported in either session (grep -c MISMATCH on both stdout and the --logfile output: 0/0 both times; clean exit 0; no ASAN memory-safety error of any kind, only the expected leak-summary). This IS the disposition this task's Observable text requires: no divergence found, against 138 total real rounds and 8 real jobs spanning persistent-limit-blocked PEND, immediate DISPATCH, and PEND-then-DISPATCH-under-a-real-limit -- not merely "ran once, nothing broke." No production code change was needed or made. Full scheduler suite re-confirmed at 85/85 pass (see this task's own build-log timestamp) with zero code changes from this task, run as the mandated final step.