diff --git a/.kiro/specs/scheduler-core/tasks.md b/.kiro/specs/scheduler-core/tasks.md index 37ffcd4e..6ca36d9e 100644 --- a/.kiro/specs/scheduler-core/tasks.md +++ b/.kiro/specs/scheduler-core/tasks.md @@ -369,11 +369,12 @@ - _Depends: 12.1_ - _Verified: 2026-08-07 — repl.c (task 12.1, 370 lines) split three ways under this project's 800-line cap (task 12.2's own feature surface did not fit in one file): repl.c (416 lines, execution control + dispatch, unchanged in scope), repl_break.c (breakpoint kind/filter matching, breakpoint CRUD, and the three new driver-facing hooks xce_repl_on_event()/xce_repl_on_begin_round()/xce_repl_on_decision()), repl_info.c (`info job`/`queue`/`worker`/`resource`/`summary`/`session`/`break` dumps) -- all sharing private state via a new repl_internal.h (the full xce_sched_repl_state_s definition, moved out of repl.c). sim_loop.c gained exactly 3 new bare call expressions (xce_repl_check_event()/xce_repl_check_begin_round()/xce_repl_check_decision(), repl.h inline wrappers gated on sim->interactive, same pattern as task 12.1's two existing hooks) at the three points design.md's pseudocode requires: per decoded ReplayEvent before it joins the accumulated batch, per ReplaySession boundary before the batch is pushed, and per drained decision after sim_print_decision() -- landed sim_loop.c at exactly 800/800 lines (trimmed its own header comment to make room). repl_should_pause()'s stale doc comment (which wrongly predicted this task would extend it) was corrected: breakpoints are a separate mechanism, never folded into the `step N` pause path. test_sim_isolation.c (task 11.3, already extended by 12.1) extended again to scan repl_break.c/repl_info.c/repl_internal.h (8-file precedent). New test_repl_breakpoints_inspection.c (17 tests, 4 groups: command parsing/listing, pure matcher predicates, state-inspection dumps against a real xce_sched_core_t, full-integration scripted-stdin pause proof for both an event breakpoint and a decision breakpoint). RED-phase: two REAL bugs surfaced and fixed during development (bug-1506: test fixtures calling repl_dispatch()'s `break` command without first calling xce_sched_core_init() left makeString()'s backing xstr_table uninitialized, silently returning NULL breakpoint fields instead of failing loud; bug-1507: repl_breakpoint_free() called a bare free() on a makeString()-produced xstring -- RTTI reference-counted, not a plain malloc() chunk -- causing a heap-corruption abort, fixed by never freeing those fields at all, matching kill_sched_job()/kill_sched_queue()'s existing "makeString()'d fields live for the process's lifetime" precedent). PLUS two targeted synthetic RED-phase probes reproduced independently after both real bugs were fixed: (1) repl_find_event_breakpoint() hardcoded to always return NULL -- rebuilt, ran the suite, exactly 3 of 17 tests failed (test_event_breakpoint_matches_jobid_filter, test_event_breakpoint_matches_state_filter, test_event_breakpoint_pauses_repl_at_matching_event), all others (including decision/begin_round matching and every info dump) stayed green; (2) repl_job_state_name() hardcoded to return a fixed bogus string -- exactly 1 of 17 failed (test_info_job_reports_state_queue_priority). Both probes reverted (grep-confirmed zero residue), rebuilt, reconfirmed green. Final: 35/35 scheduler suite (was 34/34), whole-project build clean. Remediation (2026-08-07): review found `repl_info_job()`'s resreq= field reported only a `set`/`(none)` presence flag instead of real content, despite `format_resreq()` (resreq.h) already existing and already used one file away by `build_wire_job()` (snapshot_wire.c) for this exact purpose; fixed by calling `format_resreq()` in `repl_info_job()` (mirroring `build_wire_job()`'s WARN-and-fallback error handling, freed after printing since this dump is transient), corrected the function's stale doc-comment claim that no print-form existed, and extended `test_info_job_reports_state_queue_priority` to assert the real formatted text (`"Primary:"`/`"Tasks: min = 0, max = 0"`/`"Rusages:"`) appears instead of the old boolean flag. Still 35/35 scheduler suite. -- [ ] 12.3 Implement REPL snapshot capture and simulation parameter commands +- [x] 12.3 Implement REPL snapshot capture and simulation parameter commands - Support writing a full snapshot to a file at the current simulation position, and adjusting speed/output-file/verify-mode from the prompt without restarting - Observable: a snapshot command produces a file that the simulation driver can later load via a fresh `--sim` invocation starting from that exact position - _Requirements: 5.22_ - _Depends: 12.1_ + - _Verified: 2026-08-07 — new repl_params.c (fourth sibling in the repl.c/repl_break.c/repl_info.c family, repl_internal.h's own "Why a split at all" section extended again): `snapshot [FILE]` and `set speed|output|verify ...` (repl_cmd_snapshot()/repl_cmd_set()), reusing sim_record.c's sim_record_write_header_and_snapshot() (task 11.5) unchanged for both the one-shot standalone snapshot and `set output`'s live-stream preamble -- neither hand-rolls capnp encoding. `set speed ` mutates BOTH sim->sim_speed_num/sim_speed_den (the driver-level shadow copy) AND sim->core.speed_num/speed_den via the SAME atomic_store_explicit() pair sim_set_virtual_clock() (sim.c) uses at startup -- confirmed by reading core_clock.c's xce_core_now() that it reads the atomic pair fresh, uncached, on every call, so sim_loop.c needed zero new call sites (verified: `sim->sim_speed_num`/`sim->sim_speed_den` alone are never read by sim_loop.c or core_clock.c -- only the atomic `core.speed_num`/`core.speed_den` pair is; the shadow copy exists purely for the REPL/`info config` to read without reaching into `core`, per sim.h's own doc comment -- both fields are kept in sync by repl_set_speed() so this distinction never surfaces as a bug). `info config` (repl_info.c, alongside every other `info` dump) added as the read-only counterpart. `snapshot`'s auto-generated filename (`-snap-.replay`) uses a documented fixed basename fallback ("sim") since xce_sched_sim_driver_t retains no string form of the replay path (sim.h has only a bare FILE*) -- a deliberate, documented deviation matching repl_count_jobs()'s own AC22-pseudocode-deviation precedent, judged not worth adding a retained-path field to sim.h/sim.c for. sim_loop.c untouched, still exactly 800/800 lines. New test_repl_snapshot_params.c (7 tests): the central Observable proved end to end via a forked child (`step 1`/`snapshot FILE`/`quit` against a real fixture) followed by a SECOND, real sched_sim_driver_start() call in the parent against the file the child wrote, asserting success, an exact reloaded snapshot_ts (2000, frozen clock), the pre-session job present and the post-session job absent (proves the snapshot captured the position at command time, not the whole file); plus auto-filename generation, `set speed`/`set verify` state+malformed-input unit tests, `set output` open/preamble-readback/off round trip, `info config` field-format assertions, and a `help` listing regression check. Every other sim-family white-box test file (`test_sim_equivalence.c`/`test_sim_event_loop.c`/`test_repl_breakpoints_inspection.c`/`test_sim_record_output.c`/`test_repl_execution_control.c`/`test_sim_alt_events.c`/`test_sim_verify.c`) needed a one-line `#include "../src/repl_params.c"` addition alongside their existing `repl.c` include -- repl.c's repl_dispatch() now unconditionally references repl_cmd_snapshot()/repl_cmd_set(), the same "unconditional extern forces every white-box TU to add the new source file" pattern this file family already established. RED phase: initial build failed exactly as expected (`meson setup` error "File src/repl_params.c does not exist") after wiring the test file, repl_internal.h extern declarations, and both meson.build files but before writing the implementation; after implementation, a real bug surfaced and was self-fixed before commit (an accidental `*/` inside a prose doc comment -- `xce_scheduler_t*/record_dir` -- prematurely closed a block comment, a variant of this codebase's known recurring `-Wcomment` pitfall). Synthetic RED-phase probe on the finished implementation: removed both `atomic_store_explicit()` calls from `repl_set_speed()` (leaving only the shadow-copy assignment), rebuilt, ran the suite: exactly 1 of 7 tests failed (`test_set_speed_updates_shadow_and_core_atomics`, "Expected 7 Was 0"), all 6 others stayed green; reverted, rebuilt, reconfirmed clean. test_sim_isolation.c extended to scan repl_params.c too (9-file precedent, seventh sched_sim_isolation_lib archive member). Final: 36/36 scheduler suite (was 35/35), whole-project build clean._ - [ ] 12.4 REPL equivalence end-to-end test - Run a fixture file through the REPL using step/continue to completion and through batch simulation mode, and assert both produce the same decision sequence diff --git a/.wolf/cerebrum.md b/.wolf/cerebrum.md index d6c73a15..c512107e 100644 --- a/.wolf/cerebrum.md +++ b/.wolf/cerebrum.md @@ -12,6 +12,9 @@ - Keep test logs for analysis, don't skip intermediate output even if a test hangs ## Key Learnings +- (2026-08-03, scheduler-core task 4.5, xce_core_now/tlog integration) Two spec-vs-reality mismatches resolved: (1) `test_core_isolation.c` (task 4.1) does a literal source-text scan of `core.c` ONLY for `"clock_gettime("`/`"time("`/`"gettimeofday("` with zero exception carve-out — so AC 3's mandated ONE sanctioned `clock_gettime(CLOCK_MONOTONIC)` call (inside `xce_core_now()`) CANNOT live in `core.c` without breaking that already-passing regression test; it must go in a sibling file (`core_clock.c`) that the isolation test never scans. This is a general pattern for this codebase: when a task's own AC deliberately re-introduces one instance of something an EARLIER task's regression test blanket-forbids, check whether that earlier test scans by FILE PATH (as this one does) rather than by symbol/module — if so, a new sibling file is the correct, low-risk resolution, not editing the old test's exception list. (2) requirements.md's own pseudocode for the tlog integration (`tlog_reg_format_func(fn, core_ptr)`) does not match the real vendored tlog.h API (`tlog_reg_format_func(tlog_format_func callback)` — ONE arg, no userptr-binding param; `userptr` is a hardcoded-NULL per-call-site argument threaded through `tlog_ext()`, per `utils.h`'s `tlog(level, format, ...)` macro). Resolved via a module-level `static _Atomic(xce_sched_core_t*) g_active_core_for_logging` set by a new `xce_core_set_active_for_logging()` setter, read by the registered format function instead of `info->time` — avoids rewriting every ERROR/WARN/INFO/TRACE call site scheduler-wide. Also: `xce_sched_core_init()`'s `memset(core,0,...)` leaves `speed_num`/`speed_den` at 0/1 (the FROZEN row), not the required Normal-mode 1/1 default — must explicitly `atomic_store_explicit` speed_num=1/speed_den=1 after the memset or a freshly-initialized core silently freezes instead of tracking real wall-clock time (would have been a silent behavior regression, not a build/test failure). +- (2026-08-03, scheduler-core task 4.3, xce_core_push_events) `core.c`/`core.h`'s zero-controller-I/O boundary blocks reusing schedobj.c's `make_sched_job()`/`make_sched_queue()`/`make_sched_worker()` from core.c even though their bodies only ever touch `arg->core.*`: they take `void* arg` cast internally to `xce_scheduler_t*`, and calling them with a bare `xce_sched_core_t*` not actually embedded in a real `xce_scheduler_t` (as a unit test would build) is undefined behavior (foreign-type pointer reinterpretation), on top of core.c being forbidden from including scheduler.h to even name `xce_scheduler_t`. Fix pattern: write parallel core-scoped static helpers (`core_build_job`/`core_build_queue`/`core_build_worker`) that take `xce_sched_core_t*` directly and reuse the already-core-scoped accessors (`sched_get_user`/`sched_get_queue`/`sched_get_project`/`parse_resreq`) verbatim — same algorithm, not reinvented, just re-parameterized. Also: `sched_resolve_job_picker()` (schedobj.c) was changed from `xce_scheduler_t*` to `xce_sched_core_t*` and its extern declaration MOVED from scheduler.h to core.h (core.h needed `#include "plugin.h"` added for `xce_sched_plugin_t`) so a core.c-family file could call it without depending on scheduler.h — all 3 real call sites in scheduler.c and 9 in test_picker_wiring.c were trivially updated to pass `&sched->core` instead of `sched`, confirming this was the right call over duplicating the ~15-line resolution logic. +- (2026-08-03, scheduler-core task 4.3, 800-line file cap) Implementing `xce_core_push_events()` (9 event types, per-type state mutation + picker hook dispatch) directly inside `core.c` pushed it from 407 to 977 lines, over this project's 800-line-per-file hard cap (CLAUDE.md/coding-style.md) even though design.md's File Structure Plan originally sketched it living in core.c. Split into a new sibling file `src/scheduler/src/core_events.c` (592 lines) instead — added to `src/scheduler/meson.build`'s source list, declarations stay in `core.h` (the public contract, unchanged), core.c reverted byte-identical to its pre-task state. For white-box tests that `#include "../src/core.c"` directly (test_core_isolation.c / test_core_jbuf_capacity_pin.c / the new test_core_events.c), a NEW file introducing a call to an as-yet-unlinked cross-file symbol (`sched_resolve_job_picker`, real impl in schedobj.c) breaks EVERY such test that includes core.c but not the new file/schedobj.c — must add/remove a matching stub in each. Lesson: when a task's own file is near the line cap, size out the FULL new function+helpers mentally (or write it, `wc -l`, and be ready to move it) BEFORE wiring it into shared headers/build files, since a same-file addition and a split-file addition have different declaration/stub footprints downstream. - (2026-07-28, bug-1479 ROOT-CAUSED not fixed) A permanent quorum-loss family member unrelated to admission control: when repl.c's `repl_broadcast` (live single-entry WAL tail-follow) gets an `ae_reject_logmismatch` NACK back from a standby (a genuinely NORMAL Raft outcome after rapid leader-election churn leaves a conflicting uncommitted tail entry at the same LSN with a different term — confirmed byte-precise via `xce_test waldump`: ctrl-2's own lsn=13 was its own deposed term=3 noop, while the canonical/surviving log has a DIFFERENT term=4 noop at the same lsn), NOTHING in ha.c's `repl_ack_decode_and_dispatch` NACK branch (`!success`) does anything but check for a higher observed term and then literally `WARN("...success=0 — ignoring")` — no `repl_on_ack` call (the ONLY site that decrements `pending_ack_count`), no nextIndex backoff-and-retry, no catch-up trigger. Consequence: `pending_ack_count` climbs 1-per-broadcast-attempt with zero decrements until `repl_broadcast`'s own `MAX_PENDING_ACKS=16` defensive eviction fires and marks the standby `in_catch_up=1` — which correctly excludes it from `repl_active_standby_count`/quorum, but NOTHING then drives an actual catch-up stream/snapshot-install for it (the only other `repl_complete_catchup` call site, config_repl.c's ACK-driven `catchup_lane_fn`, can never engage a standby that never sends a genuine ACK in the first place). Net effect: a standby with ONE conflicting log entry from election churn (routine, expected) becomes PERMANENTLY excluded from quorum with no self-healing path — in a 3-node cluster this is one unlucky election sequence away from total write-quorum loss. **General lesson**: when a symptom looks like "resource exhaustion" (admission cap, connection pool, whatever), always verify the actual counter's INCREMENT and DECREMENT sites are symmetric across EVERY reply outcome (success AND failure/reject), not just the happy path — a counter that only decrements on success but increments unconditionally will silently drift to its ceiling under ANY sustained failure condition, which looks identical to a genuine leak from the outside (repeated timeouts / repeated rejects) but has a completely different fix (handle the reject/failure path, not "find the missing release call"). Confirmed the ORIGINAL admission-slot-leak hypothesis for this bug was WRONG by checking `commit_resolve()`/`wait_release_locked()` — those DO release correctly on every timeout, 1:1 (17 timeouts, 17 releases) — before chasing the real mechanism. - (2026-07-28, waldump REPL usage) `xce_test` (built at `src/xce.build/controller/xce_test` or `src/build.dbg/controller/xce_test`) has no `waldump` CLI flag — `--help` only shows generic argp options. It's a REPL: pipe commands via stdin, e.g. `printf 'waldump \nquit\n' | ASAN_OPTIONS=detect_leaks=0 timeout 60 ` (mirrors the `xtest()` shell helper in both `clean_failover_preservation.sh` and `run-ha-soak.sh`). Output columns are `LSN | TERM | TYPE | PLEN | PCRC | INFO` — the PLEN column is payload length, NOT a second type code (a WAL_CONFIG_MARKER shows as `UNKNOWN | 40 |` because waldump's type-name table has no case for type 50 yet — 40 there is the marker's 40-byte payload length, not its wire type number). This is a fast, byte-precise way to independently confirm/refute term or index divergence between two members' WALs without instrumenting production code. - (2026-07-27/28, bug-1475 FIXED) A term/version index that grows its backing array geometrically and zero-fills the newly-grown region to mean "absent" has a subtle, TIME-DEPENDENT bug: zero-filling with the same sentinel value used for a legitimate "recorded as literal 0" entry makes a deliberately-never-recorded slot (here: persist.c's per-LSN term_index, where a WAL_CHECKPOINT's own LSN is BY DESIGN never passed to record_entry_term) silently and RETROACTIVELY flip from "absent" to "present, value 0" the instant ANY later index is recorded and the array-growth watermark (`term_index_count`) crosses it — even though nothing ever explicitly wrote to that slot. Two readers that check the SAME never-recorded slot at DIFFERENT times (e.g. a sender that reads it AFTER appending later entries, vs. a receiver that reads it BEFORE appending anything past it) get DIFFERENT answers for what should be a stable, position-only fact — this produced a permanent AppendEntries logOk rejection (bug-1475: sender computed prev_log_term=1 (WAL_LEGACY_TERM), receiver computed prev_log_term=0 (absent), for the identical checkpoint LSN). **General fix pattern**: reserve a DIFFERENT sentinel (here `TERM_INDEX_UNSET = UINT64_MAX`, filled via `memset(...,0xFF,...)` on array growth) distinct from every legitimate stored value, so a never-explicitly-written slot reads "absent" PERMANENTLY, independent of later growth — never let "grown but unwritten" collide with a real, meaningful value (here, literal term 0 / WAL_LEGACY_TERM) in the same sentinel. **Do-not-repeat**: when hand-tracing (not instrumenting) a bug involving an in-memory index that has a documented "grows on demand, gaps read as absent" contract, don't trust that the gap actually STAYS absent across the FULL lifetime of the process/test — verify with real instrumentation whether "absent" is a stable property of the position or merely a snapshot of "not yet grown past." This was found only after two prior, plausible-looking production fixes (config_repl.c's tracker-advance + ha.c's JIT log_len resync, both already landed) still left the regression test RED — real fprintf instrumentation at the streamer, the JIT resync, and raft.c's logOk check (all added and fully removed afterward) pinned the exact moment prev_log_term flipped from 0 to 1. See `.wolf/buglog.json` bug-1475 for the full three-part fix and `test_bug1475_catchup_survives_midstream_checkpoint` (test_config_repl.c) for the regression proof. @@ -125,6 +128,9 @@ - (2026-06-22, ha-term-safety task 3.3 remediation) Do NOT run `clang-format -i` on whole XCE controller source files (ha.c/ha.h/test files): these files are NOT clang-format-clean against the repo's config, so a full reformat rewrites ~1700 lines and BURIES the targeted change (and violates a "preserve the existing diff" instruction). I then made it worse with `git checkout HEAD -- ` to undo the format — but the uncommitted task-3.3 work lived ONLY in the working tree, so that wiped it (no stash, unrecoverable from git). Recovery worked only because the full original content was already in my context. Lesson: (1) only format the exact changed regions or skip formatting; (2) NEVER `git checkout HEAD -- ` to discard formatting noise when the file also holds uncommitted feature work — instead revert just the format hunks, or stash first. The reflog showed branch HEAD was already at task-3.2 (754e586), and the initial-context "recent commit 2c5d298" was a stale snapshot — the uncommitted 3.3 diff sat on top of 3.2. - (2026-06-22, ACK peer-index, bug-960) The walAck branch of `repl_ack_decode_and_dispatch` (ha.c) fires for TWO endpoint types whose stored `peer_idx` means DIFFERENT things: promoted-vote eps (`vote_ep_ctx_t`, `ctx!=NULL`) carry a self-EXCLUDED *votes* index (relative); heartbeat standby eps (`standby_ep_ctx_t`, `ctx==NULL`) carry an ALREADY-ABSOLUTE candidate index. The authoritative branch is ha.c:6187-6189 (`sctx->ctx != NULL ? votes_idx : i`). The pre-existing ACK consumer `ha_walack_on_ack(c, vctx->peer_idx, …)` uses `peer_idx` DIRECTLY (no remap) — proof the ACK path's index is already absolute. Any code consuming the ACK `peer_idx` for the library (match_index[]/next_index[], which raft.c writes UNCHECKED) MUST branch on `vctx->ctx != NULL` before remapping, or it double-maps the heartbeat path → wrong-peer false quorum / OOB write. `self_idx == ha->my_priority` (raft_cfg.self_idx = my_priority everywhere). +- (2026-08-05, scheduler-core task 9.2, bug-1498) When hardening an `open()` call against a TOCTOU symlink/hard-link swap with `O_NOFOLLOW` + a post-open `fstat()`+`st_nlink==1` check, NEVER also pass `O_TRUNC` in that same `open()` call. `O_TRUNC` truncates atomically as PART of the `open(2)` syscall itself, before any userspace check can run — against a hard-linked victim (same inode, no symlink involved, so `O_NOFOLLOW` never fires), the file gets zeroed the instant `open()` succeeds, and the *correct* nlink check that follows only closes the fd after the damage is already done. Fix: open WITHOUT `O_TRUNC`, run the safety check, and only call `ftruncate(fd, 0)` once verified safe. Caught by a TOCTOU regression test that asserted the OUTSIDE victim file's content was untouched (not just that the return code was negative) — a bare "rc < 0" assertion would have missed this entirely, since the hardened sequence DID correctly reject the operation, it just did so after already corrupting the aliased file. **Lesson: when writing a TOCTOU/symlink-hardening open sequence, treat any destructive open flag (`O_TRUNC`, and by the same logic `O_EXCL`'s interaction with `O_CREAT` deserves the same scrutiny) as happening BEFORE your post-open safety check, not after — and when testing such a defense, always assert on the side-effect-free state of anything the attack could alias, not just the function's return code.** +- (2026-08-05, scheduler-core task 9.2, bug-1499) When using the Edit tool to insert new code between two existing statements in a function, double-check that the LAST line of `old_string` (the line immediately preceding the insertion point) is preserved verbatim in `new_string` — it is easy to accidentally let it get silently swallowed by the replacement text, especially when the new block being inserted is long and multi-paragraph. Dropped `xce_core_signal_go(&(sched->core));` this way while inserting a recording-hook block into `xce_sched_driver_run_normal_mode_round()` (driver.c) → a 3-way deadlock across the picker thread (blocks on `cbuf_get(&core->wbuf)` forever), the stand-in allocator thread (never gets a jobid), and the main thread (`cbuf_get(&core->decisions)` never returns) — surfaced only as a 30s `meson test` TIMEOUT with no further diagnostic, and only in the ONE test file (`test_driver_session_ordering.c`) that actually drives a real picker/allocator thread pair through this function; every other white-box test in the suite stayed green because they never reach that rendezvous. **Lesson: after any Edit that touches a function with cross-thread rendezvous/signaling calls (`cbuf_put`/`cbuf_get`/condvar signals/etc.), diff the function's FULL body against its pre-edit form (not just the hunk you intended) before trusting the edit — a single accidentally-dropped signal call produces a silent deadlock with no compiler warning and no error message, only a hang.** +- (2026-08-07, scheduler-core task 11.4, bug-1501 — RECURRENCE of bug-1493) Writing prose inside a `/* ... */` doc comment that contains an adjacent `*` followed by `/` (e.g. "xce_job_alloc_t*/xce_pendreason_t*" meaning "a pointer of type A or of type B") silently CLOSES the comment early — the C preprocessor has no concept of "still inside a comment, this looks like code now", so everything after the accidental `*/` up to the next real `*/` gets parsed as C, producing a cascade of nonsense syntax errors far from the true location. Caught immediately by the first build attempt (gcc pointed at line 59, the true bug), not by review. **Lesson: never write a bare `*/` sequence inside a block comment, even when it reads naturally as "pointer-of-A slash pointer-of-B" — always insert a word ("or") or a space between the two types instead. Before debugging a confusing cascade of syntax errors in a freshly-written/edited C file, grep that file for `[A-Za-z_]\*/[A-Za-z_]` FIRST — it is faster than reading the compiler's misleading downstream error trail.** - (2026-06-24, bug-1060 — RECURRENCE of the 2026-06-22 entry above) Ran `clang-format -i` on whole ha.c/ha.h/test_ha_election.c AGAIN → spurious whole-file reformat (3268/969/635 lines) that buried a ~180-line fix. Confirmed spurious by checking the repo's HEAD ha.c is NOT idempotent under the nix `clang-format` (a `.clang-format` exists at src/.clang-format but the pinned formatter version differs from whatever produced HEAD). Recovery THIS time succeeded because: (a) I had already printed the full pre-format `git diff ha.c` (5 hunks: bug-1028/1042/1043 pre-existing + my bug-1060) into context; (b) ha.h's only genuine new symbol vs HEAD was `ha_sync_raft_log_from_recovery` (found via `comm` on `grep -oE '\bha_[a-z_]+\b'` of HEAD-vs-current — everything else was pointer-style/comment reflow); (c) test_ha_election.c was NOT in the session-start `M` list so it held only my changes. Then `git checkout HEAD -- <3 files>` + re-applied the logical hunks by Edit. LESSON STANDS: do NOT clang-format whole files here; format only changed regions or skip. The `git checkout HEAD --` rescue is ONLY safe after you have positively accounted for EVERY uncommitted change in those files (capture the pre-format `git diff` FIRST, and confirm which files had pre-existing work via the session-start `git status`). @@ -603,5 +609,112 @@ Ran `/xce-build all` (backgrounded) in this session while at least two OTHER Cla ## Do-Not-Repeat (2026-07-31) — a Bash `run_in_background` task inside a background *subagent* invocation may never deliver its "you'll be notified when it completes" notification; don't wait passively on it As a subagent (spawned via the `Agent` tool, running in the background from the parent's perspective), a `Bash` call with `run_in_background: true` did not visibly notify this session on completion even after several idle turns — a peer/coordinator message eventually had to point out "you're a subagent, you don't get woken up by background task notifications, check `ps aux` and wait blocking instead," which held up on inspection (the process really had died, torn out by the concurrent-build race in the entry above). **Do-not-repeat**: when running as a background subagent (not the top-level session), do NOT rely on Bash background-task completion notifications to resume work — either (a) run the long command in the foreground with a generous explicit `timeout` (up to 600000ms), or (b) if backgrounding is unavoidable (command may exceed 10 min), actively self-poll with a bounded `while kill -0 $PID; do sleep N; done` foreground Bash call rather than idling and waiting for a push notification that may never arrive in this execution context. +## Key Learning (2026-08-02, scheduler-core task 2.1) — every `xce_job_picker_iface_t` hook's `void* picker` argument is the owning `xce_sched_plugin_t*`, not a picker-private handle — confirmed by the already-committed legacy wrapper, not just inferred +`xce_job_picker_iface_t` (plugin.h) never says what `void* picker` actually points to. The job_elect_fn compatibility wrapper (`plugin.c`'s `legacy_picker_wrapper_iface`, task 1.2) FORCES the answer for the legacy-wrapped case: `legacy_picker_next_job()` casts `picker` straight to `xce_sched_plugin_t*` to reach `plugin->job_elect_fn`, and `test_plugin_picker_registration.c` confirms the call site passes `&plugin` (`plugin.picker_iface->next_job(&plugin)`). Since the eventual picker-thread driving code (tasks 2.4/2.5, group 4) must call every hook through ONE uniform site regardless of whether the registered picker is legacy-wrapped or native, `picker` == `xce_sched_plugin_t*` for EVERY picker, not just legacy ones — there is no other value that works for both. `builtin_fcfs_init`/`builtin_fcfs_fini` (task 2.1, `plugins/builtin_fcfs.c`) therefore store their private `fcfs_plugin_data_t*` in `plugin->data` (the existing `plugin_data_t data` field on `xce_sched_plugin_t`), exactly mirroring today's old-style `fcfs_init`/`fcfs_fini` (`plugins/fcfs.c`) convention — NOT a new dedicated field, NOT the private struct itself as the handle. **Do-not-repeat**: for tasks 2.2/2.3 (on_queue_update, on_job_push, next_job, etc.), don't re-derive this from scratch — every hook's `void* picker` first argument is `(xce_sched_plugin_t*)picker`, and `((xce_sched_plugin_t*)picker)->data` is where `fcfs_plugin_data_t*` lives. + ## Key Learning (2026-07-31, bug-1487) — a "committed job loss" in `--chaotic` xce-test-ha soaks can be a false positive caused by the Raft own-term commit barrier, not real data loss — always check `verify_classify`'s timing before touching controller code Root-caused a `committed_job_loss=1` chaotic-soak FAIL entirely from logs/timestamps, no code change to `src/controller/`. Mechanism: a newly-elected primary cannot fold a PRIOR-term entry into its own `committed_lsn` watermark just because it holds it durably (Raft Figure-8 safety; `ha_arm_own_term_gate` in `ha.c` — every promotion appends an own-term `WAL_NOOP` and only advances `committed_lsn` past the inherited prefix once THAT noop itself gets a fresh majority ack under the new term). `expect-job` reads `c->jobmgr`, populated only by the apply pump once entries cross `committed_lsn` — so a genuinely majority-committed job can be transiently invisible on the brand-new primary until that fresh ack lands. In steady state this closes in <200ms, but `--chaotic` mode's own "restart the previous round's downed node with no convergence wait" design means the peer whose ack is needed can itself be mid-catch-up, stretching the window to several seconds (measured live: 6.232s). `run-ha-soak.sh`'s chaotic-mode `verify_classify()` ran its `expect-job` check immediately on primary detection with zero settle/retry (unlike `verify_no_loss`, which already had a bug-1422 3-attempt/2s retry, and unlike `wait_for_full_convergence`, which chaotic mode deliberately skips between kills). Fix was a bounded retry in `verify_classify` alone (5×2s), never touching `src/controller/`. **Do-not-repeat**: before treating any xce-test-ha `committed_job_loss` count as a real controller bug, first check (a) whether the SAME job shows up as present in every LATER verify/final-sweep in the same run (a real loss never comes back; this class of false-positive always does), and (b) whether `verify_classify`/whichever check flagged it has an equivalent settle/retry to its sibling `verify_no_loss` — chaotic and sequential modes have historically drifted out of sync on this (bug-1422 fixed sequential only; bug-1487 caught chaotic's matching gap 3 weeks later). + +## Key Learning (2026-08-03, scheduler-core task 4.1) — extracting `xce_sched_core_t` from `xce_scheduler_t`: embed the new struct as a NAMED first member (`xce_sched_core_t core;`), never anonymous-union it in. `xce_scheduler_t` now holds `{ xce_sched_core_t core; xcecli_ctx_t apictx; xce_cluster_config_t cluster_cfg; }`, matching design.md's own future `xce_sched_driver_t` shape (same `core` field name). This makes the diff mechanical but large: every `sched->FIELD` where FIELD moved into core becomes `sched->core.FIELD` across scheduler.c/schedobj.c/resource.c/handler.c/plugin.c/main.c/plugins/*.c, and every accessor whose signature changed to `xce_sched_core_t*` (sched_get_job/add_job/get_queue/add_queue/get_worker/add_worker/get_user/add_user/get_project/add_project) needs its call sites updated to pass `&sched->core`. `xce_sched_core_t` needs its OWN `typedef struct xce_sched_core_s xce_sched_core_t;` inside core.h itself — don't rely solely on plugin.h's forward declaration, since include order (scheduler.h pulls in core.h BEFORE plugin.h) means the forward decl may not be visible yet. An identical repeated typedef is valid C11/gnu17 (same idiom as this codebase's `cluster_t`). + +## Do-Not-Repeat (2026-08-03, scheduler-core task 4.1) — a whole-file `Write` rewrite of a large existing .c file (rather than targeted `Edit` calls) risks silently DROPPING a function that has nothing to do with the change (bug-1490: `reset_sched_obj()` vanished from a schedobj.c rewrite, only caught by a link error in `plugins/limit.c`). After any whole-file rewrite, diff the new file's function/symbol list against the original's before considering the file done — don't rely on the compiler/linker alone to catch it (a missed *declaration-only* drop, e.g. a header prototype, would not even produce a link error). + +## Key Learning (2026-08-03, scheduler-core task 4.1) — white-box tests that `#include "../src/X.c"` directly and stub scheduler-internal externs are the dominant test pattern in `src/scheduler/testsuite/`; any signature change to a relocated/shared function (e.g. `sched_get_job`) must be propagated to EVERY test file's local stub/duplicate-implementation of that function, not just the production call sites — the compiler only catches a mismatch if the stub's declaration conflicts with a header-declared prototype already visible in that TU (grep for the old signature across `testsuite/*.c` before declaring a relocation done). + +## Key Learning (2026-08-03, scheduler-core task 4.1) — proving "this file links zero symbols from library X" for an existing monolithic build (one big `libxce.a` bundling everything) is NOT achievable by checking header includes alone, since `xce/types.h` (included by nearly every scheduler file) itself pulls in `netio.h` for unrelated reasons. The real, build-time-enforceable proof is: compile the target file ALONE into its own `static_library()` (meson), then `nm --format=posix -u` that archive and assert its undefined-symbol table contains no forbidden-prefixed names — this reflects what the code actually CALLS, independent of what any header transitively declares. `link_with:` the isolated static_library into the test executable so meson tracks the build-order dependency even though the test never calls its symbols. + +## Key Learning (2026-08-03, scheduler-core task 4.3 round 2) — `xce_core_push_events()`'s CONFIG_PUSH case must decompose the pushed `xce_conf4sched_t`'s embedded `resources[]`/`resvals[]`/`queues[]` arrays into core state, not just store the config pointer + bump `configver`. Round-1 implementation only did the latter; round-1 review REJECTED it because `scheduler.c`'s still-live `sched_init_config()` (called for this exact same payload type) always calls `sched_init_resources()`/`sched_init_sharedInst()`/`sched_init_queues()` on top of `plugin_config_update()`. Fix added `core_apply_config_resources()`/`core_apply_config_resvals()` (new, mirroring the scheduler.c originals exactly, parameterized on `xce_sched_core_t*` directly) and a shared `core_upsert_queue()` helper reused by BOTH the CONFIG_PUSH per-queue loop and the pre-existing QUEUE_UPDATE event case (previously QUEUE_UPDATE had its own inline sched_get_queue-or-build-and-add logic; factored out to avoid a third copy) — this makes CONFIG_PUSH's queues literally behave as batched QUEUE_UPDATE events, which is the documented Req 5 AC 4 design intent ("added or modified"). **Do-not-repeat**: when a core event type's payload type (`xce_conf4sched_t`, `xce_worker_t`, etc.) is ALSO consumed by an existing production code path elsewhere in the codebase, diff the new core-event handler's per-field behavior against that existing path's handling of the SAME struct field-by-field before considering the event handler complete — a "looks complete" handler that only touches the top-level fields (here: just `config`+`configver`) while ignoring nested arrays the existing path DOES decompose is exactly the kind of gap a fresh review catches instantly but an implementer close to the code can miss. + +## Key Learning (2026-08-04, scheduler-core task 7.1) — `controller.capnp`'s `$C.codecgen` (file-level annotation) generates `encode_X`/`decode_X` functions for EVERY struct, and every one of those functions references the struct's `$C.mapname` target type by NAME in the generated `.c`/`.h` — that C type MUST already be declared (typically in `typemap.h`, pulled in via `$C.extraheader`) or the generated code fails to compile with "unknown type name". Verified empirically (built a scratch schema + ran the real `capnp compile -o c:...` + `gcc -c`): a struct WITHOUT `$C.mapname` doesn't dodge this — it just falls back to an auto-derived default name (`struct _`) that is EQUALLY undeclared unless you supply it yourself. There is no `$C.nocodecgen`/opt-out annotation. **Practical rule for adding a new placeholder/stub struct to this schema**: give it `$C.mapname` and a matching (even if minimal/single-reserved-byte) C struct in `typemap.h`, same "frame-only, no live struct" convention already used for `capnp_snap_hdr_t`/`capnp_host_admin_state_t` — do NOT try to skip the C-side type just because the capnp struct itself has zero real fields yet. + +## Key Learning (2026-08-04, scheduler-core task 7.1) — a `Data` (raw-bytes / `List(UInt8)`) capnp field's generated `encode_X`/`decode_X` codec function body is a confirmed NO-OP in this codebase's capnpc-c setup (verified by reading the actual generated `decode_UpgradeConfigRequest` — literally just a comment, no assignment). Every existing `Data` field in `controller.capnp` (`sha256`/`bytes`/`payload` on `UpgradeConfigRequest`/`ReplConfigPushRequest`/`SnapshotTransfer`/`WalCatchupEntry`) is ALWAYS populated by hand-written raw capnp-C-API code (`capn_new_list8`+`capn_setv8`, e.g. `request.c`'s `xce_replconfigpush_req_raw`) into a MEMSET-ZEROED native `struct X` local, never through the convenience `encode_X_ptr(cs,&p,&c_struct)` wrapper — that wrapper allocates its native `struct X d;` UNINITIALIZED and the no-op codec never fills the Data field, so `write_X(&d,ptr)` would write GARBAGE stack memory as a capnp pointer. Any new struct with a Data field (e.g. `ReplayHeader.configHash`) must follow the same pattern: either build it via low-level accessors (`new_X`/`X_set_dataField`) or manually memset+populate the native struct before `write_X`, never the bare `encode_X_ptr` convenience path. + +## Key Learning (2026-08-04, scheduler-core task 7.1) — a nested-struct-typed field's generated getter (`Parent_get_child(p)`) calls `capn_getp(p.p, idx, 0)` with `resolve=0`, so it can return an UNRESOLVED far pointer (`.p.type == CAPN_FAR_POINTER == 5`) after a real segment→bytes→segment round-trip. Per-field accessors that index raw struct offsets (`capn_read64`/`capn_read32`, used by every plain-scalar `X_get_field`) do NOT auto-resolve it and will silently read zeros/garbage — unlike `capn_get_text`/`capn_get_data`, which DO resolve internally, so Text/Data fields "just work" and mask the bug. **Rule**: after getting ANY nested-struct pointer from a generated accessor, call `capn_resolve(&ptr.p)` before touching that struct's own scalar field accessors. Whether the far-pointer case actually triggers seems to depend on segment layout/allocation order (a small enough test can accidentally stay near-pointer and pass without the resolve call, which is exactly what happened here on the first pass — `test_replay_header_snapshot_entry_round_trip`'s `ReplayCluster` nested struct happened to pass without it, while the very next test's `SessionSummary` nested struct came back all-zero without it — same bug, different luck). + +## Do-Not-Repeat (2026-08-04, scheduler-core task 7.1) — `typemap.h` and `test_e2e_capnp.c` are NOT clang-format-clean at baseline (hundreds/dozens of pre-existing `-Wclang-format-violations` even before any edit in this session, confirmed via `clang-format --dry-run --Werror` against the pre-edit file). This project's actual house style for these files is hand-aligned columns that `.clang-format`'s webkit-based config doesn't reproduce, and no build/test gate enforces clang-format compliance on them (the full `meson compile`/`meson test` run is clean either way). Don't waste a cycle trying to reformat new additions to strict clang-format output in these files — match the surrounding hand-aligned style instead, which is what the codebase actually treats as canonical. + +## Key Learning (2026-08-04, scheduler-core task 7.2) — when extending a hand-aligned `:union` block (capnp) or matching C `union` (typemap.h) with new members whose ordinal/name is wider than the existing column width, do NOT reflow/realign the pre-existing member lines to make columns line up — that turns every existing line into a diff hunk (looks like a rewrite, not an addition) even though only whitespace changed. Leave the existing lines' bytes untouched and let the new, wider members be locally mis-aligned with the old column; `git diff` then shows a clean pure-addition (verified: `git diff` on both `controller.capnp`'s `ReplayEvent.body` and `typemap.h`'s `replay_event_t.body` showed 0 changed bytes on any pre-existing line after reverting an initial column-realignment pass). This matters most for schema files where reviewers explicitly gate on "existing ordinals/fields must be byte-identical." + +## Key Learning (2026-08-04, scheduler-core task 7.2) — for a NEW capnp struct being added to `controller.capnp` that reuses an EXISTING nested struct with real substructure (here: `JobUpdateEvent.job: Job`, reusing the pre-existing `xce_job_t`-backed `Job`), the codec bridge (`encode_X_ptr`/`decode_X_ptr`/`free_X_ptr`, generated by `$C.codecgen`) recurses correctly through arbitrarily deep nested-pointer and list-of-pointer fields (`Job.submitData`→`JobSubmitData`, `Job.allocs`→`JobAlloc`→`List(HostAlloc)`→`List(ResAlloc)`, etc.) with ZERO hand-written accessor code needed — confirmed by reading the actual generated `encode_Job`/`decode_Job`/`decode_HostAlloc_list` bodies, which chain `encode__ptr`/`decode__ptr` calls automatically, and every generated `decode__ptr` helper already calls `capn_resolve()` internally (unlike the raw low-level accessor path documented in task 7.1's far-pointer gotcha above). **Rule of thumb**: use the codec bridge (Test 6's `test_data_job_array_variant_round_trip` pattern) whenever the new/reused struct has a real backing C struct and no `Data` field anywhere in its transitive closure; reserve the low-level accessor API (Tests 7-8's pattern) for frame-only types with no live C struct yet, or anything touching a `Data` field (task 7.1's no-op-codec gotcha). + +## Do-Not-Repeat (2026-08-04, scheduler-core task 7.3) — never write `TypeA*/TypeB` (a pointer-star immediately followed by a slash) inside a C `/* */` block comment. `typemap.h`'s prose "the same raw uint8_t*/size_t pattern" silently closed the comment early at the `*/` in `uint8_t*/size_t` — everything after (rest of the comment, the struct's opening brace) got parsed as real tokens, producing ~20 cascading "expected declaration specifiers" errors across `typemap.h`/`controller.capnp.h`/`controller.capnp.c` that look nothing like the actual root cause at a glance. Logged as bug-1493. When a struct/type suddenly "doesn't exist" right after a comment edit, grep that comment for a bare `*/` mid-word before suspecting anything else. + +## Key Learning (2026-08-04, scheduler-core task 7.3) — when design.md's literal `$C.mapname` target for a NEW capnp struct names an EXISTING C type that lives in a component-private header (e.g. `xce_sched_user_t`/`xce_sched_project_t` in `src/scheduler/include/sched/types.h`, scheduler-only) rather than a shared header `typemap.h` can reach, do NOT reuse that exact name as the mapname target even though the field lists match perfectly. `typemap.h` is compiled into every binary linking `controller.capnp` (controller/apiserver-CGO/scheduler/xceadmin/worker), so the mapname target type must live somewhere `typemap.h` includes (`xce/types.h` or `typemap.h` itself) — but the scheduler-private header in question ALREADY `#include`s `typemap.h` (confirmed: `core_events.c`/`schedobj.c` include both in one TU), so adding a same-named typedef to `typemap.h` is a GUARANTEED duplicate-typedef compile error the instant both headers meet in one translation unit, not a hypothetical risk. Resolution: mint a fresh, differently-named C type in `typemap.h` (here `sched_snapshot_user_t`/`sched_snapshot_project_t`) with an IDENTICAL field layout, and document the deviation prominently in both the schema comment and typemap.h comment (field lists still match design.md's literal spec; only the C type name differs). This is the SAME pattern this task already used deliberately for `PreemptVictim`/`PreemptDecision` (fresh mapnames, distinct from decision.h's core-internal `xce_preempt_victim_t`/`xce_preempt_t`) — treat "component-private header defines the type design.md wants to reuse" as an automatic trigger for this pattern, not something to special-case per-type. + +## Key Learning (2026-08-04, scheduler-core task 7.3) — a struct with exactly ONE `Data`-typed field mixed among many normal (scalar/Text/nested-struct/List-of-mapname'd-struct) fields can still safely use the full `encode_X_ptr`/`decode_X` codec bridge for ALL the normal fields (confirmed by reading the generated `encode_SnapshotData`/`decode_SnapshotData` bodies: every non-Data field gets a real assignment or recursive `encode__ptr` call; the Data field line is literally just `/* internalState internal_state */`, a silent no-op comment, not a type-mismatch compile error as originally feared). The safe hybrid: call `encode_X_ptr(cs,&p,&native)` once to populate everything else (including recursively encoding nested `List(MapnameStruct)` fields straight from native `T**`+`int count` pairs — no manual list-building needed), THEN separately call the low-level `X_set_dataField(p, capn_data)` on the SAME already-allocated struct pointer `p` to fill in just the Data field (mirrors task 7.1's config_hash technique, but composed with the codec bridge instead of replacing it entirely). This is strictly less code than building the whole struct via low-level accessors just because one field can't use the bridge. + +## Key Learning (2026-08-04, scheduler-core task 8.1) — a project's 800-line-per-file cap can be respected by a 3-way split when a 2-way split can't get either resulting file under the cap: `snapshot.c` (1076 lines: xce_sched_core_save_snapshot/xce_sched_core_load_snapshot + a ~290-line shared design-decision doc comment covering BOTH directions) was split into `snapshot.c` (save + the full shared doc comment, 621 lines), `snapshot_wire.c` (small helpers + SAVE-direction wire builders — matched the file's OWN pre-existing internal section-comment boundaries exactly, 258 lines), and `snapshot_load.c` (load + load-specific "picker rebuild" doc subsection, 335 lines). Cutting along the file's own existing `/* ---- Section Name ---- */` dividers (rather than an arbitrary line-count chop) is what made all three resulting files independently cohesive. Cross-file helper sharing used the SAME idiom `core_session.c` already established for a single symbol (`extern void sched_worker_release_host_ctx(...)`, redeclared at each consumer rather than a new private header) — but a shared MACRO constant (here `SCHED_SNAPSHOT_INTERNAL_VERSION`, needed by both save's writer and load's version-check reader) can't be `extern`-declared like a function, so it moved into the existing shared umbrella header (`core.h`) instead of being duplicated (duplicating a versioning constant risks silent drift between the two copies). + +## Key Learning (2026-08-04, scheduler-core task 8.1) — test files in `src/scheduler/testsuite/` are NOT held to the same 800-line hard cap as production `.c` files: `test_picker_fcfs.c` (801 lines) and `test_driver_translate.c` (820 lines) are both pre-existing, committed, already over 800 — confirmed precedent that a cohesive single-Observable test file (all fixtures/stubs/the-one-big-test-function in one white-box TU) is accepted here even past the nominal cap, unlike production `.c` files where task 4.3's core_events.c split was enforced strictly. `test_snapshot_save_load.c` (846 lines) follows this test-file precedent rather than being force-split. + +## Key Learning (2026-08-05, scheduler-core task 9.3) — reliably forcing a REAL (not simulated-return-code) `fwrite()` failure against a `FILE*` whose underlying fd was closed out from under it requires disabling stdio buffering FIRST: a plain `close(fileno(f))` alone is not enough, because a small `fwrite()` on a still-fully-buffered stream just copies bytes into the userspace buffer and reports success without ever reaching the (now-invalid) fd — the failure would only surface later, on the next `fflush()`/`fclose()`/buffer-full write, not at the `fwrite()` call site production code actually checks. The reliable sequence (used in `test_record_lifecycle.c`'s new write-failure tests): (1) `fflush(f)` to commit any already-buffered bytes to the still-good fd, (2) `setvbuf(f, NULL, _IONBF, 0)` to force every subsequent `fwrite()` to hit the syscall directly, THEN (3) `close(fileno(f))`. After this sequence, the very next `fwrite()` fails immediately and visibly. `fflush()`/`fclose()` on the now-broken stream afterward (e.g. inside a cleanup/abort handler) are themselves safe no-ops-that-report-EBADF, not crashes — glibc's `fclose()` frees the `FILE*`'s own heap state regardless of whether the underlying `close(fd)` syscall it issues succeeds. + +## Key Learning (2026-08-05, scheduler-core task 9.3) — to test that ONE SPECIFIC write call inside a multi-write function fails while EARLIER writes in the same call succeed (e.g. proving a periodic-checkpoint write's own failure path, distinct from the per-event writes preceding it in the same `sched_record_write_events()` call), fd-breaking tricks can't discriminate between sub-writes within a single call — once the fd is broken, ALL subsequent writes in that call fail, not just the target one. Instead, find an independent precondition the LATER call alone depends on and flip only that: here, `xce_sched_core_save_snapshot()`'s documented `core->session_active == 0` precondition (core.h) is checked ONLY by the snapshot/checkpoint path, never by the plain per-event `ReplayEvent` writer, so setting `sched.core.session_active = 1` right before the triggering call makes exactly the periodic-checkpoint's `write_snapshot_entry()` fail (`-EBUSY`) while the preceding event write in the same call succeeds normally — a clean, deterministic, fd-independent way to isolate one sub-path's failure. + +## Do-Not-Repeat (2026-08-04, scheduler-core task 8.1) — an `xce_val_t` constructed by hand (not through a real wire-decode path) MUST have `.which` set explicitly to the correct `XCE_VAL_*` tag before it can safely cross `dup_val()` (or any code that branches on `.which`) — `XCE_VAL_STRING` is literally `0` (`xce/types.h`), so a `memset`-zeroed `xce_val_t` with only `.u.ivalue32` set defaults to `.which == XCE_VAL_STRING`, and `dup_val()` will reinterpret the raw int32 bits as a `char*` and crash inside `makeString()`/`hash_string()` (SEGV at the literal small-integer address). `test_core_events.c`'s own fixtures get away with never setting `.which` only because their resval path (`core_apply_config_resvals`) does a raw `MOVE_PTR`, never branching on `.which` — that convention is NOT safe to copy into any test that actually exercises a `.which`-branching consumer like `dup_val()` (task 8.1's own `snapshot_wire.c`). + +## Key Learning (2026-08-04, scheduler-core task 8.1) — `sched_get_user()`/`sched_get_project()` (`core.c`) had a real, pre-existing (tasks 4.1/4.3 era) get-or-create bug: both did `rc = key_find(...); if (rc < 0) return NULL;` before falling through to the create fallback, but `key_find()`'s actual "not found" signal is a NEGATIVE errno (`-ENOENT`, confirmed via direct instrumentation: `rc=-2` for absent, `rc=1` for present) — not `rc==0` as the surrounding code assumed. This made get-or-create unreachable for any genuinely new name, silently leaving `job->user`/`job->project` NULL for every job referencing a brand-new user/project (in BOTH the live `core_events.c` JOBS_PUSH path and the still-live legacy `schedobj.c` path), completely unnoticed because no existing test asserted `job->user`/`job->project` non-NULL for a freshly-referenced name. Caught by task 8.1's own cross-core equivalence test (RED phase). Fixed by narrowing the guard to `if ((rc < 0) && (rc != -ENOENT))` in both functions — logged as bug-1495. **Do-not-repeat**: when wrapping a `key_find()`-style lookup with get-or-create semantics in this codebase, verify the library's actual not-found return value empirically (small debug instrumentation) rather than assuming `rc==0` — this same wrong assumption was independently baked into two sibling functions. + +## Key Learning (2026-08-05, scheduler-core task 9.4 fix round) — a vtask-worker-thread reader of `xce_scheduler_t` recording fields (`sched_record_status()`) races the single-threaded driver loop's writers (`sched_record_start`/`sched_record_stop`/`sched_record_write_events`/`sched_record_abort_on_error`) with zero prior synchronization; fixed by adding a NEW dedicated `mylock_t record_lock` field (not widening `core.lock`, since `core` vs driver-level state is a deliberate architectural boundary per this spec's own design) initialized in `sched_init()`/destroyed in `sched_fini()`, taken only around the struct-field reads/writes themselves (never across `fopen()`/`fwrite()`/`capn_write_mem()` I/O). Do-not-repeat pattern for future admin/status read paths in this codebase: a synchronous "status" reply that borrows a live heap pointer (`reply->file = sched->record_file_path`) into a struct that outlives the current call's lock scope is unsafe the moment ANY other thread can mutate that pointer — the safe idiom is `strdup()` the string's bytes WHILE HOLDING the lock, hand back a caller-owned copy, and document (both the header comment and the call site) that the caller must `free()` it. This differs from the same-stack-frame "borrow a local buffer" idiom `handle_deferred_admin_record()` uses for recordStop's `last_path` (driver.c) — that idiom only works when the snapshot and the eventual consumer share one stack frame; sched_record_status()'s caller lives in a different function (handler.c), so a stack-local buffer there would dangle across the return. + +## Decision Log (2026-08-05, scheduler-core task 9.4 fix round) — chose a DETERMINISTIC pthread-based lock-contention test (hold `record_lock` on the main test thread, spawn a real second pthread calling `sched_record_status()`, assert it has NOT returned after a generous `usleep()`, then assert it completes once the lock is released) over a genuinely racy ASan-timing-dependent reproduction, following the SAME precedent task 9.2 already established for its TOCTOU test (test_record_lifecycle.c) of preferring a deterministic proof over a flaky race test. Verified the test is not vacuous by temporarily commenting out the `mylock_lock()`/`mylock_unlock()` calls in `sched_record_status()` and confirming the test genuinely FAILS (`Expected 0 Was 1`) before restoring the real fix — all 26/26 scheduler tests pass with the fix in place. A second, fully deterministic (no timing dependency at all) test proves the actual use-after-free is closed: take a status snapshot, then call the real `sched_record_stop()` (which `free()`s `sched->record_file_path` for real under ASan), and confirm the snapshot's `reply->file` — a separate `strdup()` allocation — survives untouched. + +## Key Learning (2026-08-05, scheduler-core task 10.3) — `src/xceadmin/src/xcecommon/schema_files/controller.capnp` (the bundled schema copy `xcecommon.schema.load_schema()`'s default path resolves to) is a BUILD ARTIFACT, not tracked in git (`git ls-files` confirms) — `src/xceadmin/meson-build.sh` `cp`s the real source-of-truth `src/schema/controller.capnp` into it fresh on every `meson compile -C xceadmin` run, inside an out-of-tree mirror. The live checkout's copy under `src/xceadmin/src/xcecommon/schema_files/` can be silently STALE relative to `src/schema/controller.capnp` (confirmed: it was still missing task 7.4's `SchedAdminAction`/`SchedAdminRequest`/`SchedAdminReply` additions when checked directly) — this is harmless for the real meson-driven test run (which always re-syncs), but would bite a bare `pytest src/xceadmin/tests` run from the live checkout without first re-syncing or setting `XCEADMIN_SCHEMA_PATH`. Never hand-edit this file; if editing `controller.capnp`, always edit `src/schema/controller.capnp` (already established in admin-config-tool's own task 1.3 note) and let the build re-sync it. + +## Decision Log (2026-08-05, scheduler-core task 10.3) — `xceadmin.scheduler`'s `record_start/stop/status()` deliberately DO NOT follow `xceadmin.push.push_config()`'s established "return an application-level failure as data" convention (`PushResult.error` populated, never raised, caller/CLI decides). Task 10.3's own Observable text is explicit and different: "raises a typed error on connection failure, decode failure, or an error reply" — naming a business-level `SchedAdminReply{ok=false}` as a THIRD raising failure mode on equal footing with connection/decode failures. Implemented as a new `SchedAdminError(XceAdminError)` (`xcecommon/errors.py`) carrying `error`/`recording`/`file`/`event_count`/`started_at` (full reply state, not just the reason string, since e.g. "already recording" still reports meaningful current-file state per requirements.md's Reply field semantics table). design.md's older API Contract table (written before task breakdown) still lists Errors generically as "XceAdminError subclasses (connection, redirect, decode)" with no explicit error-reply row — treated as design.md being superseded by the more specific, later tasks.md Observable, not a conflict to resolve by asking. Future work on this file (task 10.4's `dump()`, task 10.5's CLI wiring) should NOT assume `push.py`'s return-as-data convention applies uniformly across this package — check each function's own task Observable text. + +## Do-Not-Repeat (2026-08-06, scheduler-core task 10.5) — a `click` CLI command function must NEVER share its bare name with a directly-imported business-logic function it delegates to, even though nothing raises an ImportError. `cli.py`'s existing convention (`config_build`/`config_push` calling `build_cluster_config`/`push_config`) already dodges this by prefixing the CLI function with its group name — but it would be an easy trap to miss when adding `scheduler record start/stop/status` + `scheduler dump`, since `from xceadmin.scheduler import record_start, record_stop, record_status, dump` are the exact literal command names AC 10/20 use. Defining `def record_start(...):` under `@record.command("start")` that internally calls `record_start(...)` creates infinite self-recursion (Python resolves the free-variable name at call time from the module's current globals, which by then point at the new local `def`, not the import) — no error at import time, only a `RecursionError` the first time the command actually runs. Fix used here: keep the direct unaliased imports, but name every CLI command function with its full group-path prefix (`scheduler_record_start`/`scheduler_record_stop`/`scheduler_record_status`/`scheduler_dump`), passing the short literal command string explicitly to `@x.command("start")`/`@scheduler.command("dump")` (click infers the CLI-visible name from the decorator arg, not the Python function name, so the prefix never leaks into `--help` output). Same collision risk applies to the top-level `@cli.group() def scheduler()` vs. `import xceadmin.scheduler` — used `from xceadmin.scheduler import ...` (never `from xceadmin import scheduler`) for exactly this reason. + +## Key Learning (2026-08-06, scheduler-core task 10.5) — requirements.md's "Status output" example under AC 10 is a load-bearing literal spec, not illustrative prose: every label (`recording:`/`file:`/`events:`/`started at:`) is left-padded to column 13 before the value (verified by counting characters in the raw markdown — `"recording:".ljust(13)`==`"file:".ljust(13)`==`"events:".ljust(13)`==`"started at:".ljust(13)`, all 13 wide). `startedAt` is documented as "nanoseconds since epoch" (real Unix epoch, not a scheduler-internal logical clock), rendered as timezone-less ISO-8601 (`2026-07-29T02:09:00`, i.e. `datetime.fromtimestamp(ns/1e9, tz=timezone.utc).strftime("%Y-%m-%dT%H:%M:%S")`). AC 10 also says `record start`/`record stop` return the SAME confirmation fields as `record status` "for confirmation" — so all three CLI subcommands should share one formatting helper, not three separate one-off `click.echo()` calls. + +## Decision Log (2026-08-06, scheduler-core task 10.5) — `xceadmin.cli`'s established precedent (config_push) surfaces every `XceAdminError` subclass via bare `e.message`. `SchedAdminError` is a deliberate, documented exception to that precedent: its own docstring (`xcecommon/errors.py`, task 10.3) explicitly invites a richer CLI message ("so a caller that wants to display it ... does not have to re-query record status separately"), and `e.message` alone (`"scheduler admin request failed: already recording"`) does carry the reason but drops the `recording`/`file`/`event_count` state the exception was specifically built to carry. Chose: catch `SchedAdminError` in a branch ahead of the generic `XceAdminError` catch-all, and only add extra detail (current file + event count) when `e.recording` is true (i.e. only the "already recording" case has genuinely useful extra state to show — "not recording"/"scheduler not connected"/"request timeout"/"invalid record file name" all report `recording=false` and fall through to the plain `e.error` string, matching precedent for those). Every other `XceAdminError` subclass (including `ReplayFileError` for `scheduler dump`) still gets the bare `e.message` treatment, unchanged. + +## Do-Not-Repeat (2026-08-06, scheduler-core task 11.1) — `xce_sched_core_fini()` (`core.c`) unconditionally calls `dataq_fini(core->pend_updates[0])`/`dataq_fini(core->pend_updates[1])`, and `dataq_fini()` itself SEGVs (`dataq_deq_head` NULL deref) if passed a NULL `dataq_t*`. `xce_sched_core_init()` deliberately leaves both slots NULL (core.c's own file header comment: the dataq killer callback stays driver-owned) — so ANY caller of `xce_sched_core_init()` that might later call `xce_sched_core_fini()` (including on an early-return error path) MUST separately `calloc(1, sizeof(dataq_t))` + `dataq_init(..., killer_or_NULL)` both slots immediately after init, even if that driver never actually enqueues anything into them (a NULL killer is fine in that case — matches every core.c-including test fixture's own established pattern, and scheduler.c's real `sched_init()`). Building `xce_sched_sim_driver_t` (task 11.1's own sim.c) hit this directly: the sim driver never uses `pend_updates` (task 11.2's event loop bypasses them entirely per Req 5 AC 18, translating `.replay` events straight into `xce_core_event_t` batches), but its own error-path cleanup calls to `xce_sched_core_fini()` crashed until the same alloc+init pair was added right after `xce_sched_core_init()`. + +## Key Learning (2026-08-06, scheduler-core task 11.1) — the C-side equivalent of xceadmin's raw-byte `.replay` message-shape-peek technique (task 10.4, `xceadmin/scheduler.py`'s `_peek_message()`) needs no manual byte parsing: after `capn_init_fp(&c, f, 0)` decodes one message, `capn_getp(capn_root(&c), 0, 1)` (resolve=1) returns the REAL top-level payload's `capn_ptr` (every writer in this codebase stores its actual message at pointer slot 0 of the implicit root struct, per `write_capnp_message()`'s callers' `capn_setp(capn_root(&c), 0, ptr.p)` convention) — its `.datasz` (BYTES, divide by 8 for word count) and `.ptrs` fields give the exact `(dataWordCount, pointerCount)` shape tuple, which matches controller.capnp.h's own generated `_word_count`/`_pointer_count` constants 1:1 (verified: ReplayHeader=(1,5), SnapshotEntry=(1,1), ReplayEvent=(2,1), ReplaySession=(1,2), identical to the Python-side values already documented). This is how sim.c distinguishes "first message is ReplayHeader" / "first data message is SnapshotEntry" without any self-describing type tag (none exists in the wire format) and without a fragile trial-decode. + +## Do-Not-Repeat (2026-08-06, scheduler-core task 11.1) — after `capn_free(&c)`, ANY `capn_ptr`/generated `_ptr` wrapper still holding a pointer INTO that freed arena becomes immediately dangling; re-reading a scalar field through it (even a cheap accessor like `SnapshotEntry_get_ts(sp)`) is a genuine ASan-catchable use-after-free, not just a style nit. In a scan loop that frees `c` per iteration, any value needed AFTER the free (e.g. "the timestamp of whichever candidate we kept") must be captured into a plain local BEFORE the `capn_free(&c)` call that owns it — never re-derived from the wire-struct view afterward, even if the pointer LOOKS like it should still be readable. + +## Key Learning (2026-08-06, scheduler-core task 11.1) — `xce_sched_core_load_snapshot()` (snapshot_load.c, task 8.1) aliases `core->config = in->config` (a borrowed-reference assignment, not a deep copy, per its own documented "config -- borrowed reference" choice) — after a successful call, `in`'s `config` field (and therefore anything that owns/frees `in` as a whole, e.g. a decoded `replay_snapshot_entry_t` from a `.replay` file) must NEVER be freed, or `core->config` immediately dangles. Every OTHER field `load_snapshot()` restores (jobs/queues/workers/users/projects/resources/resvals) IS independently deep-copied (via `makeString()`/`dup_val()`/fresh `calloc()`s), so only `config` carries this hazard — but since there is no partial-free API, the safe rule (matching `recording.c`'s `write_snapshot_entry()` own documented choice on the SAVE side) is: never free the snapshot struct that was actually handed to `load_snapshot()` at all, and accept the leak of its non-aliased remainder (this whole test suite already runs under `ASAN_OPTIONS=detect_leaks=0` for exactly this reason). Any INTERMEDIATE candidate discarded BEFORE ever being passed to `load_snapshot()` (e.g. while scanning a `.replay` file for the best matching snapshot) is safe to free normally — the hazard only attaches to the one candidate that was actually loaded. + +## Key Learning (2026-08-06, scheduler-core task 11.2) — the simulation driver (`xce_sched_sim_driver_t`, sim.h) deliberately has no `xce_scheduler_t*` (Req 5 AC 17), which means `plugin_register()`/`register_builtin_fcfs()` (both `xce_scheduler_t*`-typed) and `sched_job_picker()`/`sched_allocator()` (both `xce_scheduler_t*`-typed thread entry points, scheduler.c) are ALL unreachable from it. Task 11.1 punted registering any picker into `sim->core.plugins`; task 11.2 discovered this is load-bearing the moment a real event loop calls `xce_core_go()` and then blocks draining `core->decisions` — with zero threads ever consuming `core->wbuf`, that block is a permanent deadlock, not a degraded-but-safe path, regardless of whether the fixture has any real jobs. Fixed by: (1) a new generic `plugin_register_core(xce_sched_core_t*, xce_sched_plugin_t*)` (plugin.c/plugin.h) that does `init_fn()` + `key_add(core->plugins,...)` directly, skipping `plugin_register()`'s `plugin->sched = sched` bookkeeping (`plugin->sched` stays NULL — safe, since nothing calls `plugin_unregister()` on a sim-registered plugin, no sim-driver teardown function exists yet); (2) a new `builtin_fcfs_new_plugin()` factory (builtin_fcfs.c/plugin.h) extracted from `register_builtin_fcfs()`'s existing template-build step (behavior-preserving refactor, zero change to the real production registration path); (3) a NEW persistent core-scoped picker-driving thread (`sim_picker_thread_fn`, sim_loop.c) mirroring `sched_job_picker()`'s outer loop, spawned once per `sched_sim_driver_run()` call, never joined (no sim-driver fini exists). Real resource-matching allocation (`sched_make_allocation()`) is confirmed (independently, by an earlier task 4.6 cerebrum note: "sched_allocator Requires Full xce_scheduler_t* Context; Cannot Be Used in Pure-Core Contract Tests") to be unreachable without a genuine `xce_scheduler_t*` too, and has no pure-core equivalent — that extraction belongs to the separate, not-yet-designed `scheduler-allocation` spec. Task 11.2's thread therefore conservatively PENDs every picker candidate (real `on_pend()`/`make_pend_decision()`/`make_pendreason()` calls, never a fabricated DISPATCH+allocation) rather than block on or fake real resource matching. + +## Do-Not-Repeat (2026-08-06, scheduler-core task 11.2) — a header's forward-declared opaque type (e.g. `plugin.h`'s `typedef struct xce_sched_core_s xce_sched_core_t;`, placed deliberately just above the picker-interface section so `xce_job_picker_iface_t`'s hooks can reference it) only satisfies function DECLARATIONS placed textually AFTER it in the same file — inserting a new `extern` prototype using that type earlier in the file (e.g. right after the `register_*` block, before the forward-declare) fails to compile with "unknown type name" everywhere the header is included, even though the type IS declared later in the very same file. Always grep/read for where a needed forward-declare actually sits before adding a new declaration that depends on it. + +## Do-Not-Repeat (2026-08-06, scheduler-core task 11.2) — a new PUBLIC header (e.g. `sim.h`) must never gain a declaration whose parameter types (`struct capn`, `capn_ptr`) come from a heavy third-party runtime header (`controller.capnp.h`/capnp_c.h) unless every consumer of that public header already includes the heavy header first — `main.c` and other non-capnp-aware callers include `sim.h` directly. The fix used here: keep such a shared-but-not-really-public helper (`msg_root_has_shape`, shared between `sim.c` and the new `sim_loop.c`) as a non-static (external-linkage) function with NO declaration in the public header at all — instead, each `.c` file that calls it carries its own local `extern` prototype, the SAME "extern declared where consumed, not in a shared header" convention `recording_events.c`/`recording_session.c` already establish for `write_capnp_message()` (`recording.c`). + +## Decision Log (2026-08-06, scheduler-core task 11.2) — chose to advance the simulation driver's virtual clock via a small NEW `sim_advance_virtual_clock()` living at driver level (`sim_loop.c`, `static`, not exported through `core.h`) rather than adding a general "jump virtual time" setter to `core_clock.c`'s public API. Rationale: `core_clock.c`'s own file header comment claims `xce_core_now()`'s `clock_gettime()` call is "the ONE wall-clock call in the entire scheduler-core module family" — adding a second wall-clock call site inside `core_clock.c` itself would make that claim stale/false. `sim.c`'s own task-11.1 `sim_set_virtual_clock()` already established the precedent that driver-level `clock_gettime()` calls (re-seeding `virtual_clock_base`/`real_clock_base`) are legitimate and OUTSIDE `test_core_isolation.c`'s scan boundary (scoped to `core.c`/`core.h` text only) — the new function is the same pattern, just re-anchoring speed_num/speed_den-agnostic (mid-run jump only, never touching the speed settings `sched_sim_driver_start()` already configured), used for every ReplayEvent's own timestamp AND every ReplaySession's `beginRound`. + +## Do-Not-Repeat (2026-08-06, scheduler-core task 11.3) — NEVER run `clang-format -i` against a `meson.build` file. It is a C/C++ formatter with no meson/Python-ish grammar awareness; pointed at `meson.build`, it silently "succeeds" (no error) but reinterprets meson's syntax as C-ish tokens and mangles it into garbage (e.g. `scheduler_test_src = [...]` became `scheduler_test_src =\n [\n ...\n ]\n\n scheduler_test_deps\n = [...]`, and every `#` comment line got wrapped/mis-indented). Caught immediately only because the post-edit file-watcher hook echoed the corrupted content back in a system-reminder. Fix: `git checkout -- ` to discard the corruption (safe only if the file had no OTHER uncommitted changes worth keeping — re-apply any real edits via Edit afterward, never via another format pass). When a task says "run clang-format on changed/new files," scope the command explicitly to `*.c`/`*.h` files only, never a blanket pass over every changed path in the diff. + +## Key Learning (2026-08-06, scheduler-core task 11.3) — a source-text isolation-scan test (the `test_core_isolation.c` genre: `strstr()` a forbidden symbol/type name across a whole file's raw text) breaks the moment the audited file's OWN doc comments legitimately name the forbidden symbol in prose while documenting the very boundary being enforced. Concretely: `sim.h`'s task-11.1 boundary comment says *"`xce_sched_sim_driver_t` below contains no `xcecli_ctx_t`, no `xce_cluster_config_t`..."* — a raw whole-file scan for those exact substrings false-positives on that sentence. `test_core_isolation.c` never hit this because `core.h`/`core.c`'s comments never happen to name a forbidden symbol by name. Fix used in `test_sim_isolation.c`: a small state-machine `strip_c_comments()` (block + line comments replaced with a space, string/char literal contents left untouched) run BEFORE every substring scan — comments never produce compiled symbols anyway, so stripping them first keeps the source-text layer consistent with what the nm(1) link-time layer actually measures. General rule for any future isolation/boundary audit test in this codebase: if the file under audit is expected to document its own forbidden-dependency list in prose (encouraged, good practice), the scanner MUST comment-strip first, not scan raw text. + +## Key Learning (2026-08-06, scheduler-core task 11.3) — proving "the struct definition itself" has no forbidden field (Req 5 AC 17's literal wording) needs a NARROWER check than a whole-file scan: extract just the `typedef struct { ... } xce_sched_sim_driver_t;` body text (find the last `typedef struct {` strictly before the unique `} xce_sched_sim_driver_t;` end marker — `sim.h` has a SECOND, later, unrelated `typedef struct {` for `sched_sim_args_t`, so "first occurrence" would be wrong if the struct order ever changed, but "last occurrence before the known end marker" is robust either way) and scan only that substring. This also catches a real gap the generic FORBIDDEN_SYMBOLS prefix list (reused from `test_core_isolation.c`) does NOT cover: `xce_cluster_config_t` is a plain type name, not a call-site function-name prefix, and no existing FORBIDDEN_SYMBOLS entry is a substring/superstring match for it (`xce_cluster_config_validate` is a different, unrelated string) — Req 5 AC 17 names it explicitly, so it needs its own dedicated STRUCT_FORBIDDEN_TYPES list, separate from FORBIDDEN_SYMBOLS. + +## Decision Log (2026-08-06, scheduler-core task 11.3) — verified self-review discipline for isolation/audit tests in this codebase (matches `test_core_isolation.c`'s own `any_undefined_seen` vacuous-pass guard bar): don't just assert the new test passes clean — temporarily inject one violation per layer (a forbidden struct field via an opaque forward-declared pointer type so it still compiles; a forbidden `#include "client.h"`; a forbidden call-site symbol reference via a locally-declared `extern` prototype so it links without pulling in the real client library), rebuild+run, confirm the EXACT expected test/assertion fails (not a different one, not a build break), then revert via `cp`-from-backup and `diff -q` to prove byte-identical restoration. For `test_sim_isolation.c` this caught the struct-field layer, the `#include` layer, and — usefully — proved the source-text-scan and nm(1) link-time layers are genuinely independent (both fired on the same injected `xcecli_hello` call, not just one). + +## Do-Not-Repeat (2026-08-06, scheduler-core task 11.5) — recurrence of the accidental-`*/`-inside-a-comment bug (same family as bug-1493 in typemap.h): writing prose like `xce_scheduler_t*/scheduler.h dependency` inside a `/* ... */` block comment terminates the comment early at the literal `*/` substring, turning the REST of the doc comment into raw C tokens and producing dozens of cascading "stray '#'"/"missing terminating character" errors far from the true location. Caught immediately by the RED-phase build (not by inspection). When writing a doc comment that needs to say "type* / other-thing" (a type name immediately followed by a slash), always insert a space around the slash (`xce_scheduler_t* / scheduler.h`) — grep new/edited comment blocks for the regex `[A-Za-z0-9_]\*/` before building whenever a comment discusses pointer types near a path or ratio. + +## Do-Not-Repeat (2026-08-07, scheduler-core task 12.3) — THIRD recurrence of the accidental-`*/`-inside-a-comment bug (same family as bug-1493/the 11.5 entry above): `xce_scheduler_t*/record_dir` in a new repl_params.c doc comment closed the comment early again, despite the lesson already being logged twice. This confirms the existing "grep before building" advice isn't being followed proactively — going forward: after writing ANY new multi-paragraph doc comment in this codebase (not just when a build error already occurred), run `grep -n '[A-Za-z0-9_]\*/' ` on it BEFORE the first build attempt, not after. Applied retroactively this session across all 4 touched repl*.c files before the final build — zero hits confirmed the fix was complete. + +## Key Learning (2026-08-06, scheduler-core task 11.5) — this repo's top-level `xce.build` sometimes needs TWO consecutive `ninja -C xce.build ` invocations after a `meson.build` source-list change (adding a new `.c` file to an existing target, or a whole new `executable()`): the first invocation's "Regenerating build files" step reconfigures but the immediately-following `ninja` command in the SAME invocation can still report "unknown target" against the freshly-added target/source; a second, separate `ninja -C xce.build ` (or `ninja -C xce.build` with no target, or `ninja -C xce.build -t targets all` to confirm the target now exists) picks it up. Don't treat "unknown target" right after a meson.build edit as evidence the edit is wrong — retry the build once before debugging further. Also: `ninja -t targets` (no arg) only lists targets needed for the DEFAULT build, not the whole project — use `ninja -t targets all` to enumerate everything when hunting for a specific new target name. + +## Decision Log (2026-08-06, scheduler-core task 11.5) — SUPERSEDED 2026-08-07, see Do-Not-Repeat below: the selective-`git stash` RED-phase technique described here did NOT actually reproduce when independently re-run (twice by the round-1 reviewer, once by the round-2 remediation pass) — it produced a hard `ninja` build failure, not the claimed 3-test-failure RED, because `test_sim_event_loop.c` (left un-stashed, since it's a test file) already calls `sim_process_replay_event_message()` with the NEW 5-arg signature while the stashed-back `sim_loop.c` only declares the OLD 4-arg one. The original claimed evidence (3 tests failing under `meson test`) was therefore not reproducible as described. Do not treat this stash technique as validated; see the replacement methodology below. + +## Do-Not-Repeat (2026-08-07, scheduler-core task 11.5 review round 2) — `git stash` scoped to "behavioral production files only" is UNSAFE as a RED-phase probe whenever any test file in the same change already calls the NEW signature/behavior of a function whose OLD declaration lives in a stashed-back production file — the stash desyncs caller (test, not stashed) from callee declaration (production, stashed), producing a build failure that looks like a red herring, not a real RED. Validated replacement methodology (reproduced clean by both the round-1 reviewer and this round-2 remediation): make a DIRECT, TARGETED, IN-PLACE temporary edit to ONLY the specific lines implementing the Observable under test inside the one production `.c` file (never touch test files, never touch sibling production files, never use `git stash`) — e.g. hardcode a conditional selection to always take one branch, or wrap a side-effecting call's guard in `if (0 && ...)` to no-op it while leaving it compiling. Rebuild (`ninja -C xce.build`) + run the real suite (`meson test -C xce.build --suite `), capture the ACTUAL fail count/names/assertion messages verbatim (never guess or reuse a previous session's numbers). Revert each edit by restoring the EXACT original text (not a paraphrase), then prove zero residue two ways: `git diff ` shows only the pre-existing (pre-probe) diff, AND `grep` for whatever temporary marker/pattern you introduced (e.g. a "TEMP RED-PHASE PROBE" comment, `if (0 &&`) returns zero matches. Rebuild + rerun to confirm full green restored before reporting. + +## Key Learning (2026-08-06, scheduler-core task 11.5) — recording_session.c's `sched_record_session_add_decision(ctx, ts, decision)` was ALREADY scheduler_t-free before this task (no `xce_scheduler_t*` parameter) — only `sched_record_session_begin()`/`sched_record_session_end()` gate on `sched->recording`/`sched->record_file`. To let a `xce_scheduler_t*`-less caller (the simulation driver, Req 5 AC 17) reuse the exact same accumulate-then-finalize protocol, extracted two new non-static functions — `sched_record_session_ctx_new(begin_round_ts)` (pure calloc+stamp, no gate) and `sched_record_session_finalize(FILE* f, ctx, end_ts)` (pure encode+write+free, target file passed explicitly instead of read off `sched->record_file`) — and turned the existing `_begin()`/`_end()` into thin wrappers around them (gate check, then delegate). This is a pure behavior-preserving refactor (verified identical control flow) that lets a second, unrelated caller reuse 100% of the wire-encoding logic via a local `extern` declaration (never `#include "recording.h"`, which pulls in `scheduler.h` → `client.h`/`xce/cluster.h`, exactly the forbidden dependency chain) instead of hand-deriving the `xce_decision_t` → `replay_decision_t` field mapping a second time. + +## Do-Not-Repeat (2026-08-07, bug-1503, scheduler-core task 11.6) — `makeString()` (`lib/xstring.c`) is NOT a plain-heap `strdup()` equivalent: it is backed by the vendored `rtti` library's `makeInstance()` allocator, whose returned pointer sits SOME BYTES INTO a larger rtti-managed allocation (confirmed via ASan: "56 bytes inside of a 61-byte region"), not at the start of a plain `malloc()` block. `makeString()`-allocated strings are only safe when released by the SAME rtti-aware teardown path that produced them (e.g. `xce_sched_core_load_snapshot()`'s own deep-copy fields, later freed by that module's own kill/free functions) — never by a capnp-generated `free_X()`/`free_X_ptr()` function, which calls plain `free()` on each string field directly. Hit building `test_sim_equivalence.c`'s `make_queue()`/`make_job()` fixtures: populating `xce_queue_t.queue`/`xce_job_t.submit->{queue,user,project_name,resreq}` via `makeString()` then driving them through the REAL production cleanup path (`release_translated_payload()` → `free_QueueConf_ptr()`/`free_Job_ptr()`, driver.c/controller.capnp.c) crashed with an ASan bad-free. Fix: use `strdup()` for any string field a capnp-generated `free_X()` will later `free()` directly; reserve `makeString()` only for fields whose owning module's own teardown path is known to handle rtti-backed allocations. + +## Key Learning (2026-08-07, scheduler-core task 11.6) — `sim_loop.c`'s `sim_picker_thread_fn()` (task 11.2) unconditionally PENDs every scheduling candidate it is handed — it can NEVER emit a live DISPATCH decision, under any replayed event sequence, by construction (its own doc comment calls this a "Known scope boundary": real resource-matching allocation needs a live `xce_scheduler_t*` the simulation driver deliberately does not have, Req 5 AC 17, and belongs to the not-yet-designed `scheduler-allocation` spec). `testsuite/meson.build`'s own comment on `sched_sim_verify_test` (task 11.4) already documented the same fact independently ("including DISPATCH, which sim_loop.c's own real picker thread can never produce live"). Practical consequence for task 11.6's normal↔sim equivalence test: a normal-mode recording containing ANY genuine DISPATCH decision will ALWAYS diverge when replayed in simulation (recorded=DISPATCH, simulated=PEND for the same job) — not a bug in the equivalence test, a confirmed, pre-existing, already-documented architectural gap. `test_sim_equivalence.c`'s fixture is therefore deliberately PEND-only (multiple jobs across two differently-prioritized queues, still a genuine non-trivial regression guard on event translation + picker ordering + PEND-reason content fidelity) rather than silently asserting equality on only a subset of a mixed fixture. If a future task closes the scheduler-allocation gap and gives the sim driver a real allocator, `test_sim_equivalence.c` should be revisited to add a genuine DISPATCH case. + +## Decision Log (2026-08-07, scheduler-core task 11.6) — combining BOTH the normal-mode driver stack (`driver.c` + `recording*.c`) and the full simulation stack (`sim.c`/`sim_loop.c`/`sim_record.c`/`sim_verify.c`) as white-box `#include`s in ONE test TU (`test_sim_equivalence.c`) had no prior precedent in this test suite — every existing test file combines one side or the other, never both. Verified safe before writing the file: a mechanical scan of every `static` function name across all 17 combined `.c` files found zero collisions, and `scheduler.h` (needed for the normal-mode fixture) already coexists with `sim.c`/`sim_loop.c` in the SAME TU in `test_sim_record_output.c` today (that file includes `scheduler.h` for its own fixture code even though `sim.c`/`sim_loop.c` themselves stay `scheduler.h`-free per their own Req 5 AC 17 isolation boundary) — confirming the combination compiles before spending time debugging a hypothetical conflict. + +## Key Learning (2026-08-07, scheduler-core task 12.1) — this codebase's capnpc-c backend silently no-ops `Data`-field encoding inside generated `encode_XXX()` composite helpers, AND the generated `encode_XXX_ptr()` wrapper never zero-initializes its local stack struct first — so any `:Data` field left unpopulated by the no-op'd encode step is written to the wire as UNINITIALIZED STACK GARBAGE, a real, reproducible SIGSEGV risk (`capn_setp`/`read_ptr` deref garbage), not a flaky test. Whether it actually crashes is pure luck of what garbage bytes land in that specific call site's stack frame — confirmed present in at least 2 known fields (`SnapshotData.internalState`, fixed as bug-1504; `WalCatchupEntry.payload`, found but deliberately deferred as bug-1505) across 2 unrelated subsystems (scheduler-core, controller HA/WAL). Fix that actually works (proven, not a workaround): switch the field from `:Data` to `:List(UInt8)` with a companion `UInt32 $C.maplistcount(...)` count field — this SAME capnpc-c backend DOES correctly auto-generate real `capn_new_list8()`/`capn_set8()` encode and `calloc()`+`capn_get8()` decode for `List(UInt8)` (working precedent already in the schema: `FetchFileReply.content`/`bytesRead`). No hand-written low-level accessor code needed after the type swap. Before this, `Data` fields required either "intentionally left unset" (silent data loss, e.g. `ReplayHeader.configHash`) or fully hand-written low-level `capn_setv`/`capn_new_data`-style accessor code bypassing the no-op'd generic codec entirely (e.g. `ReplLogEntry.batchPayloads`, bug-847's fix). Any future `:Data` field in `controller.capnp` should default to `List(UInt8)` instead unless there's a specific reason not to. + +## Do-Not-Repeat (2026-08-07, scheduler-core task 12.1) — a `rm -rf xce.build && meson setup && meson compile` full clean rebuild can flip a genuinely pre-existing, dormant undefined-behavior bug (uninitialized stack memory, see Key Learning above) from silently-inert to deterministically-crashing, purely by shifting the whole binary's stack/register layout — this is NOT something the rebuild "caused," it's exposing UB that was always there. Don't assume a crash that only appears after a full clean rebuild is caused by whatever prompted the rebuild (here: an unrelated nix dependency swap, readline→libedit) — verify root cause with gdb before attributing blame; the actual crash site can be completely unrelated code untouched by the triggering change. + +## Do-Not-Repeat (2026-08-07, scheduler-core task 12.1) — when a defensive guard is added based on an INITIAL crash theory that later turns out to be wrong (root-caused via gdb to something else entirely), re-evaluate whether the guard is still independently justified before keeping it. Here: a `core->config == NULL` fail-loud guard was added to `xce_sched_core_save_snapshot()` believing NULL config caused the SnapshotData crash — it didn't (the generated `encode_Conf4Sched_ptr()` already null-checks and safely emits `capn_null`); the real crash was the internalState `Data`-field UB above. The guard, though "defensively reasonable" in isolation, broke 8 other test fixtures across the suite that never set up a config (a much bigger regression than the 1 crash it prevented, which wasn't actually preventable via that path anyway) — reverted cleanly once the real root cause was found. Lesson: verify a defensive fix against the ACTUAL proven root cause (gdb backtrace + field values), not against a plausible-sounding first theory, before letting it ship. + +## Do-Not-Repeat (2026-08-07, bug-1506/1507, scheduler-core task 12.2) — two makeString() gotchas hit building repl_break.c's breakpoint storage, both caught by this task's own RED-phase test run (not by inspection): (1) makeString() (lib/xstring.c) silently returns NULL rather than crashing when its backing xstr_table hasn't been initialized via xstring_table_init() (only ever called transitively by xce_sched_core_init(), core.c) — a test fixture that builds a bare xce_sched_sim_driver_t via memset(0) and drives repl_dispatch()'s `break` command WITHOUT first calling xce_sched_core_init(&sim.core) gets silently-NULL breakpoint token/filter fields instead of a loud failure (bug-1506). Always call xce_sched_core_init() first in any repl.c-family test fixture, even one that never touches core state otherwise — it's the same precondition the real driver flow always satisfies (sched_sim_driver_start() runs before xce_repl_start()). (2) Confirms and extends bug-1503's existing "makeString() is not strdup(), never free() its result directly" lesson: this task's own repl_breakpoint_free() called a bare FREEUP()/free() on a makeString()-produced xstring, producing "free(): invalid pointer" (glibc heap-corruption abort). The correct precedent for a field that is NEVER meant to be individually released is what kill_sched_job()/kill_sched_queue() (schedobj.c) already do: don't free it at all — makeString()'d fields on scheduler-core objects live for the process's lifetime, matching this whole test suite's `detect_leaks=0`-by-design convention. Only call free() on a makeString() result if you can point to the SPECIFIC rtti-aware teardown path that produced it (bug-1503's own narrower case); when in doubt, follow kill_sched_job()/kill_sched_queue()'s "never free it" default instead. diff --git a/.wolf/memory.md b/.wolf/memory.md index 11e83d53..8b3cc6e6 100644 --- a/.wolf/memory.md +++ b/.wolf/memory.md @@ -2,6 +2,10 @@ > Chronological action log. Hooks and AI append to this file automatically. +| 00:45 | scheduler-core task 11.4: decision verification. ReplaySession branch in sched_sim_driver_run() now decode_ReplaySession_ptr()s the full session (decisions list) before capn_free, drains alongside di index, calls sim_decision_eq()/sim_log_mismatch() (new sim_verify.c, split out to respect 800-line cap) when sim->verify set, reports length-mismatch tail too, frees via free_ReplaySession_ptr(). --sim-verify CLI wiring confirmed already complete (main.c->sim_args.verify->sim->verify), no gap. Fixed a bug-1493-pattern accidental `*/` inside a doc comment (premature block-comment close) caught by first build attempt. TDD: temporarily neutered sim_decision_eq to always-match, confirmed RED (9/14 tests fail incl. the altered-copy Observable), reverted to GREEN | src/scheduler/src/sim_verify.c (new), src/scheduler/src/sim_loop.c, src/scheduler/testsuite/test_sim_verify.c (new), src/scheduler/testsuite/test_sim_event_loop.c, src/scheduler/meson.build, src/scheduler/testsuite/meson.build | 30/30 scheduler suite OK, full `ninja -C xce.build` clean (incl. xceadmin pytest), clang-format clean | ~140k | + +| 09:05 | scheduler-core task 2.5: verified sched_job_picker() (scheduler.c, post-task-2.4) already contains zero project/queue/job traversal (only picker_iface calls + job_elect_fn delegation); sched_update/sched_allocator/sched_account_jobs/sched_end_session GC all confirmed unrelated (event dispatch/accounting/GC, not ordering); added source-text regression guard test_picker_core_loop_boundary.c (fopen+strstr brace-extraction of sched_job_picker's body, checks forbidden traversal symbols absent + required picker_iface/job_elect_fn calls present) since scheduler.c can't be tractably whole-file-included per task 2.4's own test_picker_wiring.c rationale; RED demonstrated by temporarily injecting `sched->jobs` into real scheduler.c (reverted, zero diff after), then GREEN | src/scheduler/testsuite/test_picker_core_loop_boundary.c (new), src/scheduler/testsuite/meson.build | 12/12 scheduler suite tests OK, full ninja build clean, clang-format clean | ~85k | + | 04:09 | Created brief.md and requirements.md for scheduler-preemption spec, extracting Req 6 from scheduler-framework/requirements.md | .kiro/specs/scheduler-preemption/brief.md, .kiro/specs/scheduler-preemption/requirements.md | created both files successfully | ~3500 > Old sessions are consolidated by the daemon weekly. | 21:32 | full ASAN rebuild (`xce-build.sh all asan`) to pick up bug-1474 fix (commit c0b0c7d) + produce xceadmin binary; hit bug-1439 ninja parallel-build race TWICE (1st: capnp.h missing for hook.c.o, 2nd: NEW variant -lxce missing for apiserver's Go/CGO link), 3rd invocation succeeded clean | src/xce.build/controller/{xce_controller,xce_test}, src/xce.build/xceadmin/.venv/bin/xceadmin, src/xce.build/frontend/{apiserver/xce_apiserver,xcecli/xcecli} | PASS: meson Ok:9 Fail:0, scheduler 15/15, apiserver+xcecli Go tests ok, xceadmin pytest 166 passed; bug-1474 WARN string confirmed present in binary | ~95k | @@ -7295,3 +7299,1135 @@ | 20:20 | Session end: 16 writes across 7 files (jul27-aug02-2026.md, ha.c, test_ha_prevote.c, test_term_adoption_vectors.c, meson.build) | 13 reads | ~276916 tok | | 21:32 | Session end: 16 writes across 7 files (jul27-aug02-2026.md, ha.c, test_ha_prevote.c, test_term_adoption_vectors.c, meson.build) | 13 reads | ~276916 tok | | 21:34 | Session end: 16 writes across 7 files (jul27-aug02-2026.md, ha.c, test_ha_prevote.c, test_term_adoption_vectors.c, meson.build) | 13 reads | ~276916 tok | +| 21:37 | Session end: 16 writes across 7 files (jul27-aug02-2026.md, ha.c, test_ha_prevote.c, test_term_adoption_vectors.c, meson.build) | 13 reads | ~276916 tok | + +## Session: 2026-08-03 19:48 + +| Time | Action | File(s) | Outcome | ~Tokens | +|------|--------|---------|---------|--------| + +## Session: 2026-08-03 19:52 + +| Time | Action | File(s) | Outcome | ~Tokens | +|------|--------|---------|---------|--------| +| 20:00 | Edited .kiro/specs/scheduler-core/brief.md | inline fix | ~101 | +| 20:00 | Edited .kiro/specs/scheduler-core/brief.md | inline fix | ~26 | +| 20:00 | Edited .kiro/specs/scheduler-core/brief.md | inline fix | ~16 | +| 20:00 | Edited .kiro/specs/scheduler-core/brief.md | inline fix | ~22 | +| 20:00 | Edited .kiro/specs/scheduler-core/requirements.md | modified of() | ~473 | +| 20:00 | Edited .kiro/specs/scheduler-core/requirements.md | 2→3 lines | ~98 | +| 20:01 | Edited .kiro/specs/scheduler-core/requirements.md | ".replay" → "OPCode_schedAdmin" | ~113 | +| 20:01 | Edited .kiro/specs/scheduler-core/requirements.md | 1→4 lines | ~94 | +| 20:01 | Edited .kiro/specs/scheduler-core/requirements.md | expanded (+9 lines) | ~552 | +| 20:01 | Edited .kiro/specs/scheduler-core/requirements.md | 3→4 lines | ~93 | +| 20:02 | Edited .kiro/specs/scheduler-core/requirements.md | 9→11 lines | ~165 | +| 20:04 | Session end: 11 writes across 2 files (brief.md, requirements.md) | 3 reads | ~29214 tok | +| 20:21 | Edited .kiro/specs/scheduler-core/requirements.md | expanded (+6 lines) | ~425 | +| 20:22 | Created .kiro/specs/scheduler-core/research.md | — | ~3902 | +| 20:26 | Created .kiro/specs/scheduler-core/design.md | — | ~14376 | +| 20:27 | Edited .kiro/specs/scheduler-core/design.md | 22→22 lines | ~220 | +| 20:28 | Edited .kiro/specs/scheduler-core/design.md | 14→14 lines | ~165 | +| 20:28 | Edited .kiro/specs/scheduler-core/spec.json | 14→14 lines | ~119 | +| 20:28 | Session end: 17 writes across 5 files (brief.md, requirements.md, research.md, design.md, spec.json) | 15 reads | ~97687 tok | +| 20:35 | Edited .kiro/specs/scheduler-core/design.md | inline fix | ~94 | +| 20:36 | Edited .kiro/specs/scheduler-core/design.md | inline fix | ~79 | +| 20:36 | Edited .kiro/specs/scheduler-core/design.md | inline fix | ~148 | +| 20:36 | Edited .kiro/specs/scheduler-core/design.md | inline fix | ~94 | +| 20:36 | Edited .kiro/specs/scheduler-core/research.md | 12→13 lines | ~581 | +| 20:37 | Edited .kiro/steering/roadmap.md | 2→2 lines | ~744 | +| 20:37 | Edited .kiro/specs/scheduler-core/design.md | inline fix | ~152 | +| 20:38 | Session end: 24 writes across 6 files (brief.md, requirements.md, research.md, design.md, spec.json) | 17 reads | ~115169 tok | +| 20:43 | Session end: 24 writes across 6 files (brief.md, requirements.md, research.md, design.md, spec.json) | 19 reads | ~117688 tok | +| 20:47 | Edited .kiro/specs/scheduler-core/design.md | inline fix | ~164 | +| 20:47 | Edited .kiro/specs/scheduler-core/design.md | 1→2 lines | ~181 | +| 20:48 | Edited .kiro/specs/scheduler-core/design.md | 1→2 lines | ~51 | +| 20:48 | Edited .kiro/specs/scheduler-core/design.md | inline fix | ~148 | +| 20:48 | Edited .kiro/specs/scheduler-core/design.md | modified case() | ~309 | +| 20:49 | Edited .kiro/specs/scheduler-core/design.md | 1→2 lines | ~218 | +| 20:49 | Edited .kiro/specs/scheduler-core/research.md | expanded (+12 lines) | ~900 | +| 20:49 | Session end: 31 writes across 6 files (brief.md, requirements.md, research.md, design.md, spec.json) | 19 reads | ~120450 tok | +| 20:52 | Edited .kiro/specs/scheduler-core/design.md | inline fix | ~52 | +| 20:52 | Edited .kiro/specs/scheduler-core/design.md | modified decision() | ~248 | +| 20:53 | Edited .kiro/specs/scheduler-core/design.md | expanded (+28 lines) | ~778 | +| 20:54 | Edited .kiro/specs/scheduler-core/design.md | 2→2 lines | ~74 | +| 20:54 | Edited .kiro/specs/scheduler-core/design.md | inline fix | ~13 | +| 20:54 | Edited .kiro/specs/scheduler-core/design.md | inline fix | ~18 | +| 20:55 | Edited .kiro/specs/scheduler-core/design.md | inline fix | ~63 | +| 20:55 | Edited .kiro/specs/scheduler-core/design.md | inline fix | ~27 | +| 20:56 | Session end: 39 writes across 6 files (brief.md, requirements.md, research.md, design.md, spec.json) | 19 reads | ~122044 tok | +| 21:01 | Edited .kiro/specs/scheduler-core/design.md | modified mapname() | ~243 | +| 21:02 | Edited .kiro/specs/scheduler-core/design.md | inline fix | ~71 | +| 21:02 | Edited .kiro/specs/scheduler-core/design.md | 2→5 lines | ~85 | +| 21:02 | Edited .kiro/specs/scheduler-core/design.md | "controller/testsuite/" → "test_sched_admin.c" | ~64 | +| 21:02 | Edited .kiro/specs/scheduler-core/design.md | inline fix | ~67 | +| 21:03 | Edited .kiro/specs/scheduler-core/design.md | 1→2 lines | ~55 | +| 21:03 | Edited .kiro/specs/scheduler-core/design.md | inline fix | ~86 | +| 21:04 | Edited .kiro/specs/scheduler-core/design.md | modified compat() | ~76 | +| 21:05 | Session end: 47 writes across 6 files (brief.md, requirements.md, research.md, design.md, spec.json) | 19 reads | ~123788 tok | +| 21:10 | Created .kiro/specs/scheduler-core/tasks.md | — | ~7467 | +| 21:11 | Edited .kiro/specs/scheduler-core/tasks.md | 7→5 lines | ~86 | +| 21:11 | Edited .kiro/specs/scheduler-core/tasks.md | 7→5 lines | ~87 | +| 21:11 | Edited .kiro/specs/scheduler-core/tasks.md | 7→5 lines | ~123 | +| 21:13 | Edited .kiro/specs/scheduler-core/tasks.md | modified not() | ~598 | +| 21:13 | Edited .kiro/specs/scheduler-core/tasks.md | modified returns() | ~300 | +| 21:14 | Edited .kiro/specs/scheduler-core/tasks.md | 2→2 lines | ~12 | +| 21:15 | Edited .kiro/specs/scheduler-core/tasks.md | 2→2 lines | ~11 | +| 21:23 | Edited .kiro/specs/scheduler-core/tasks.md | modified recording() | ~962 | +| 21:24 | Edited .kiro/specs/scheduler-core/tasks.md | 8→8 lines | ~115 | +| 21:24 | Edited .kiro/specs/scheduler-core/tasks.md | 33→34 lines | ~723 | +| 21:25 | Edited .kiro/specs/scheduler-core/tasks.md | 7→8 lines | ~255 | +| 21:31 | Edited .kiro/specs/scheduler-core/tasks.md | 6→7 lines | ~302 | +| 21:32 | Edited .kiro/specs/scheduler-core/spec.json | 20→20 lines | ~150 | +| 21:32 | Session end: 61 writes across 7 files (brief.md, requirements.md, research.md, design.md, spec.json) | 23 reads | ~147054 tok | +| 21:39 | Edited src/scheduler/include/plugin.h | modified above() | ~1801 | +| 21:40 | Created src/scheduler/testsuite/test_picker_iface.c | — | ~2106 | +| 21:40 | Edited src/scheduler/testsuite/meson.build | expanded (+12 lines) | ~323 | +| 21:40 | Edited src/scheduler/testsuite/meson.build | 2→4 lines | ~70 | +| 21:49 | Edited .kiro/specs/scheduler-core/tasks.md | inline fix | ~13 | +| 22:02 | Edited src/scheduler/include/plugin.h | modified struct() | ~522 | +| 22:03 | Edited src/scheduler/include/plugin.h | modified struct() | ~26 | +| 22:03 | Edited src/scheduler/src/plugin.c | added 2 condition(s) | ~1519 | +| 22:05 | Created src/scheduler/testsuite/test_plugin_picker_registration.c | — | ~2127 | +| 22:05 | Edited src/scheduler/testsuite/meson.build | modified compatibility() | ~605 | +| 22:09 | Edited src/scheduler/testsuite/test_plugin_picker_registration.c | modified sched_fill_limit_requests() | ~176 | +| 22:23 | Edited .kiro/specs/scheduler-core/tasks.md | inline fix | ~22 | +| 22:30 | Edited src/scheduler/include/scheduler.h | modified 1() | ~160 | +| 22:31 | Edited src/scheduler/include/scheduler.h | modified 1() | ~96 | +| 22:31 | Edited src/scheduler/include/sched/types.h | modified struct() | ~339 | +| 22:31 | Edited src/scheduler/src/schedobj.c | modified 3() | ~196 | +| 22:31 | Edited src/scheduler/src/schedobj.c | added 2 condition(s) | ~425 | +| 22:35 | Created src/scheduler/testsuite/test_picker_config_fields.c | — | ~3190 | +| 22:36 | Edited src/scheduler/testsuite/meson.build | expanded (+12 lines) | ~324 | +| 22:36 | Edited src/scheduler/testsuite/meson.build | 2→4 lines | ~74 | +| 22:36 | Edited src/scheduler/testsuite/test_picker_config_fields.c | modified sched_get_queue() | ~57 | +| 22:41 | scheduler-core task 1.3: added job_pick_plugin (xce_scheduler_t), project_name+job_pick_plugin (xce_sched_queue_t), priority (xce_sched_project_t), sched_set_default_job_pick_plugin() helper, new white-box test (10 cases) | src/scheduler/include/scheduler.h, src/scheduler/include/sched/types.h, src/scheduler/src/schedobj.c, src/scheduler/testsuite/test_picker_config_fields.c, src/scheduler/testsuite/meson.build | RED confirmed (compile fails without fields) then GREEN: 8/8 scheduler suite passes | ~45000 | +| 22:45 | Edited .kiro/specs/scheduler-core/tasks.md | inline fix | ~12 | +| 22:45 | Edited .kiro/specs/scheduler-core/tasks.md | expanded (+7 lines) | ~435 | +| 22:53 | Created src/scheduler/src/plugins/builtin_fcfs.c | — | ~2409 | +| 22:54 | Created src/scheduler/testsuite/test_picker_fcfs.c | — | ~2267 | +| 22:54 | Edited src/scheduler/testsuite/meson.build | expanded (+11 lines) | ~204 | +| 22:54 | Edited src/scheduler/testsuite/meson.build | 2→4 lines | ~70 | +| 22:55 | Edited src/scheduler/testsuite/test_picker_fcfs.c | expanded (+7 lines) | ~191 | +| 22:55 | Edited src/scheduler/testsuite/test_picker_fcfs.c | 5→7 lines | ~54 | +| 22:58 | scheduler-core task 2.1: builtin_fcfs lifecycle (init/fini) + private fcfs_plugin_data_t/fcfs_queue_t/fcfs_job_t state, new file (fcfs.c untouched, still live) | src/scheduler/src/plugins/builtin_fcfs.c, src/scheduler/testsuite/test_picker_fcfs.c, src/scheduler/testsuite/meson.build | RED confirmed (missing-file compile error) then GREEN: 9/9 scheduler suite passes; leak-detection-ON run (ASAN_OPTIONS=detect_leaks=1) exit 0, zero LeakSanitizer output | ~15000 | +| 23:03 | Edited .kiro/specs/scheduler-core/tasks.md | inline fix | ~16 | +| 23:04 | Session end: 91 writes across 18 files (brief.md, requirements.md, research.md, design.md, spec.json) | 46 reads | ~216828 tok | +| 23:38 | Edited src/scheduler/src/plugins/builtin_fcfs.c | 5→6 lines | ~26 | +| 23:39 | Edited src/scheduler/src/plugins/builtin_fcfs.c | added 26 condition(s) | ~2851 | +| 23:41 | Edited src/scheduler/testsuite/test_picker_fcfs.c | modified collect_queue_names() | ~6281 | +| 23:50 | Task 2.2 done: builtin_fcfs on_queue_update/on_job_push/on_job_state_change/on_job_update hooks (RED via temp revert+rebuild, then GREEN) | src/scheduler/src/plugins/builtin_fcfs.c, src/scheduler/testsuite/test_picker_fcfs.c | 21/21 tests pass, scheduler suite 9/9 | ~1200 | + +## Session: 2026-08-03 23:48 + +| Time | Action | File(s) | Outcome | ~Tokens | +|------|--------|---------|---------|--------| +| 23:55 | Edited .kiro/specs/scheduler-core/tasks.md | inline fix | ~14 | +| 00:00 | Edited src/scheduler/src/plugins/builtin_fcfs.c | 4→5 lines | ~28 | +| 00:01 | Edited src/scheduler/src/plugins/builtin_fcfs.c | modified struct() | ~308 | +| 00:01 | Edited src/scheduler/src/plugins/builtin_fcfs.c | 3→4 lines | ~27 | +| 00:02 | Edited src/scheduler/src/plugins/builtin_fcfs.c | added 10 condition(s) | ~2074 | +| 00:03 | Created src/scheduler/testsuite/test_picker_fcfs_session.c | — | ~4774 | +| 00:04 | Edited src/scheduler/testsuite/meson.build | expanded (+14 lines) | ~365 | +| 00:04 | Edited src/scheduler/testsuite/meson.build | 2→4 lines | ~70 | +| 00:06 | Edited src/scheduler/src/plugins/builtin_fcfs.c | added 1 condition(s) | ~183 | +| 00:06 | Edited src/scheduler/src/plugins/builtin_fcfs.c | added 1 condition(s) | ~164 | +| 00:07 | Edited src/scheduler/testsuite/test_picker_fcfs.c | modified sched_queue_is_ready() | ~231 | +| 00:10 | Task 2.3: implemented builtin_fcfs session hooks (begin_session/next_job/end_session) and post-allocation hooks (on_dispatch/on_pend) via cursor+job_cursor kept in lockstep by fcfs_cursor_enter_queue(); split new tests into test_picker_fcfs_session.c (766+786+396 lines, avoids >800 ceiling) | src/scheduler/src/plugins/builtin_fcfs.c, src/scheduler/testsuite/test_picker_fcfs.c, src/scheduler/testsuite/test_picker_fcfs_session.c, src/scheduler/testsuite/meson.build | RED confirmed via git-stash then GREEN; 10/10 scheduler suite pass, ASAN leak-check clean | ~9000 | +| 00:18 | Edited .kiro/specs/scheduler-core/tasks.md | 1→2 lines | ~279 | +| 00:18 | Edited .kiro/specs/scheduler-core/tasks.md | inline fix | ~18 | +| 00:37 | Edited src/scheduler/include/plugin.h | 4→5 lines | ~54 | +| 00:38 | Edited src/scheduler/include/scheduler.h | modified 1() | ~342 | +| 00:38 | Edited src/scheduler/src/schedobj.c | added 4 condition(s) | ~503 | +| 00:38 | Edited src/scheduler/src/plugins/builtin_fcfs.c | modified Concurrency() | ~960 | +| 00:38 | Edited src/scheduler/src/plugins/builtin_fcfs.c | modified fix() | ~489 | +| 00:39 | Edited src/scheduler/src/plugins/builtin_fcfs.c | modified if() | ~144 | + +## Session: 2026-08-03 00:56 + +| Time | Action | File(s) | Outcome | ~Tokens | +|------|--------|---------|---------|--------| +| 01:16 | Edited src/scheduler/src/plugins/builtin_fcfs.c | 8→12 lines | ~197 | +| 01:17 | Edited src/scheduler/src/plugins/builtin_fcfs.c | modified if() | ~299 | +| 01:17 | Edited src/scheduler/src/plugins/builtin_fcfs.c | modified builtin_fcfs_on_job_push() | ~378 | +| 01:17 | Edited src/scheduler/src/plugins/builtin_fcfs.c | modified builtin_fcfs_on_job_state_change() | ~381 | +| 01:17 | Edited src/scheduler/src/plugins/builtin_fcfs.c | modified builtin_fcfs_on_job_update() | ~337 | +| 01:17 | Edited src/scheduler/src/plugins/builtin_fcfs.c | modified if() | ~156 | +| 01:18 | Edited src/scheduler/src/plugins/builtin_fcfs.c | 8→12 lines | ~198 | +| 01:18 | Edited src/scheduler/src/plugins/builtin_fcfs.c | added 1 condition(s) | ~865 | +| 01:18 | Edited src/scheduler/src/plugins/builtin_fcfs.c | modified builtin_fcfs_on_dispatch() | ~194 | +| 01:19 | Edited src/scheduler/src/plugins/builtin_fcfs.c | added 2 condition(s) | ~1326 | +| 01:19 | Edited src/scheduler/src/scheduler.c | modified sched_register_plugins() | ~153 | +| 01:19 | Edited src/scheduler/src/scheduler.c | added 1 condition(s) | ~339 | +| 01:19 | Edited src/scheduler/src/scheduler.c | modified 5() | ~160 | +| 01:20 | Edited src/scheduler/src/scheduler.c | added 1 condition(s) | ~1017 | +| 01:20 | Edited src/scheduler/src/scheduler.c | added 1 condition(s) | ~968 | +| 01:21 | Edited src/scheduler/meson.build | 2→3 lines | ~25 | +| 01:22 | Edited src/scheduler/testsuite/test_picker_fcfs.c | modified sched_job_is_ready() | ~208 | +| 01:22 | Edited src/scheduler/testsuite/test_picker_fcfs_session.c | modified sched_job_is_ready() | ~156 | +| 01:25 | Created src/scheduler/testsuite/test_picker_wiring.c | — | ~5179 | +| 01:26 | Edited src/scheduler/testsuite/meson.build | expanded (+16 lines) | ~370 | +| 01:26 | Edited src/scheduler/testsuite/meson.build | 2→4 lines | ~71 | +| 01:50 | Edited .kiro/specs/scheduler-core/tasks.md | inline fix | ~25 | +| 01:51 | Edited .kiro/specs/scheduler-core/tasks.md | modified fixed() | ~746 | +| 02:01 | Created src/scheduler/testsuite/test_picker_core_loop_boundary.c | — | ~2467 | +| 02:01 | Edited src/scheduler/testsuite/meson.build | modified boundary() | ~411 | +| 02:01 | Edited src/scheduler/testsuite/meson.build | 2→5 lines | ~75 | +| 02:02 | Edited src/scheduler/testsuite/test_picker_core_loop_boundary.c | modified setUp() | ~26 | +| 02:02 | Edited src/scheduler/src/scheduler.c | added 1 condition(s) | ~109 | +| 02:03 | Edited src/scheduler/src/scheduler.c | modified while() | ~28 | +| 02:09 | Edited src/scheduler/src/scheduler.c | added 1 condition(s) | ~47 | +| 02:11 | Edited .kiro/specs/scheduler-core/tasks.md | inline fix | ~17 | +| 02:23 | Created src/scheduler/testsuite/test_picker_pendreason_factory_audit.c | — | ~2396 | +| 02:23 | Edited src/scheduler/testsuite/meson.build | modified 2() | ~383 | +| 02:23 | Edited src/scheduler/testsuite/meson.build | 3→6 lines | ~82 | +| 02:23 | Edited src/scheduler/testsuite/test_picker_pendreason_factory_audit.c | 4→4 lines | ~30 | +| 02:35 | Edited .kiro/specs/scheduler-core/tasks.md | inline fix | ~18 | + +## Session: 2026-08-03 02:44 + +| Time | Action | File(s) | Outcome | ~Tokens | +|------|--------|---------|---------|--------| +| 02:52 | Edited .kiro/specs/scheduler-core/tasks.md | 5→6 lines | ~125 | +| 02:52 | scheduler-core task 3: 15-round sequential HA soak vs builtin_fcfs picker rework (tasks 2.1-2.6) | .claude/skills/xce-test-ha/run-ha-soak.sh (driver, unmodified) | PASS: 15 rounds, 48 jobs, zero job loss, all 3 nodes led (ctrl-1/ctrl-2/ctrl-3); no HA regression from Phase 1 picker changes; task 3 marked [x] | ~15k | +| 02:52 | Session end: 1 writes across 1 files (tasks.md) | 1 reads | ~8736 tok | +| 03:00 | Session end: 1 writes across 1 files (tasks.md) | 4 reads | ~50914 tok | +| 03:11 | Created src/scheduler/include/core.h | — | ~1394 | +| 03:15 | Created src/scheduler/src/core.c | — | ~2624 | +| 03:17 | Edited src/scheduler/src/core.c | 10→11 lines | ~54 | +| 03:17 | Edited src/scheduler/include/scheduler.h | 11→12 lines | ~65 | +| 03:17 | Edited src/scheduler/include/scheduler.h | modified struct() | ~246 | +| 03:17 | Edited src/scheduler/include/scheduler.h | reduced (-7 lines) | ~214 | +| 03:20 | Created src/scheduler/src/scheduler.c | — | ~11268 | +| 03:21 | Session end: 8 writes across 5 files (tasks.md, core.h, core.c, scheduler.h, scheduler.c) | 28 reads | ~132309 tok | +| 03:21 | Created src/scheduler/src/schedobj.c | — | ~4795 | +| 03:21 | Edited src/scheduler/src/resource.c | inline fix | ~16 | +| 03:21 | Edited src/scheduler/src/resource.c | modified if() | ~119 | +| 03:22 | Edited src/scheduler/src/resource.c | modified if() | ~77 | +| 03:22 | Edited src/scheduler/src/resource.c | modified if() | ~227 | +| 03:22 | Edited src/scheduler/src/resource.c | inline fix | ~22 | +| 03:22 | Edited src/scheduler/src/resource.c | modified for() | ~127 | +| 03:22 | Edited src/scheduler/src/handler.c | modified enq_pend_update() | ~71 | +| 03:22 | Edited src/scheduler/src/handler.c | inline fix | ~16 | +| 03:22 | Edited src/scheduler/src/plugin.c | inline fix | ~6 | +| 03:22 | Edited src/scheduler/src/main.c | 2→2 lines | ~22 | +| 03:22 | Edited src/scheduler/src/main.c | inline fix | ~16 | +| 03:23 | Edited src/scheduler/src/main.c | inline fix | ~8 | +| 03:23 | Edited src/scheduler/src/plugins/fcfs.c | inline fix | ~12 | +| 03:23 | Edited src/scheduler/src/plugins/fcfs.c | inline fix | ~16 | +| 03:23 | Edited src/scheduler/src/plugins/fcfs.c | inline fix | ~20 | +| 03:23 | Edited src/scheduler/src/plugins/fcfs.c | 3→3 lines | ~18 | +| 03:24 | Edited src/scheduler/src/plugins/limit.c | 2→2 lines | ~41 | +| 03:24 | Edited src/scheduler/src/plugins/limit.c | 3→3 lines | ~24 | +| 03:24 | Edited src/scheduler/src/plugins/limit.c | modified if() | ~130 | +| 03:25 | Edited src/scheduler/src/plugins/limit.c | modified if() | ~20 | +| 03:25 | Edited src/scheduler/src/plugins/limit.c | inline fix | ~12 | +| 03:26 | Edited src/scheduler/src/plugins/limit.c | 3→3 lines | ~24 | +| 03:26 | Edited src/scheduler/src/plugins/limit.c | modified if() | ~83 | +| 03:26 | Edited src/scheduler/src/plugins/seqjob_allocator.c | inline fix | ~12 | +| 03:26 | Edited src/scheduler/src/plugins/seqjob_allocator.c | 11→11 lines | ~137 | +| 03:26 | Edited src/scheduler/src/plugins/seqjob_allocator.c | inline fix | ~20 | +| 03:27 | Edited src/scheduler/src/plugins/parjob_allocator.c | inline fix | ~12 | +| 03:27 | Edited src/scheduler/meson.build | 14→15 lines | ~87 | +| 03:27 | Edited src/scheduler/meson.build | modified 1() | ~284 | +| 03:29 | Created src/scheduler/testsuite/test_core_isolation.c | — | ~2919 | +| 03:29 | Edited src/scheduler/testsuite/meson.build | modified Observable() | ~478 | +| 03:29 | Edited src/scheduler/testsuite/meson.build | 3→6 lines | ~76 | +| 03:30 | Edited src/scheduler/testsuite/test.c | modified sched_get_worker() | ~312 | +| 03:30 | Edited src/scheduler/testsuite/test.c | 7→7 lines | ~77 | +| 03:30 | Edited src/scheduler/testsuite/test.c | 10→10 lines | ~127 | +| 03:30 | Edited src/scheduler/testsuite/test.c | 14→14 lines | ~214 | +| 03:31 | Edited src/scheduler/testsuite/test_handlers.c | modified sched_get_job() | ~68 | +| 03:31 | Edited src/scheduler/testsuite/test_handlers.c | modified fixture_init() | ~100 | +| 03:31 | Edited src/scheduler/testsuite/test_handlers.c | inline fix | ~22 | +| 03:31 | Edited src/scheduler/testsuite/test_job_update.c | modified sched_get_user() | ~698 | +| 03:32 | Edited src/scheduler/testsuite/test_job_update.c | 6→6 lines | ~61 | +| 03:32 | Edited src/scheduler/testsuite/test_job_update.c | 4→4 lines | ~55 | +| 03:32 | Edited src/scheduler/testsuite/test_job_array_expand.c | modified sched_add_job() | ~212 | +| 03:33 | Edited src/scheduler/testsuite/test_job_array_expand.c | modified seed_existing_job() | ~163 | +| 03:33 | Edited src/scheduler/testsuite/test_job_array_expand.c | modified if() | ~121 | +| 03:33 | Edited src/scheduler/testsuite/test_job_array_wire_e2e.c | modified sched_add_job() | ~230 | +| 03:33 | Edited src/scheduler/testsuite/test_job_array_wire_e2e.c | 8→8 lines | ~104 | +| 03:33 | Edited src/scheduler/testsuite/test_job_array_wire_e2e.c | inline fix | ~23 | +| 03:33 | Edited src/scheduler/testsuite/test_job_array_wire_e2e.c | 4→4 lines | ~50 | +| 03:34 | Edited src/scheduler/testsuite/test_picker_config_fields.c | modified sched_get_user() | ~455 | +| 03:35 | Edited src/scheduler/testsuite/test_picker_config_fields.c | modified plugin_job_update() | ~427 | +| 03:36 | Edited src/scheduler/testsuite/test_picker_wiring.c | modified here() | ~323 | +| 03:37 | Edited src/scheduler/testsuite/test_picker_pendreason_factory_audit.c | 18→20 lines | ~137 | +| 03:38 | Edited src/scheduler/include/core.h | modified and() | ~122 | +| 03:38 | Edited src/scheduler/src/schedobj.c | modified reset_sched_obj() | ~61 | +| 03:39 | Edited src/scheduler/src/core.c | 3→4 lines | ~67 | +| 03:42 | Session end: 65 writes across 24 files (tasks.md, core.h, core.c, scheduler.h, scheduler.c) | 31 reads | ~150187 tok | +| 03:43 | Created ../../../../../../tmp/claude-1000/-home-rshen-projects-xce-worktrees-scheduler-framework/af7e2e71-6a3d-487f-8cd6-2b0ae196cb3a/scratchpad/fix_buglog.py | — | ~367 | +| 10:44 | Task 4.1 complete: extracted xce_sched_core_t (core.h/core.c) from xce_scheduler_t, zero controller I/O; relocated 10 CRUD accessors + make_sched_user/project; updated 18 call-site files + 8 testsuite files; new sched_core_isolation_lib + test_core_isolation.c (nm-based link-time proof) | core.h, core.c, scheduler.h, scheduler.c, schedobj.c, resource.c, handler.c, plugin.c, main.c, plugins/*.c, meson.build x2, testsuite/*.c x9 | 14/14 scheduler tests pass, full build green | ~0 | +| 03:47 | Session end: 66 writes across 25 files (tasks.md, core.h, core.c, scheduler.h, scheduler.c) | 32 reads | ~151906 tok | +| 03:55 | Edited .kiro/specs/scheduler-core/tasks.md | 6→7 lines | ~277 | +| 03:56 | Task 4.1 reviewed (independent fresh build/test/nm/RED repro, APPROVED) and committed | 26 files, commit 7c229284 | task 4.1 [x], moving to 4.2 | ~5k | +| 03:57 | Session end: 67 writes across 25 files (tasks.md, core.h, core.c, scheduler.h, scheduler.c) | 34 reads | ~156785 tok | +| 06:12 | Session end: 67 writes across 25 files (tasks.md, core.h, core.c, scheduler.h, scheduler.c) | 38 reads | ~164372 tok | +| 06:18 | Session end: 67 writes across 25 files (tasks.md, core.h, core.c, scheduler.h, scheduler.c) | 38 reads | ~164372 tok | +| 06:28 | Edited src/scheduler/src/core.c | 4→3 lines | ~35 | +| 06:28 | Edited src/scheduler/src/core.c | modified 7() | ~318 | +| 06:29 | Created src/scheduler/testsuite/test_core_jbuf_capacity_pin.c | — | ~2572 | +| 06:30 | Edited src/scheduler/testsuite/meson.build | modified half() | ~481 | +| 06:30 | Edited src/scheduler/testsuite/meson.build | 3→6 lines | ~75 | +| 06:32 | Edited src/scheduler/testsuite/test_core_jbuf_capacity_pin.c | modified test_jbuf_capacity_is_runtime_one() | ~217 | +| 06:33 | Edited src/scheduler/src/core.c | 5→5 lines | ~83 | +| 06:37 | Session end: 74 writes across 26 files (tasks.md, core.h, core.c, scheduler.h, scheduler.c) | 40 reads | ~171637 tok | +| 06:42 | Session end: 74 writes across 26 files (tasks.md, core.h, core.c, scheduler.h, scheduler.c) | 40 reads | ~171785 tok | +| 06:56 | Edited src/scheduler/src/core.c | modified of() | ~863 | +| 06:59 | Session end: 75 writes across 26 files (tasks.md, core.h, core.c, scheduler.h, scheduler.c) | 40 reads | ~174395 tok | +| 07:08 | Edited .kiro/specs/scheduler-core/tasks.md | modified fixed() | ~395 | +| 07:09 | Edited .kiro/specs/scheduler-core/tasks.md | 5→6 lines | ~331 | +| 07:14 | Session end: 77 writes across 26 files (tasks.md, core.h, core.c, scheduler.h, scheduler.c) | 40 reads | ~175434 tok | +| 07:35 | Edited src/scheduler/include/core.h | 5→6 lines | ~34 | +| 07:35 | Session end: 78 writes across 26 files (tasks.md, core.h, core.c, scheduler.h, scheduler.c) | 44 reads | ~192531 tok | +| 07:35 | Edited src/scheduler/include/core.h | modified enum() | ~1339 | +| 07:36 | Edited src/scheduler/src/schedobj.c | modified only() | ~621 | +| 07:36 | Edited src/scheduler/include/scheduler.h | reduced (-10 lines) | ~114 | +| 07:36 | Edited src/scheduler/src/scheduler.c | modified if() | ~107 | +| 07:36 | Edited src/scheduler/src/scheduler.c | modified if() | ~108 | +| 07:36 | Edited src/scheduler/src/scheduler.c | modified if() | ~23 | +| 07:37 | Edited src/scheduler/testsuite/test_picker_wiring.c | inline fix | ~17 | +| 07:43 | Created src/scheduler/testsuite/test_core_events.c | — | ~5855 | +| 07:44 | Edited src/scheduler/testsuite/test_core_events.c | 23→27 lines | ~236 | +| 07:44 | Edited src/scheduler/testsuite/test_core_events.c | 7→7 lines | ~92 | +| 07:44 | Edited src/scheduler/testsuite/meson.build | expanded (+16 lines) | ~307 | +| 07:44 | Edited src/scheduler/testsuite/meson.build | 3→6 lines | ~74 | +| 07:45 | Edited src/scheduler/testsuite/test_core_events.c | inline fix | ~19 | +| 07:45 | Edited src/scheduler/src/core.c | 11→16 lines | ~83 | +| 07:46 | Edited src/scheduler/src/core.c | added 51 condition(s) | ~5565 | +| 07:48 | Edited src/scheduler/testsuite/test_core_jbuf_capacity_pin.c | added 1 condition(s) | ~231 | +| 07:56 | Session end: 94 writes across 27 files (tasks.md, core.h, core.c, scheduler.h, scheduler.c) | 45 reads | ~210298 tok | +| 07:58 | Created src/scheduler/src/core_events.c | — | ~5771 | +| 07:59 | Edited src/scheduler/src/core.c | 16→11 lines | ~54 | +| 08:00 | Edited src/scheduler/meson.build | 3→4 lines | ~22 | +| 08:01 | Edited src/scheduler/testsuite/test_core_jbuf_capacity_pin.c | modified kill_sched_job() | ~92 | +| 08:02 | Edited src/scheduler/testsuite/test_core_events.c | modified reference() | ~223 | +| 08:02 | Edited src/scheduler/testsuite/test_core_events.c | 4→5 lines | ~68 | +| 08:08 | Edited src/scheduler/testsuite/test_picker_wiring.c | 7→7 lines | ~82 | +| 08:16 | Session end: 101 writes across 28 files (tasks.md, core.h, core.c, scheduler.h, scheduler.c) | 46 reads | ~228664 tok | +| 08:29 | Session end: 101 writes across 28 files (tasks.md, core.h, core.c, scheduler.h, scheduler.c) | 47 reads | ~234648 tok | +| 08:33 | Edited src/scheduler/testsuite/test_core_events.c | modified test_config_push_decomposes_queues_resources_and_resvals() | ~879 | +| 08:34 | Edited src/scheduler/testsuite/test_core_events.c | 2→3 lines | ~58 | +| 08:36 | Edited src/scheduler/src/core_events.c | added 10 condition(s) | ~1205 | +| 08:36 | Edited src/scheduler/src/core_events.c | added 1 condition(s) | ~159 | +| 08:36 | Edited src/scheduler/src/core_events.c | reduced (-24 lines) | ~97 | +| 08:37 | Edited src/scheduler/src/core_events.c | expanded (+12 lines) | ~255 | +| 08:38 | Edited src/scheduler/src/core_events.c | modified core_upsert_queue() | ~112 | +| 08:43 | Task 4.3 round-2 fix: CONFIG_PUSH decomposition | core_events.c, test_core_events.c | RED->GREEN, 16/16 scheduler suite green | ~350 | +| 08:46 | Session end: 108 writes across 28 files (tasks.md, core.h, core.c, scheduler.h, scheduler.c) | 47 reads | ~240078 tok | +| 08:54 | Edited src/scheduler/src/core_events.c | modified if() | ~144 | +| 09:04 | Edited .kiro/specs/scheduler-core/tasks.md | 6→7 lines | ~377 | +| 09:04 | Edited .kiro/specs/scheduler-core/tasks.md | 1→2 lines | ~398 | +| 19:17 | Session end: 111 writes across 28 files (tasks.md, core.h, core.c, scheduler.h, scheduler.c) | 47 reads | ~244275 tok | +| 19:29 | Edited src/scheduler/include/decision.h | modified enum() | ~445 | +| 19:29 | Edited src/scheduler/src/decision.c | added 1 condition(s) | ~170 | +| 19:29 | Edited src/scheduler/include/core.h | 11→12 lines | ~62 | +| 19:29 | Edited src/scheduler/include/core.h | modified struct() | ~152 | +| 19:29 | Edited src/scheduler/include/core.h | modified 6() | ~177 | +| 19:29 | Edited src/scheduler/include/core.h | expanded (+51 lines) | ~735 | +| 19:31 | Created src/scheduler/src/core_session.c | — | ~3878 | +| 19:32 | Edited src/scheduler/meson.build | 3→4 lines | ~22 | +| 19:34 | Created src/scheduler/testsuite/test_core_session.c | — | ~3290 | +| 19:34 | Edited src/scheduler/testsuite/meson.build | expanded (+18 lines) | ~327 | +| 19:34 | Edited src/scheduler/testsuite/meson.build | 3→6 lines | ~68 | +| 19:34 | Edited src/scheduler/testsuite/test_picker_pendreason_factory_audit.c | 2→3 lines | ~18 | +| 19:35 | Edited src/scheduler/testsuite/test_core_session.c | 3→3 lines | ~60 | +| 19:37 | Edited src/scheduler/testsuite/test_core_session.c | modified kill_sched_job() | ~218 | +| 19:38 | Session end: 125 writes across 32 files (tasks.md, core.h, core.c, scheduler.h, scheduler.c) | 49 reads | ~258140 tok | +| 19:39 | Edited src/scheduler/include/core.h | 2→2 lines | ~21 | +| 02:42 | Task 4.4 complete: xce_core_go/xce_core_end_session/xce_core_set_decision_cb (core_session.c, new) + XCE_DECISION_PREEMPT (decision.h/.c) + 8-test contract suite (test_core_session.c, new); 17/17 scheduler suite green | core.h, core_session.c, decision.h, decision.c, test_core_session.c, meson.build x2, test_picker_pendreason_factory_audit.c | pass | ~12000 | +| 19:50 | Session end: 126 writes across 32 files (tasks.md, core.h, core.c, scheduler.h, scheduler.c) | 49 reads | ~258163 tok | +| 20:08 | Edited .kiro/specs/scheduler-core/tasks.md | 6→7 lines | ~434 | +| 20:11 | Session end: 127 writes across 32 files (tasks.md, core.h, core.c, scheduler.h, scheduler.c) | 50 reads | ~267980 tok | +| 20:20 | Created src/scheduler/testsuite/test_clock.c | — | ~4903 | +| 20:21 | Edited src/scheduler/testsuite/test_clock.c | 3→2 lines | ~37 | +| 20:21 | Edited src/scheduler/meson.build | 4→5 lines | ~28 | +| 20:21 | Edited src/scheduler/testsuite/meson.build | modified live() | ~314 | +| 20:21 | Edited src/scheduler/testsuite/meson.build | 3→6 lines | ~68 | +| 20:22 | Edited src/scheduler/include/core.h | modified virtualization() | ~848 | +| 20:22 | Edited src/scheduler/include/core.h | modified integration() | ~361 | +| 20:22 | Edited src/scheduler/include/core.h | modified reads() | ~538 | +| 20:23 | Created src/scheduler/src/core_clock.c | — | ~2616 | +| 20:23 | Edited src/scheduler/src/core.c | modified 3() | ~230 | +| 20:23 | Edited src/scheduler/src/core_session.c | modified 12() | ~98 | +| 20:23 | Edited src/scheduler/src/core_session.c | modified 12() | ~131 | +| 20:23 | Edited src/scheduler/testsuite/test_core_session.c | modified 5() | ~189 | +| 20:23 | Edited src/scheduler/testsuite/test_core_session.c | 3→4 lines | ~32 | +| 20:32 | Task 4.5 complete: xce_core_now() two-anchor+speed formula in new core_clock.c (isolated from core.c to preserve test_core_isolation.c wall-clock guarantee); tlog format-func integration via module-level active-core pointer (resolves tlog_reg_format_func(fn,core_ptr) spec-vs-real-API mismatch); session_start/session_duration_ns in core_session.c; 18/18 scheduler suite green (was 17/17) | core.h,core.c,core_clock.c(new),core_session.c,test_clock.c(new),test_core_session.c,meson.build×2 | success | ~high | +| 20:29 | Session end: 141 writes across 34 files (tasks.md, core.h, core.c, scheduler.h, scheduler.c) | 51 reads | ~282406 tok | +| 20:38 | Edited .kiro/specs/scheduler-core/tasks.md | 6→7 lines | ~565 | +| 20:38 | Edited .kiro/specs/scheduler-core/tasks.md | 1→2 lines | ~386 | +| 20:40 | Session end: 143 writes across 34 files (tasks.md, core.h, core.c, scheduler.h, scheduler.c) | 54 reads | ~291570 tok | +| 20:50 | Created src/scheduler/testsuite/test_core_full_cycle.c | — | ~7655 | +| 20:50 | Edited src/scheduler/testsuite/meson.build | expanded (+20 lines) | ~363 | +| 20:51 | Edited src/scheduler/testsuite/meson.build | 3→6 lines | ~70 | +| 20:51 | Edited src/scheduler/testsuite/test_core_full_cycle.c | 2→2 lines | ~42 | +| 20:52 | Edited src/scheduler/src/core_session.c | modified if() | ~32 | +| 20:53 | Edited src/scheduler/src/core_session.c | 5→6 lines | ~54 | +| 20:53 | Edited src/scheduler/src/core_events.c | modified if() | ~11 | +| 20:53 | Edited src/scheduler/src/core_events.c | 3→3 lines | ~22 | +| 20:55 | Task 4.6 complete: full push/go/drain/end contract test, 19/19 scheduler suite green, RED/GREEN proven for AC5+AC8, 30x flake-free | src/scheduler/testsuite/test_core_full_cycle.c, meson.build | READY_FOR_REVIEW | ~200 | +| 20:57 | Session end: 151 writes across 35 files (tasks.md, core.h, core.c, scheduler.h, scheduler.c) | 55 reads | ~307243 tok | +| 21:01 | Edited src/scheduler/src/core_session.c | modified if() | ~80 | +| 21:01 | Edited src/scheduler/src/core_session.c | 7→8 lines | ~101 | +| 21:01 | Edited src/scheduler/src/core_events.c | modified if() | ~11 | +| 21:02 | Edited src/scheduler/src/core_events.c | 3→3 lines | ~22 | +| 21:05 | Edited .kiro/specs/scheduler-core/tasks.md | 5→6 lines | ~334 | +| 21:08 | Session end: 156 writes across 35 files (tasks.md, core.h, core.c, scheduler.h, scheduler.c) | 55 reads | ~303368 tok | +| 21:17 | Created src/scheduler/include/driver.h | — | ~771 | +| 21:18 | Created src/scheduler/src/driver.c | — | ~4168 | +| 21:19 | Created src/scheduler/testsuite/test_driver_translate.c | — | ~6575 | +| 21:20 | Edited src/scheduler/meson.build | 2→3 lines | ~16 | +| 21:20 | Edited src/scheduler/testsuite/meson.build | modified path() | ~349 | +| 21:20 | Edited src/scheduler/testsuite/meson.build | 3→6 lines | ~73 | +| 21:21 | Edited src/scheduler/testsuite/test_driver_translate.c | 3→4 lines | ~31 | +| 21:21 | Edited src/scheduler/testsuite/test_driver_translate.c | 2→4 lines | ~66 | +| 21:25 | Edited src/scheduler/testsuite/test_driver_translate.c | modified make_wire_job() | ~384 | +| 21:26 | Edited src/scheduler/testsuite/test_driver_translate.c | modified test_drain_translates_and_pushes_all_ten_op_types() | ~2403 | +| 21:26 | Edited src/scheduler/testsuite/test_driver_translate.c | 11→14 lines | ~165 | +| 21:27 | Edited src/scheduler/testsuite/test_driver_translate.c | inline fix | ~21 | +| 21:28 | Session end: 168 writes across 38 files (tasks.md, core.h, core.c, scheduler.h, scheduler.c) | 58 reads | ~327166 tok | +| 21:30 | Session end: 168 writes across 38 files (tasks.md, core.h, core.c, scheduler.h, scheduler.c) | 58 reads | ~327166 tok | +| 21:39 | Edited .kiro/specs/scheduler-core/tasks.md | 4→5 lines | ~354 | +| 21:42 | Session end: 169 writes across 38 files (tasks.md, core.h, core.c, scheduler.h, scheduler.c) | 61 reads | ~332953 tok | +| 21:51 | Edited src/scheduler/include/core.h | modified does() | ~1151 | +| 21:51 | Edited src/scheduler/src/core_session.c | added 2 condition(s) | ~572 | +| 21:52 | Edited src/scheduler/include/scheduler.h | expanded (+10 lines) | ~229 | +| 21:52 | Edited src/scheduler/src/scheduler.c | modified sched_account_jobs() | ~68 | +| 21:52 | Edited src/scheduler/include/driver.h | modified can() | ~616 | +| 21:52 | Edited src/scheduler/src/driver.c | added 1 condition(s) | ~865 | +| 21:53 | Edited src/scheduler/src/main.c | modified parse_opt() | ~544 | +| 21:53 | Edited src/scheduler/src/main.c | added 3 condition(s) | ~1258 | +| 21:53 | Edited src/scheduler/src/main.c | added 1 condition(s) | ~241 | +| 21:53 | Edited src/scheduler/src/main.c | modified 6() | ~201 | +| 21:54 | Edited src/scheduler/testsuite/test_driver_translate.c | modified kill_sched_job() | ~331 | +| 21:56 | Created src/scheduler/testsuite/test_driver_decision_cb.c | — | ~2573 | +| 00:19 | Session end: 181 writes across 39 files (tasks.md, core.h, core.c, scheduler.h, scheduler.c) | 61 reads | ~344205 tok | +| 00:24 | Edited src/scheduler/include/core.h | run_normal_mode_round() → xce_sched_driver_run_normal_mode_round() | ~68 | +| 00:24 | Edited src/scheduler/include/scheduler.h | run_normal_mode_round() → xce_sched_driver_run_normal_mode_round() | ~43 | +| 00:24 | Edited src/scheduler/include/driver.h | modified no() | ~648 | +| 00:24 | Edited src/scheduler/src/driver.c | added 4 condition(s) | ~1244 | +| 00:25 | Edited src/scheduler/src/main.c | removed 99 lines | ~9 | +| 00:25 | Edited src/scheduler/src/main.c | run_normal_mode_round() → xce_sched_driver_run_normal_mode_round() | ~28 | +| 00:27 | Edited src/scheduler/testsuite/meson.build | modified set() | ~961 | +| 00:27 | Edited src/scheduler/testsuite/meson.build | expanded (+6 lines) | ~115 | +| 00:34 | Created src/scheduler/testsuite/test_driver_session_ordering.c | — | ~7541 | +| 00:34 | Edited src/scheduler/testsuite/test_driver_session_ordering.c | spy() → below() | ~50 | +| 00:35 | Edited src/scheduler/testsuite/test_driver_decision_cb.c | modified kill_sched_resval() | ~355 | +| 00:35 | Edited src/scheduler/testsuite/test_driver_translate.c | modified sched_publish_pend_reason() | ~364 | +| 00:37 | Edited src/scheduler/src/driver.c | modified REPRODUCTION() | ~89 | +| 00:37 | Edited src/scheduler/src/driver.c | 6→5 lines | ~66 | +| 00:40 | Session end: 195 writes across 40 files (tasks.md, core.h, core.c, scheduler.h, scheduler.c) | 62 reads | ~363013 tok | +| 00:42 | Session end: 195 writes across 40 files (tasks.md, core.h, core.c, scheduler.h, scheduler.c) | 62 reads | ~363013 tok | +| 00:48 | Edited src/scheduler/src/driver.c | 7→9 lines | ~119 | +| 01:01 | Session end: 196 writes across 40 files (tasks.md, core.h, core.c, scheduler.h, scheduler.c) | 63 reads | ~371327 tok | +| 01:04 | Edited .kiro/specs/scheduler-core/tasks.md | 5→5 lines | ~229 | +| 01:04 | Edited .kiro/specs/scheduler-core/tasks.md | 2→3 lines | ~430 | +| 01:04 | Edited .kiro/specs/scheduler-core/tasks.md | 1→2 lines | ~408 | + +## Session: 2026-08-04 01:07 + +| Time | Action | File(s) | Outcome | ~Tokens | +|------|--------|---------|---------|--------| + +## Session: 2026-08-04 01:08 + +| Time | Action | File(s) | Outcome | ~Tokens | +|------|--------|---------|---------|--------| + +## Session: 2026-08-04 01:09 + +| Time | Action | File(s) | Outcome | ~Tokens | +|------|--------|---------|---------|--------| +| 01:14 | Edited .kiro/specs/scheduler-core/tasks.md | 5→6 lines | ~222 | +| 01:25 | Session end: 1 writes across 1 files (tasks.md) | 2 reads | ~36721 tok | +| 01:42 | Edited src/schema/controller.capnp | modified mapname() | ~1647 | +| 01:42 | Edited src/include/typemap.h | modified struct() | ~912 | +| 01:43 | Session end: 3 writes across 3 files (tasks.md, controller.capnp, typemap.h) | 8 reads | ~82624 tok | +| 01:50 | Edited src/controller/testsuite/test_e2e_capnp.c | modified test_replay_header_snapshot_entry_round_trip() | ~2613 | +| 01:52 | Edited src/controller/testsuite/test_e2e_capnp.c | capn_len() → capn_resolve() | ~90 | +| 01:52 | Edited src/controller/testsuite/test_e2e_capnp.c | 2→2 lines | ~35 | +| 01:58 | Edited src/controller/testsuite/test_e2e_capnp.c | expanded (+7 lines) | ~148 | +| 01:58 | Edited src/controller/testsuite/test_e2e_capnp.c | 3→8 lines | ~133 | +| 01:58 | Edited src/controller/testsuite/test_e2e_capnp.c | 3→1 lines | ~18 | +| 02:00 | Task 7.1 done: replay envelope schema (ReplayHeader/ReplayCluster/SnapshotEntry/SnapshotData-stub/ReplayEvent/SessionSummary/ReplayDecision-stub/ReplaySession) added to controller.capnp + typemap.h C counterparts + round-trip tests | controller.capnp, typemap.h, test_e2e_capnp.c | full project build GREEN (controller/apiserver-CGO/scheduler/worker/xceadmin), controller_unit_test 1075/1075 pass | ~85000 | +| 02:04 | Session end: 9 writes across 4 files (tasks.md, controller.capnp, typemap.h, test_e2e_capnp.c) | 8 reads | ~88546 tok | +| 02:25 | Session end: 9 writes across 4 files (tasks.md, controller.capnp, typemap.h, test_e2e_capnp.c) | 8 reads | ~89249 tok | +| 02:36 | Session end: 9 writes across 4 files (tasks.md, controller.capnp, typemap.h, test_e2e_capnp.c) | 9 reads | ~91987 tok | +| 02:37 | Edited .kiro/specs/scheduler-core/tasks.md | 4→5 lines | ~466 | +| 02:40 | Session end: 10 writes across 4 files (tasks.md, controller.capnp, typemap.h, test_e2e_capnp.c) | 9 reads | ~92487 tok | +| 02:46 | Edited src/schema/controller.capnp | modified 2() | ~605 | +| 02:47 | Edited src/include/typemap.h | modified struct() | ~436 | +| 02:53 | Edited src/controller/testsuite/test_e2e_capnp.c | modified test_job_state_change_event_round_trip() | ~2695 | +| 02:53 | Edited src/controller/testsuite/test_e2e_capnp.c | 3→5 lines | ~56 | +| 02:56 | Edited src/schema/controller.capnp | 11→11 lines | ~197 | +| 02:57 | Edited src/include/typemap.h | 13→13 lines | ~137 | +| 03:00 | Task 7.2 done: JobStateChangeEvent/JobUpdateEvent added to controller.capnp + typemap.h C counterparts (job_state_change_event_t/job_update_event_t) + ReplayEvent.body union extended (jobStateChange@10/jobUpdate@11) + 2 round-trip tests (incl. full nested Job/submit/allocs/pidinfo/pend_reasons tree) | controller.capnp, typemap.h, test_e2e_capnp.c | full project build GREEN (controller/apiserver-CGO/scheduler/worker/xceadmin), controller_unit_test 1077/1077 pass, diff verified pure-addition (existing ReplayEvent @0-@9 byte-identical) | ~135000 | +| 03:01 | Session end: 16 writes across 4 files (tasks.md, controller.capnp, typemap.h, test_e2e_capnp.c) | 11 reads | ~104652 tok | +| 03:03 | Session end: 16 writes across 4 files (tasks.md, controller.capnp, typemap.h, test_e2e_capnp.c) | 11 reads | ~104652 tok | +| 03:15 | Session end: 16 writes across 4 files (tasks.md, controller.capnp, typemap.h, test_e2e_capnp.c) | 11 reads | ~104641 tok | +| 03:17 | Edited .kiro/specs/scheduler-core/tasks.md | modified not() | ~399 | +| 03:19 | Session end: 17 writes across 4 files (tasks.md, controller.capnp, typemap.h, test_e2e_capnp.c) | 11 reads | ~105406 tok | +| 05:15 | Session end: 17 writes across 4 files (tasks.md, controller.capnp, typemap.h, test_e2e_capnp.c) | 12 reads | ~106925 tok | +| 05:29 | Edited src/schema/controller.capnp | modified to() | ~865 | +| 05:29 | Edited src/schema/controller.capnp | modified 3() | ~442 | +| 05:30 | Edited src/schema/controller.capnp | modified compile() | ~384 | +| 05:30 | Edited src/include/typemap.h | modified here() | ~632 | +| 05:30 | Edited src/include/typemap.h | modified struct() | ~293 | +| 05:35 | Session end: 22 writes across 4 files (tasks.md, controller.capnp, typemap.h, test_e2e_capnp.c) | 13 reads | ~111140 tok | +| 05:35 | Edited src/controller/testsuite/test_e2e_capnp.c | modified test_dispatch_decision_round_trip() | ~6060 | +| 05:38 | Edited src/include/typemap.h | 4→4 lines | ~82 | +| 05:46 | Session end: 24 writes across 4 files (tasks.md, controller.capnp, typemap.h, test_e2e_capnp.c) | 13 reads | ~118439 tok | +| 12:52 | Task 7.3: added DispatchDecision/PendDecision/PreemptVictim/PreemptDecision + SchedUser/SchedProject (fresh mapnames, avoids scheduler-private xce_sched_user_t collision) + full SnapshotData (11 fields) + ReplayDecision body union | schema/controller.capnp, include/typemap.h, controller/testsuite/test_e2e_capnp.c | full build green, controller_unit_test 1082/1082 (0 fail) | ~145000 | +| 05:56 | Session end: 24 writes across 4 files (tasks.md, controller.capnp, typemap.h, test_e2e_capnp.c) | 13 reads | ~118439 tok | +| 06:10 | Edited .kiro/specs/scheduler-core/tasks.md | modified compile() | ~658 | +| 06:14 | Session end: 25 writes across 4 files (tasks.md, controller.capnp, typemap.h, test_e2e_capnp.c) | 13 reads | ~125168 tok | +| 06:19 | Edited src/schema/controller.capnp | 3→4 lines | ~20 | +| 06:20 | Edited src/schema/controller.capnp | modified mapname() | ~503 | +| 06:20 | Edited src/schema/controller.capnp | 3→4 lines | ~30 | +| 06:20 | Edited src/schema/controller.capnp | 2→3 lines | ~26 | +| 06:20 | Edited src/include/typemap.h | modified struct() | ~319 | +| 06:20 | Edited src/include/typemap.h | 3→4 lines | ~44 | +| 06:21 | Edited src/include/typemap.h | 3→4 lines | ~44 | +| 06:25 | Edited src/controller/testsuite/test_e2e_capnp.c | modified sched_admin_request_round_trip_case() | ~1748 | +| 06:25 | Edited src/controller/testsuite/test_e2e_capnp.c | 2→4 lines | ~39 | +| 06:35 | Session end: 34 writes across 4 files (tasks.md, controller.capnp, typemap.h, test_e2e_capnp.c) | 13 reads | ~129367 tok | +| 06:38 | Session end: 34 writes across 4 files (tasks.md, controller.capnp, typemap.h, test_e2e_capnp.c) | 13 reads | ~129367 tok | +| 06:46 | Session end: 34 writes across 4 files (tasks.md, controller.capnp, typemap.h, test_e2e_capnp.c) | 13 reads | ~129367 tok | +| 06:53 | Session end: 34 writes across 4 files (tasks.md, controller.capnp, typemap.h, test_e2e_capnp.c) | 13 reads | ~129762 tok | +| 06:59 | Edited .kiro/specs/scheduler-core/tasks.md | 6→7 lines | ~412 | +| 07:01 | Session end: 35 writes across 4 files (tasks.md, controller.capnp, typemap.h, test_e2e_capnp.c) | 13 reads | ~130203 tok | +| 07:10 | Edited src/controller/testsuite/test_e2e_capnp.c | modified test_replay_event_job_state_change_round_trip() | ~2291 | +| 07:10 | Edited src/controller/testsuite/test_e2e_capnp.c | 3→6 lines | ~73 | +| 07:12 | Edited src/controller/testsuite/test_e2e_capnp.c | 5→5 lines | ~56 | +| 07:13 | Edited src/controller/testsuite/test_e2e_capnp.c | 5→5 lines | ~56 | +| 07:13 | Edited src/controller/testsuite/test_e2e_capnp.c | 3→3 lines | ~40 | +| 07:14 | Edited src/controller/testsuite/test_e2e_capnp.c | 16→16 lines | ~123 | +| 07:19 | Session end: 41 writes across 4 files (tasks.md, controller.capnp, typemap.h, test_e2e_capnp.c) | 13 reads | ~137699 tok | +| 07:30 | Session end: 41 writes across 4 files (tasks.md, controller.capnp, typemap.h, test_e2e_capnp.c) | 14 reads | ~137699 tok | +| 07:31 | Edited .kiro/specs/scheduler-core/tasks.md | 5→6 lines | ~477 | +| 07:34 | Session end: 42 writes across 4 files (tasks.md, controller.capnp, typemap.h, test_e2e_capnp.c) | 14 reads | ~138450 tok | +| 07:55 | Session end: 42 writes across 4 files (tasks.md, controller.capnp, typemap.h, test_e2e_capnp.c) | 32 reads | ~208786 tok | +| 07:56 | Edited src/scheduler/include/core.h | expanded (+24 lines) | ~422 | +| 07:57 | Edited src/scheduler/include/core.h | expanded (+44 lines) | ~729 | +| 08:01 | Created src/scheduler/src/snapshot.c | — | ~11590 | +| 08:02 | Edited src/scheduler/meson.build | 3→4 lines | ~22 | +| 19:32 | Session end: 46 writes across 7 files (tasks.md, controller.capnp, typemap.h, test_e2e_capnp.c, core.h) | 35 reads | ~234914 tok | +| 19:41 | Session end: 46 writes across 7 files (tasks.md, controller.capnp, typemap.h, test_e2e_capnp.c, core.h) | 37 reads | ~249590 tok | +| 19:42 | Created src/scheduler/src/snapshot.c | — | ~7974 | +| 19:43 | Session end: 47 writes across 7 files (tasks.md, controller.capnp, typemap.h, test_e2e_capnp.c, core.h) | 37 reads | ~258133 tok | +| 19:43 | Created src/scheduler/src/snapshot_wire.c | — | ~2152 | +| 19:43 | Created src/scheduler/src/snapshot_load.c | — | ~3346 | +| 19:44 | Edited src/scheduler/include/core.h | modified never() | ~492 | +| 19:44 | Edited src/scheduler/meson.build | 3→5 lines | ~29 | +| 19:49 | Edited .kiro/specs/scheduler-core/tasks.md | 4→5 lines | ~241 | +| 19:49 | Session end: 52 writes across 9 files (tasks.md, controller.capnp, typemap.h, test_e2e_capnp.c, core.h) | 37 reads | ~265197 tok | +| 19:53 | Session end: 52 writes across 9 files (tasks.md, controller.capnp, typemap.h, test_e2e_capnp.c, core.h) | 37 reads | ~265197 tok | +| 20:05 | Created src/scheduler/testsuite/test_snapshot_save_load.c | — | ~8539 | +| 20:05 | Edited src/scheduler/testsuite/meson.build | expanded (+23 lines) | ~428 | +| 20:06 | Edited src/scheduler/testsuite/meson.build | 3→6 lines | ~78 | +| 20:08 | Edited src/scheduler/testsuite/test_snapshot_save_load.c | 2→3 lines | ~43 | +| 20:14 | Edited src/scheduler/testsuite/test_snapshot_save_load.c | expanded (+10 lines) | ~151 | +| 20:14 | Session end: 57 writes across 10 files (tasks.md, controller.capnp, typemap.h, test_e2e_capnp.c, core.h) | 40 reads | ~288397 tok | +| 20:19 | Edited src/scheduler/testsuite/test_snapshot_save_load.c | expanded (+24 lines) | ~362 | +| 20:21 | Edited src/scheduler/src/core.c | added 1 condition(s) | ~359 | +| 20:21 | Edited src/scheduler/src/core.c | modified above() | ~165 | +| 20:22 | Edited src/scheduler/testsuite/test_snapshot_save_load.c | removed 40 lines | ~77 | +| 20:23 | Edited src/scheduler/testsuite/test_snapshot_save_load.c | expanded (+7 lines) | ~163 | +| 20:24 | Edited src/scheduler/testsuite/test_snapshot_save_load.c | 10→13 lines | ~170 | +| 20:25 | Edited src/scheduler/src/snapshot_load.c | modified if() | ~185 | +| 20:35 | Session end: 64 writes across 11 files (tasks.md, controller.capnp, typemap.h, test_e2e_capnp.c, core.h) | 41 reads | ~293568 tok | +| 20:37 | Session end: 64 writes across 11 files (tasks.md, controller.capnp, typemap.h, test_e2e_capnp.c, core.h) | 41 reads | ~293568 tok | +| 20:55 | Session end: 64 writes across 11 files (tasks.md, controller.capnp, typemap.h, test_e2e_capnp.c, core.h) | 42 reads | ~292320 tok | +| 20:56 | Edited .kiro/specs/scheduler-core/tasks.md | 6→7 lines | ~591 | +| 20:59 | Session end: 65 writes across 11 files (tasks.md, controller.capnp, typemap.h, test_e2e_capnp.c, core.h) | 42 reads | ~293436 tok | +| 21:04 | Edited src/scheduler/testsuite/test_snapshot_save_load.c | added 1 condition(s) | ~1691 | +| 21:04 | Edited src/scheduler/testsuite/test_snapshot_save_load.c | 4→6 lines | ~76 | +| 04:06 | Task 8.2: added genuine-active-session -EBUSY proof (real xce_core_go + picker thread) for save/load snapshot guards, existing 8.1 test only used synthetic session_active flip | src/scheduler/testsuite/test_snapshot_save_load.c | 23/23 scheduler suite green, 3/3 in snapshot binary | ~9k | +| 21:09 | Session end: 67 writes across 11 files (tasks.md, controller.capnp, typemap.h, test_e2e_capnp.c, core.h) | 42 reads | ~295329 tok | +| 21:16 | Session end: 67 writes across 11 files (tasks.md, controller.capnp, typemap.h, test_e2e_capnp.c, core.h) | 42 reads | ~296966 tok | +| 21:17 | Edited .kiro/specs/scheduler-core/tasks.md | 5→6 lines | ~395 | +| 21:19 | Session end: 68 writes across 11 files (tasks.md, controller.capnp, typemap.h, test_e2e_capnp.c, core.h) | 42 reads | ~297389 tok | +| 21:24 | Session end: 68 writes across 11 files (tasks.md, controller.capnp, typemap.h, test_e2e_capnp.c, core.h) | 44 reads | ~301058 tok | +| 21:26 | Created src/scheduler/include/recording.h | — | ~1460 | +| 21:26 | Created src/scheduler/src/recording.c | — | ~1352 | +| 21:27 | Edited src/scheduler/include/scheduler.h | modified struct() | ~319 | +| 21:27 | Edited src/scheduler/src/main.c | expanded (+13 lines) | ~442 | +| 21:27 | Edited src/scheduler/src/main.c | 6→9 lines | ~52 | +| 21:27 | Edited src/scheduler/src/main.c | modified 10() | ~140 | +| 21:28 | Edited src/scheduler/src/main.c | added 1 condition(s) | ~119 | +| 21:28 | Edited src/scheduler/src/main.c | modified 10() | ~109 | +| 21:28 | Edited src/scheduler/meson.build | 2→3 lines | ~16 | +| 21:29 | Created src/scheduler/testsuite/test_recording_path_validation.c | — | ~4180 | +| 21:29 | Edited src/scheduler/testsuite/test_recording_path_validation.c | 3→1 lines | ~19 | +| 21:30 | Edited src/scheduler/testsuite/meson.build | modified under() | ~330 | +| 21:31 | Edited src/scheduler/testsuite/meson.build | 3→6 lines | ~79 | +| 21:33 | Edited src/scheduler/testsuite/test_recording_path_validation.c | modified make_tmp_dir() | ~110 | +| 21:44 | Session end: 82 writes across 16 files (tasks.md, controller.capnp, typemap.h, test_e2e_capnp.c, core.h) | 48 reads | ~322737 tok | +| 22:04 | Session end: 82 writes across 16 files (tasks.md, controller.capnp, typemap.h, test_e2e_capnp.c, core.h) | 48 reads | ~322771 tok | +| 22:05 | Edited src/scheduler/src/recording.c | added 1 condition(s) | ~380 | +| 22:05 | Edited src/scheduler/include/recording.h | modified 10() | ~336 | +| 22:06 | Edited src/scheduler/include/recording.h | modified exists() | ~445 | +| 22:06 | Edited src/scheduler/testsuite/test_recording_path_validation.c | added error handling | ~610 | +| 22:06 | Edited src/scheduler/testsuite/test_recording_path_validation.c | 3→5 lines | ~58 | +| 22:08 | Edited src/scheduler/src/recording.c | removed 24 lines | ~42 | +| 22:09 | Created src/scheduler/src/recording.c | — | ~1674 | + +## Session: 2026-08-05 22:14 + +| Time | Action | File(s) | Outcome | ~Tokens | +|------|--------|---------|---------|--------| +| 22:22 | Edited src/scheduler/testsuite/test_recording_path_validation.c | modified test_symlink_to_hardlinked_alias_inside_record_dir_is_rejected() | ~602 | +| 22:23 | Edited src/scheduler/testsuite/test_recording_path_validation.c | 3→4 lines | ~58 | +| 22:23 | Edited src/scheduler/src/recording.c | added 2 condition(s) | ~511 | +| 22:26 | Session end: 3 writes across 2 files (test_recording_path_validation.c, recording.c) | 3 reads | ~7854 tok | +| 22:26 | Edited src/scheduler/include/recording.h | expanded (+9 lines) | ~379 | +| 22:26 | Edited src/scheduler/include/recording.h | modified exists() | ~153 | +| 22:27 | Edited src/scheduler/src/recording.c | 14→13 lines | ~201 | +| 22:28 | scheduler-core task 9.1 round 3: closed symlink-to-hardlink-alias bypass in sched_validate_record_filename() S_ISLNK branch (record_dir/alias -> record_dir/internal, internal is hard link to outside victim, previously accepted rc=0) -- added stat() on realpath()-resolved target + S_ISREG && st_nlink>1 -EACCES check after path_is_contained() passes, mirroring round-2 direct-entry check; RED confirmed live (Expected -13 Was 0) then GREEN | src/scheduler/src/recording.c, src/scheduler/include/recording.h, src/scheduler/testsuite/test_recording_path_validation.c | 18/18 unit assertions pass (was 17), full scheduler suite 24/24 OK, clang-format --dry-run --Werror clean | ~55k | +| 22:30 | Session end: 6 writes across 3 files (test_recording_path_validation.c, recording.c, recording.h) | 3 reads | ~10747 tok | +| 22:32 | Created ../../../../../../tmp/claude-1000/-home-rshen-projects-xce-worktrees-scheduler-framework/af7e2e71-6a3d-487f-8cd6-2b0ae196cb3a/scratchpad/poc_recording.c | — | ~2221 | +| 22:33 | Edited ../../../../../../tmp/claude-1000/-home-rshen-projects-xce-worktrees-scheduler-framework/af7e2e71-6a3d-487f-8cd6-2b0ae196cb3a/scratchpad/poc_recording.c | 9→11 lines | ~75 | +| 22:33 | Edited ../../../../../../tmp/claude-1000/-home-rshen-projects-xce-worktrees-scheduler-framework/af7e2e71-6a3d-487f-8cd6-2b0ae196cb3a/scratchpad/poc_recording.c | added 5 condition(s) | ~661 | +| 22:38 | Edited .kiro/specs/scheduler-core/tasks.md | 6→7 lines | ~823 | +| 22:39 | Task 9.1 committed (4e5547ee): recording dir provisioning + path validation, 3-round CWE-59 inode-alias hunt (direct hardlink + symlink-to-hardlink compound), both closed | recording.c, recording.h, test_recording_path_validation.c, scheduler.h, main.c, tasks.md | 24/24 pass | ~1200 | +| 00:36 | Session end: 10 writes across 5 files (test_recording_path_validation.c, recording.c, recording.h, poc_recording.c, tasks.md) | 9 reads | ~92540 tok | +| 00:40 | Session end: 10 writes across 5 files (test_recording_path_validation.c, recording.c, recording.h, poc_recording.c, tasks.md) | 9 reads | ~92540 tok | +| 01:01 | Edited .kiro/specs/scheduler-core/tasks.md | modified task() | ~578 | +| 01:02 | Session end: 11 writes across 5 files (test_recording_path_validation.c, recording.c, recording.h, poc_recording.c, tasks.md) | 20 reads | ~139873 tok | +| 01:06 | Edited .kiro/specs/scheduler-core/tasks.md | inline fix | ~588 | +| 01:06 | Session end: 12 writes across 5 files (test_recording_path_validation.c, recording.c, recording.h, poc_recording.c, tasks.md) | 20 reads | ~140503 tok | +| 01:07 | Session end: 12 writes across 5 files (test_recording_path_validation.c, recording.c, recording.h, poc_recording.c, tasks.md) | 20 reads | ~140503 tok | +| 01:07 | Edited src/scheduler/include/scheduler.h | 5→7 lines | ~38 | +| 01:08 | Edited src/scheduler/include/scheduler.h | modified 10() | ~567 | +| 01:08 | Edited src/scheduler/include/recording.h | modified every() | ~222 | +| 01:09 | Edited src/scheduler/include/recording.h | modified returns() | ~2207 | +| 01:09 | Edited src/scheduler/src/recording.c | modified contract() | ~243 | +| 01:10 | Edited src/scheduler/src/recording.c | added error handling | ~5976 | +| 01:13 | Created src/scheduler/src/recording.c | — | ~5263 | +| 01:14 | Created src/scheduler/src/recording_events.c | — | ~1705 | +| 01:14 | Created src/scheduler/src/recording_session.c | — | ~1975 | +| 01:14 | Edited src/scheduler/src/recording_session.c | 5→6 lines | ~30 | +| 01:15 | Edited src/scheduler/meson.build | 3→5 lines | ~32 | +| 01:15 | Edited src/scheduler/src/driver.c | 5→6 lines | ~36 | +| 01:16 | Edited src/scheduler/src/driver.c | added 1 condition(s) | ~365 | +| 01:16 | Edited src/scheduler/src/driver.c | modified xce_sched_driver_run_normal_mode_round() | ~71 | +| 01:16 | Edited src/scheduler/src/driver.c | added 3 condition(s) | ~580 | +| 01:18 | Edited src/scheduler/src/recording_session.c | 6→6 lines | ~82 | +| 01:21 | Edited src/scheduler/src/driver.c | modified xce_sched_driver_translate_pending_events() | ~40 | +| 01:21 | Edited src/scheduler/src/driver.c | modified 10() | ~443 | +| 01:21 | Edited src/scheduler/src/driver.c | modified 8() | ~159 | +| 01:22 | Edited src/scheduler/include/driver.h | modified must() | ~429 | +| 01:23 | Edited src/scheduler/testsuite/test_driver_translate.c | modified sched_publish_pend_reason() | ~495 | +| 01:24 | Edited src/scheduler/testsuite/test_driver_decision_cb.c | modified sched_account_jobs() | ~407 | +| 01:25 | Edited src/scheduler/testsuite/test_driver_session_ordering.c | modified sched_fill_limit_requests() | ~600 | +| 01:26 | Edited src/scheduler/testsuite/test_recording_path_validation.c | modified under() | ~290 | +| 01:30 | Edited src/scheduler/src/driver.c | modified 10() | ~231 | +| 01:38 | Session end: 37 writes across 14 files (test_recording_path_validation.c, recording.c, recording.h, poc_recording.c, tasks.md) | 25 reads | ~187036 tok | +| 01:40 | Created src/scheduler/testsuite/test_record_lifecycle.c | — | ~7793 | +| 01:41 | Edited src/scheduler/testsuite/meson.build | modified hooks() | ~411 | +| 01:41 | Edited src/scheduler/testsuite/meson.build | 3→6 lines | ~78 | +| 01:43 | Edited src/scheduler/src/recording.c | added 1 condition(s) | ~470 | +| 01:45 | Edited src/scheduler/testsuite/test_record_lifecycle.c | added 1 condition(s) | ~306 | +| 01:45 | Edited src/scheduler/testsuite/test_record_lifecycle.c | modified fixture_init() | ~118 | +| 01:57 | Session end: 43 writes across 15 files (test_recording_path_validation.c, recording.c, recording.h, poc_recording.c, tasks.md) | 27 reads | ~212765 tok | +| 01:59 | Session end: 43 writes across 15 files (test_recording_path_validation.c, recording.c, recording.h, poc_recording.c, tasks.md) | 27 reads | ~212765 tok | +| 02:06 | Edited .kiro/specs/scheduler-core/tasks.md | 8→9 lines | ~1079 | +| 02:07 | Edited .kiro/specs/scheduler-core/tasks.md | modified decided() | ~251 | +| 02:24 | Task 9.2 committed (ff8d4f41): record start/stop lifecycle, TOCTOU-hardened open, driver hooks; bug-1498 O_TRUNC race + bug-1499 deadlock found/fixed | recording.c/h, recording_events.c, recording_session.c, driver.c/h, scheduler.h, test_record_lifecycle.c | 25/25 pass | ~2400 | +| 02:11 | Session end: 45 writes across 15 files (test_recording_path_validation.c, recording.c, recording.h, poc_recording.c, tasks.md) | 27 reads | ~215514 tok | +| 02:21 | Edited src/scheduler/src/recording.c | modified contract() | ~369 | +| 02:21 | Edited src/scheduler/src/recording.c | modified itself() | ~587 | +| 02:21 | Edited src/scheduler/src/recording_events.c | 18→23 lines | ~360 | +| 02:22 | Edited src/scheduler/src/recording_events.c | added 2 condition(s) | ~811 | +| 02:22 | Edited src/scheduler/include/recording.h | modified checkpoint() | ~508 | +| 02:22 | Edited src/scheduler/include/scheduler.h | 3→6 lines | ~97 | +| 02:24 | Edited src/scheduler/testsuite/test_record_lifecycle.c | modified submit_one_jobstate_event() | ~4414 | +| 02:37 | Session end: 52 writes across 15 files (test_recording_path_validation.c, recording.c, recording.h, poc_recording.c, tasks.md) | 27 reads | ~223170 tok | +| 02:43 | Task 9.3 complete: periodic snapshot checkpointing (recording_events.c/recording.c/recording.h/scheduler.h) + write-failure e2e tests | recording.c, recording_events.c, recording.h, scheduler.h, test_record_lifecycle.c | 25/25 scheduler suite pass, clang-format clean | ~9000 | +| 02:44 | Edited src/scheduler/testsuite/test_record_lifecycle.c | modified test() | ~603 | +| 02:47 | Session end: 53 writes across 15 files (test_recording_path_validation.c, recording.c, recording.h, poc_recording.c, tasks.md) | 27 reads | ~228076 tok | +| 03:01 | Edited .kiro/specs/scheduler-core/tasks.md | modified recording() | ~736 | +| 03:01 | Edited .kiro/specs/scheduler-core/tasks.md | 1→3 lines | ~616 | +| 03:15 | Task 9.3 committed (7494542a): periodic snapshot checkpointing, write-failure test w/ real closed-fd; flagged --record-snapshot-interval CLI gap | recording.c, recording_events.c, recording.h, scheduler.h, test_record_lifecycle.c | 25/25 pass | ~2000 | +| 03:05 | Session end: 55 writes across 15 files (test_recording_path_validation.c, recording.c, recording.h, poc_recording.c, tasks.md) | 28 reads | ~236620 tok | +| 03:27 | Edited src/scheduler/include/scheduler.h | modified control() | ~349 | +| 03:27 | Edited src/scheduler/include/scheduler.h | expanded (+21 lines) | ~359 | +| 03:28 | Edited src/scheduler/include/recording.h | expanded (+27 lines) | ~462 | +| 03:29 | Edited src/scheduler/src/recording.c | added 1 condition(s) | ~247 | +| 03:30 | Edited src/include/client.h | expanded (+20 lines) | ~353 | +| 03:30 | Edited src/lib/client.c | added 1 condition(s) | ~451 | +| 03:30 | Edited src/scheduler/src/handler.c | 7→8 lines | ~42 | +| 03:31 | Edited src/scheduler/src/handler.c | added 5 condition(s) | ~1386 | +| 03:31 | Edited src/scheduler/src/driver.c | 10→12 lines | ~62 | +| 03:31 | Session end: 64 writes across 18 files (test_recording_path_validation.c, recording.c, recording.h, poc_recording.c, tasks.md) | 33 reads | ~305052 tok | +| 03:31 | Edited src/scheduler/src/driver.c | added 8 condition(s) | ~1870 | +| 03:32 | Edited src/scheduler/src/driver.c | added 1 condition(s) | ~255 | +| 03:34 | Edited src/scheduler/testsuite/test_handlers.c | modified sched_get_job() | ~194 | +| 03:34 | Edited src/scheduler/testsuite/test_job_array_expand.c | modified sched_init_config() | ~142 | +| 03:34 | Edited src/scheduler/testsuite/test_job_array_wire_e2e.c | modified sched_init_config() | ~146 | +| 03:37 | Edited src/scheduler/testsuite/test_driver_translate.c | modified sched_record_write_events() | ~212 | +| 03:38 | Edited src/scheduler/testsuite/test_driver_decision_cb.c | modified sched_record_write_events() | ~212 | +| 03:43 | Edited src/scheduler/testsuite/test_driver_session_ordering.c | modified sched_record_write_events() | ~212 | +| 05:50 | Session end: 72 writes across 21 files (test_recording_path_validation.c, recording.c, recording.h, poc_recording.c, tasks.md) | 35 reads | ~314148 tok | +| 06:04 | Created src/scheduler/testsuite/test_admin_request.c | — | ~7746 | +| 06:04 | Edited src/scheduler/testsuite/test_admin_request.c | 5→6 lines | ~29 | +| 06:05 | Edited src/scheduler/testsuite/meson.build | expanded (+24 lines) | ~438 | +| 06:05 | Edited src/scheduler/testsuite/meson.build | 3→6 lines | ~72 | +| 06:06 | Edited src/scheduler/testsuite/test_admin_request.c | inline fix | ~21 | +| 06:13 | Session end: 77 writes across 22 files (test_recording_path_validation.c, recording.c, recording.h, poc_recording.c, tasks.md) | 37 reads | ~338139 tok | +| 06:27 | Session end: 77 writes across 22 files (test_recording_path_validation.c, recording.c, recording.h, poc_recording.c, tasks.md) | 43 reads | ~352172 tok | +| 06:29 | Session end: 77 writes across 22 files (test_recording_path_validation.c, recording.c, recording.h, poc_recording.c, tasks.md) | 43 reads | ~352172 tok | +| 06:37 | Edited src/scheduler/include/scheduler.h | 2→4 lines | ~29 | +| 06:37 | Edited src/scheduler/include/scheduler.h | modified Concurrency() | ~211 | +| 06:37 | Edited src/scheduler/include/scheduler.h | expanded (+7 lines) | ~285 | +| 06:37 | Edited src/scheduler/include/scheduler.h | expanded (+18 lines) | ~378 | +| 06:38 | Edited src/scheduler/src/scheduler.c | modified if() | ~205 | +| 06:38 | Edited src/scheduler/src/scheduler.c | 4→8 lines | ~60 | +| 06:38 | Edited src/scheduler/src/recording.c | modified record_lock() | ~190 | +| 06:38 | Edited src/scheduler/src/recording.c | modified record_lock() | ~352 | +| 06:39 | Edited src/scheduler/src/recording.c | added 1 condition(s) | ~938 | +| 06:39 | Edited src/scheduler/src/recording_events.c | modified write_one_replay_event() | ~34 | +| 06:39 | Edited src/scheduler/src/recording_events.c | expanded (+7 lines) | ~235 | +| 06:39 | Edited src/scheduler/src/recording_events.c | modified if() | ~61 | +| 06:40 | Edited src/scheduler/src/recording_events.c | modified for() | ~35 | +| 06:40 | Edited src/scheduler/include/recording.h | modified is() | ~688 | +| 06:40 | Edited src/scheduler/include/recording.h | modified Concurrency() | ~184 | +| 06:40 | Edited src/scheduler/include/recording.h | modified Concurrency() | ~198 | +| 06:41 | Edited src/scheduler/include/recording.h | modified Concurrency() | ~176 | +| 06:42 | Edited src/scheduler/src/handler.c | modified if() | ~185 | +| 06:42 | Edited src/scheduler/testsuite/test_admin_request.c | modified fixture_init() | ~229 | +| 06:42 | Edited src/scheduler/testsuite/test_record_lifecycle.c | modified fixture_init() | ~186 | +| 06:48 | Edited src/scheduler/testsuite/test_admin_request.c | modified struct() | ~1553 | +| 06:48 | Edited src/scheduler/testsuite/test_admin_request.c | 4→6 lines | ~94 | +| 06:48 | Edited src/scheduler/testsuite/test_handlers.c | modified sched_record_status() | ~36 | +| 06:49 | Edited src/scheduler/testsuite/test_job_array_expand.c | modified sched_record_status() | ~36 | +| 06:53 | Session end: 101 writes across 23 files (test_recording_path_validation.c, recording.c, recording.h, poc_recording.c, tasks.md) | 47 reads | ~377058 tok | +| 06:59 | Session end: 101 writes across 23 files (test_recording_path_validation.c, recording.c, recording.h, poc_recording.c, tasks.md) | 47 reads | ~377058 tok | +| 07:05 | Edited src/scheduler/src/recording.c | modified strdup() | ~95 | +| 07:06 | Edited src/scheduler/src/recording.c | modified strdup() | ~94 | +| 07:11 | Edited .kiro/specs/scheduler-core/tasks.md | 5→6 lines | ~1086 | +| 07:26 | Task 9.4 committed (e819a506): scheduler-side OPCode_schedAdmin handler, deferred dispatch via pend_updates; round-1 review found+fixed real cross-thread UAF race (record_lock) | recording.c/h, handler.c, driver.c, scheduler.h/c, client.c/h, test_admin_request.c | 26/26 pass | ~2500 | +| 07:13 | Session end: 104 writes across 23 files (test_recording_path_validation.c, recording.c, recording.h, poc_recording.c, tasks.md) | 47 reads | ~381128 tok | +| 07:19 | Edited src/scheduler/testsuite/test_admin_request.c | modified level() | ~397 | +| 07:19 | Edited src/scheduler/testsuite/test_admin_request.c | 8→9 lines | ~48 | +| 07:19 | Edited src/scheduler/testsuite/test_admin_request.c | modified make_tmp_dir() | ~179 | +| 07:20 | Edited src/scheduler/testsuite/test_admin_request.c | added error handling | ~1616 | +| 07:21 | Edited src/scheduler/testsuite/test_admin_request.c | modified test_write_failure_through_admin_path_disables_recording_cleanly() | ~1320 | +| 07:21 | Edited src/scheduler/testsuite/test_admin_request.c | 7→11 lines | ~147 | +| 07:24 | Edited src/scheduler/testsuite/test_admin_request.c | expanded (+7 lines) | ~368 | +| 07:24 | Edited src/scheduler/testsuite/test_admin_request.c | 5→5 lines | ~63 | +| 07:27 | Session end: 112 writes across 23 files (test_recording_path_validation.c, recording.c, recording.h, poc_recording.c, tasks.md) | 47 reads | ~388862 tok | +| 07:35 | Edited .kiro/specs/scheduler-core/tasks.md | modified tests() | ~826 | +| 07:53 | Task 9.5 committed (2d533e65): recording lifecycle integration tests through real admin handler; Group 9 (Core: Recording Control) fully complete | test_admin_request.c | 26/26 pass | ~1800 | +| 07:39 | Session end: 113 writes across 23 files (test_recording_path_validation.c, recording.c, recording.h, poc_recording.c, tasks.md) | 48 reads | ~410809 tok | +| 07:52 | Edited src/include/request.h | modified only() | ~128 | +| 07:52 | Edited src/lib/request.c | added 1 condition(s) | ~238 | +| 07:52 | Edited src/controller/cluster.c | 6→8 lines | ~43 | +| 07:53 | Edited src/controller/cluster.c | added 2 condition(s) | ~847 | +| 07:53 | Edited src/controller/cluster.c | added 7 condition(s) | ~1507 | +| 07:54 | Edited src/controller/cluster.c | added 2 condition(s) | ~680 | +| 19:25 | Session end: 119 writes across 26 files (test_recording_path_validation.c, recording.c, recording.h, poc_recording.c, tasks.md) | 52 reads | ~550070 tok | +| 19:41 | Created src/controller/testsuite/test_sched_admin_dispatch.c | — | ~3161 | +| 19:41 | Edited src/controller/meson.build | 2→3 lines | ~27 | +| 19:42 | Edited src/controller/testsuite/controller_test.c | modified setUp() | ~31 | +| 19:42 | Edited src/controller/testsuite/controller_test.c | 3→4 lines | ~26 | +| 19:48 | Session end: 123 writes across 28 files (test_recording_path_validation.c, recording.c, recording.h, poc_recording.c, tasks.md) | 59 reads | ~564274 tok | +| 03:09 | Independent adversarial review of task 10.1 (schedAdmin controller dispatch) — APPROVED with findings | src/controller/cluster.c, src/lib/request.c, src/include/request.h, src/controller/testsuite/test_sched_admin_dispatch.c | build+1090/1090 controller+26/26 scheduler PASS, clang-format clean, no correctness bugs found; flagged sched.lock contention (broader than prior session noted), narrow-window leak, whole-file reformat noise, no TSan | ~large | +| 20:12 | Session end: 123 writes across 28 files (test_recording_path_validation.c, recording.c, recording.h, poc_recording.c, tasks.md) | 63 reads | ~803441 tok | +| 20:16 | Edited src/lib/request.c | added 1 condition(s) | ~238 | +| 20:17 | Edited src/controller/cluster.c | added 2 condition(s) | ~878 | +| 20:17 | Edited src/controller/cluster.c | 6→8 lines | ~43 | +| 20:19 | Edited src/controller/cluster.c | added 9 condition(s) | ~2212 | +| 20:24 | Edited src/controller/cluster.c | modified while() | ~568 | +| 20:30 | diff-hygiene fix: reverted whole-file clang-format reformat noise in cluster.c/request.c to touch only task 10.1 semantic additions (schedadmin struct+helpers+case block+reply branch, xce_schedadmin_req); fixed schedadmin race-window leak (unregister before re-reading pending outcome, avoiding AB-BA deadlock vs schedadmin_signal); controller 3/3 (incl 3 schedAdmin tests), scheduler 26/26, 1090 unity tests 0 failures | src/controller/cluster.c, src/lib/request.c | done | ~45000 | +| 20:33 | Edited src/include/request.h | modified only() | ~150 | +| 20:38 | Edited .kiro/specs/scheduler-core/tasks.md | modified seam() | ~1258 | +| 20:39 | Task 10.1 committed (045be6b3): controller-side OPCode_schedAdmin dispatch, HA-critical; 2-round adversarial review + diff-hygiene cleanup + leak fix | cluster.c, request.c/h, test_sched_admin_dispatch.c | controller 3/3, scheduler 26/26 | ~4500 | +| 20:40 | Session end: 130 writes across 28 files (test_recording_path_validation.c, recording.c, recording.h, poc_recording.c, tasks.md) | 66 reads | ~811080 tok | +| 20:49 | Edited src/controller/cluster.c | modified XCE_TESTING() | ~375 | +| 20:49 | Edited src/controller/cluster.c | added 1 condition(s) | ~112 | +| 20:50 | Edited src/controller/testsuite/test_sched_admin_dispatch.c | expanded (+56 lines) | ~1090 | +| 20:50 | Edited src/controller/testsuite/test_sched_admin_dispatch.c | 1→6 lines | ~87 | +| 20:50 | Edited src/controller/testsuite/test_sched_admin_dispatch.c | modified sa_mono_ms() | ~125 | +| 20:51 | Edited src/controller/testsuite/test_sched_admin_dispatch.c | added 1 condition(s) | ~2405 | +| 20:51 | Edited src/controller/testsuite/test_sched_admin_dispatch.c | 4→7 lines | ~88 | +| 20:52 | Edited src/controller/testsuite/test_sched_admin_dispatch.c | 4→5 lines | ~26 | +| 20:54 | Edited src/controller/cluster.c | modified if() | ~71 | +| 20:54 | Edited src/controller/testsuite/test_sched_admin_dispatch.c | 3→3 lines | ~36 | +| 20:55 | Edited src/controller/testsuite/test_sched_admin_dispatch.c | modified struct() | ~46 | +| 21:02 | Session end: 141 writes across 28 files (test_recording_path_validation.c, recording.c, recording.h, poc_recording.c, tasks.md) | 66 reads | ~819424 tok | +| 21:19 | Edited .kiro/specs/scheduler-core/tasks.md | modified XCE_TESTING() | ~906 | +| 21:31 | Task 10.2 committed (30ff995c): controller dispatch timeout + round-trip tests, XCE_TESTING-only seam verified inert in production | cluster.c, test_sched_admin_dispatch.c | controller 3/3 (1093/1093), scheduler 26/26 | ~3800 | +| 21:21 | Session end: 142 writes across 28 files (test_recording_path_validation.c, recording.c, recording.h, poc_recording.c, tasks.md) | 67 reads | ~829855 tok | +| 21:27 | Edited src/xceadmin/src/xcecommon/errors.py | modified __init__() | ~573 | +| 21:28 | Edited src/xceadmin/tests/test_errors.py | 22→24 lines | ~142 | +| 21:28 | Edited src/xceadmin/tests/test_errors.py | modified test_sched_admin_error_carries_error_and_reply_state() | ~326 | +| 21:28 | Edited src/xceadmin/tests/test_errors.py | expanded (+6 lines) | ~87 | +| 21:29 | Created src/xceadmin/src/xceadmin/scheduler.py | — | ~2473 | +| 21:29 | Edited src/xceadmin/src/xceadmin/scheduler.py | added 1 import(s) | ~65 | +| 21:29 | Edited src/xceadmin/src/xceadmin/scheduler.py | modified _send_via() | ~73 | +| 21:30 | Created src/xceadmin/tests/test_scheduler.py | — | ~4378 | +| 21:30 | Edited src/xceadmin/tests/test_scheduler.py | removed 33 lines | ~59 | +| 21:33 | Edited .kiro/specs/scheduler-core/tasks.md | modified returns() | ~932 | +| 21:35 | Session end: 152 writes across 32 files (test_recording_path_validation.c, recording.c, recording.h, poc_recording.c, tasks.md) | 77 reads | ~863504 tok | +| 21:43 | Edited .kiro/specs/scheduler-core/tasks.md | inline fix | ~282 | +| 22:03 | Task 10.3 committed (b64875f4): xceadmin scheduler.py record_start/stop/status client functions, SchedAdminError | scheduler.py, errors.py, test_scheduler.py | 188 passed 3 skipped | ~3200 | +| 21:45 | Session end: 153 writes across 32 files (test_recording_path_validation.c, recording.c, recording.h, poc_recording.c, tasks.md) | 79 reads | ~870381 tok | +| 21:51 | Created ../../../../../../tmp/claude-1000/-home-rshen-projects-xce-worktrees-scheduler-framework/af7e2e71-6a3d-487f-8cd6-2b0ae196cb3a/scratchpad/probe_capnp.py | — | ~1568 | +| 21:51 | Edited ../../../../../../tmp/claude-1000/-home-rshen-projects-xce-worktrees-scheduler-framework/af7e2e71-6a3d-487f-8cd6-2b0ae196cb3a/scratchpad/probe_capnp.py | 26→26 lines | ~188 | +| 21:52 | Edited ../../../../../../tmp/claude-1000/-home-rshen-projects-xce-worktrees-scheduler-framework/af7e2e71-6a3d-487f-8cd6-2b0ae196cb3a/scratchpad/probe_capnp.py | 3→1 lines | ~8 | +| 21:52 | Edited ../../../../../../tmp/claude-1000/-home-rshen-projects-xce-worktrees-scheduler-framework/af7e2e71-6a3d-487f-8cd6-2b0ae196cb3a/scratchpad/probe_capnp.py | 3→1 lines | ~8 | +| 21:52 | Edited ../../../../../../tmp/claude-1000/-home-rshen-projects-xce-worktrees-scheduler-framework/af7e2e71-6a3d-487f-8cd6-2b0ae196cb3a/scratchpad/probe_capnp.py | 3→1 lines | ~7 | +| 21:53 | Edited ../../../../../../tmp/claude-1000/-home-rshen-projects-xce-worktrees-scheduler-framework/af7e2e71-6a3d-487f-8cd6-2b0ae196cb3a/scratchpad/probe_capnp.py | modified enumerate() | ~203 | +| 21:53 | Edited ../../../../../../tmp/claude-1000/-home-rshen-projects-xce-worktrees-scheduler-framework/af7e2e71-6a3d-487f-8cd6-2b0ae196cb3a/scratchpad/probe_capnp.py | 3→4 lines | ~24 | +| 21:53 | Edited ../../../../../../tmp/claude-1000/-home-rshen-projects-xce-worktrees-scheduler-framework/af7e2e71-6a3d-487f-8cd6-2b0ae196cb3a/scratchpad/probe_capnp.py | 3→2 lines | ~13 | +| 21:55 | Edited ../../../../../../tmp/claude-1000/-home-rshen-projects-xce-worktrees-scheduler-framework/af7e2e71-6a3d-487f-8cd6-2b0ae196cb3a/scratchpad/probe_capnp.py | added error handling | ~423 | +| 21:57 | Edited ../../../../../../tmp/claude-1000/-home-rshen-projects-xce-worktrees-scheduler-framework/af7e2e71-6a3d-487f-8cd6-2b0ae196cb3a/scratchpad/probe_capnp.py | modified items() | ~96 | +| 21:59 | Edited ../../../../../../tmp/claude-1000/-home-rshen-projects-xce-worktrees-scheduler-framework/af7e2e71-6a3d-487f-8cd6-2b0ae196cb3a/scratchpad/probe_capnp.py | modified _peek_message() | ~417 | +| 22:03 | Edited src/xceadmin/src/xcecommon/errors.py | modified __init__() | ~286 | +| 22:03 | Edited src/xceadmin/src/xceadmin/scheduler.py | added 1 import(s) | ~91 | +| 22:04 | Edited src/xceadmin/src/xceadmin/scheduler.py | modified record_status() | ~4401 | +| 22:05 | Edited src/xceadmin/src/xceadmin/scheduler.py | modified dump() | ~192 | +| 22:06 | Edited src/xceadmin/tests/test_scheduler.py | expanded (+7 lines) | ~506 | +| 22:06 | Edited src/xceadmin/tests/test_scheduler.py | added error handling | ~2712 | +| 22:11 | Session end: 170 writes across 33 files (test_recording_path_validation.c, recording.c, recording.h, poc_recording.c, tasks.md) | 80 reads | ~885858 tok | +| 22:16 | Created ../../../../../../tmp/claude-1000/-home-rshen-projects-xce-worktrees-scheduler-framework/af7e2e71-6a3d-487f-8cd6-2b0ae196cb3a/scratchpad/probe_capnp.py | — | ~1098 | +| 22:16 | Edited ../../../../../../tmp/claude-1000/-home-rshen-projects-xce-worktrees-scheduler-framework/af7e2e71-6a3d-487f-8cd6-2b0ae196cb3a/scratchpad/probe_capnp.py | 11→11 lines | ~138 | +| 22:17 | Edited ../../../../../../tmp/claude-1000/-home-rshen-projects-xce-worktrees-scheduler-framework/af7e2e71-6a3d-487f-8cd6-2b0ae196cb3a/scratchpad/probe_capnp.py | 10→10 lines | ~133 | +| 22:17 | Edited ../../../../../../tmp/claude-1000/-home-rshen-projects-xce-worktrees-scheduler-framework/af7e2e71-6a3d-487f-8cd6-2b0ae196cb3a/scratchpad/probe_capnp.py | 7→7 lines | ~94 | +| 22:18 | Edited ../../../../../../tmp/claude-1000/-home-rshen-projects-xce-worktrees-scheduler-framework/af7e2e71-6a3d-487f-8cd6-2b0ae196cb3a/scratchpad/collision_schema/controller.capnp | modified mapname() | ~64 | +| 22:25 | Edited src/xceadmin/src/xceadmin/scheduler.py | modified items() | ~463 | +| 22:26 | Edited src/xceadmin/tests/test_scheduler.py | modified test_dump_raises_on_ambiguous_message_shapes() | ~433 | +| 22:28 | Edited .kiro/specs/scheduler-core/tasks.md | added error handling | ~898 | +| 22:34 | Task 10.4 committed (8034fb6e): xceadmin dump() local .replay decoder via shape-based type discrimination; review found+fixed real collision landmine | scheduler.py, errors.py, test_scheduler.py | 201 passed 3 skipped | ~4200 | +| 22:30 | Session end: 178 writes across 34 files (test_recording_path_validation.c, recording.c, recording.h, poc_recording.c, tasks.md) | 81 reads | ~892122 tok | +| 00:30 | Session end: 178 writes across 34 files (test_recording_path_validation.c, recording.c, recording.h, poc_recording.c, tasks.md) | 81 reads | ~892122 tok | +| 00:35 | Edited src/xceadmin/src/xceadmin/cli.py | modified handling() | ~581 | +| 00:36 | Edited src/xceadmin/src/xceadmin/cli.py | added error handling | ~1578 | +| 00:37 | Edited src/xceadmin/tests/test_cli.py | added 3 import(s) | ~207 | +| 00:39 | Edited src/xceadmin/tests/test_cli.py | modified rather() | ~3335 | +| 00:42 | Verified build: meson compile -C xce.build xceadmin (task 10.5, scheduler CLI wiring) | src/xceadmin/src/xceadmin/cli.py, src/xceadmin/tests/test_cli.py | 220 passed 3 skipped (was 201/3) | ~200 | +| 00:42 | Edited .kiro/specs/scheduler-core/tasks.md | modified names() | ~1085 | + +## Session: 2026-08-06 00:53 + +| Time | Action | File(s) | Outcome | ~Tokens | +|------|--------|---------|---------|--------| +| 01:00 | Edited src/xceadmin/tests/test_cli.py | added 1 import(s) | ~76 | +| 01:01 | Edited src/xceadmin/tests/test_cli.py | modified test_scheduler_dump_missing_file_exits_nonzero_with_clean_message() | ~3407 | +| 01:02 | Edited src/xceadmin/tests/test_cli.py | 6→5 lines | ~68 | +| 01:02 | Edited src/xceadmin/tests/test_cli.py | removed 275 lines | ~54 | +| 01:03 | Created src/xceadmin/tests/test_cli_scheduler_mock.py | — | ~4258 | +| 08:05 | Task 10.6: xceadmin scheduler CLI tests complete | src/xceadmin/tests/test_cli_scheduler_mock.py | 7 new tests (record start/stop/status happy-path, redirect-following, error case; dump happy-path+missing-file), all against real CliRunner + real mock TLS server / real fixture file, no production code changed; full suite 227 passed 3 skipped (was 220/3) | ~500 | +| 01:12 | Edited .kiro/specs/scheduler-core/tasks.md | 5→6 lines | ~374 | +| 01:49 | Created src/scheduler/include/sim.h | — | ~3435 | +| 01:50 | Created src/scheduler/testsuite/test_sim_startup.c | — | ~5066 | +| 01:51 | Edited src/scheduler/testsuite/meson.build | modified startup() | ~384 | +| 01:51 | Edited src/scheduler/testsuite/meson.build | 3→6 lines | ~69 | +| 01:52 | Created src/scheduler/src/sim.c | — | ~4335 | +| 01:52 | Edited src/scheduler/meson.build | 4→5 lines | ~30 | +| 01:53 | Edited src/scheduler/testsuite/test_sim_startup.c | modified kill_sched_resval() | ~96 | +| 01:54 | Edited src/scheduler/testsuite/test_sim_startup.c | 3→3 lines | ~36 | +| 01:54 | Edited src/scheduler/testsuite/test_sim_startup.c | 7→7 lines | ~56 | +| 01:54 | Edited src/scheduler/src/sim.c | 8→9 lines | ~88 | +| 01:55 | Edited src/scheduler/src/sim.c | 3→4 lines | ~33 | +| 01:55 | Edited src/scheduler/src/sim.c | 4→9 lines | ~100 | +| 01:56 | Edited src/scheduler/src/sim.c | added 1 condition(s) | ~448 | +| 01:57 | Edited src/scheduler/src/sim.c | reduced (-6 lines) | ~64 | +| 01:58 | Edited src/scheduler/src/main.c | 5→6 lines | ~27 | +| 01:58 | Edited src/scheduler/src/main.c | modified 13() | ~1153 | +| 01:59 | Edited src/scheduler/src/main.c | added 6 condition(s) | ~872 | +| 01:59 | Edited src/scheduler/src/main.c | 2→3 lines | ~15 | +| 02:04 | Task 11.1 complete: sim.c/sim.h simulation-mode startup (AC13 steps 1-4), main.c --sim CLI wiring, 6 new tests | src/scheduler/src/sim.c, src/scheduler/include/sim.h, src/scheduler/src/main.c, src/scheduler/testsuite/test_sim_startup.c | 27/27 scheduler suite PASS, full project build clean | ~145000 | +| 02:14 | Edited src/scheduler/src/sim.c | modified if() | ~100 | +| 02:21 | Edited .kiro/specs/scheduler-core/tasks.md | modified contract() | ~903 | +| 02:43 | Edited src/scheduler/include/plugin.h | modified 17() | ~438 | +| 02:43 | Edited src/scheduler/src/plugin.c | added 4 condition(s) | ~206 | +| 02:44 | Edited src/scheduler/include/plugin.h | modified template() | ~261 | +| 02:44 | Edited src/scheduler/src/plugins/builtin_fcfs.c | added 1 condition(s) | ~384 | +| 02:44 | Edited src/scheduler/src/sim.c | modified c() | ~105 | +| 02:44 | Edited src/scheduler/src/sim.c | modified contract() | ~160 | +| 02:45 | Edited src/scheduler/src/sim.c | 2→2 lines | ~31 | +| 02:45 | Edited src/scheduler/include/sim.h | modified c() | ~584 | +| 02:45 | Edited src/scheduler/include/sim.h | removed 19 lines | ~35 | +| 02:48 | Created src/scheduler/src/sim_loop.c | — | ~6311 | +| 02:49 | Edited src/scheduler/meson.build | 2→3 lines | ~15 | +| 02:49 | Edited src/scheduler/include/plugin.h | removed 45 lines | ~89 | +| 02:50 | Edited src/scheduler/include/plugin.h | modified struct() | ~624 | +| 02:50 | Edited src/scheduler/testsuite/test_sim_startup.c | modified main() | ~246 | +| 02:53 | Created src/scheduler/testsuite/test_sim_event_loop.c | — | ~5228 | +| 02:54 | Edited src/scheduler/testsuite/meson.build | modified advancement() | ~397 | +| 02:54 | Edited src/scheduler/testsuite/meson.build | 3→6 lines | ~70 | +| 02:54 | Edited src/scheduler/testsuite/test_sim_event_loop.c | modified kill_sched_resval() | ~172 | +| 02:55 | Edited src/scheduler/testsuite/test_sim_event_loop.c | modified sched_fill_limit_requests() | ~105 | +| 02:57 | Edited src/scheduler/testsuite/test_sim_event_loop.c | modified unconditionally() | ~426 | +| 02:57 | Edited src/scheduler/testsuite/test_sim_event_loop.c | 6→4 lines | ~45 | +| 02:58 | Edited src/scheduler/src/sim_loop.c | modified if() | ~54 | +| 03:05 | Task 11.2: sim event loop + virtual clock advancement implemented | src/scheduler/src/sim_loop.c, src/scheduler/src/sim.c, src/scheduler/include/{sim.h,plugin.h}, src/scheduler/src/{plugin.c,plugins/builtin_fcfs.c}, testsuite/{test_sim_startup.c,test_sim_event_loop.c,meson.build}, scheduler/meson.build | 28/28 scheduler tests pass, 32/32 project-wide, clang-format clean, full build clean | ~9200 | +| 03:20 | Edited src/scheduler/src/sim_loop.c | modified sched_sim_driver_run() | ~120 | +| 03:21 | Edited src/scheduler/src/sim_loop.c | modified if() | ~23 | +| 03:27 | Edited .kiro/specs/scheduler-core/tasks.md | 6→7 lines | ~857 | +| 03:36 | Created src/scheduler/testsuite/test_sim_isolation.c | — | ~5558 | +| 03:36 | Edited src/scheduler/meson.build | modified 3() | ~287 | +| 03:36 | Edited src/scheduler/testsuite/meson.build | modified advancement() | ~792 | +| 03:37 | Edited src/scheduler/testsuite/meson.build | 3→6 lines | ~71 | +| 03:38 | Edited src/scheduler/testsuite/test_sim_isolation.c | 6→7 lines | ~117 | +| 03:40 | Edited src/scheduler/include/sim.h | modified struct() | ~147 | +| 03:41 | Edited src/scheduler/src/sim.c | 4→5 lines | ~41 | +| 03:42 | Edited src/scheduler/src/sim_loop.c | added 1 condition(s) | ~98 | +| 03:43 | Edited src/scheduler/meson.build | modified 3() | ~287 | +| 03:44 | Edited src/scheduler/testsuite/meson.build | modified Observable() | ~540 | +| 03:44 | Edited src/scheduler/testsuite/meson.build | 3→6 lines | ~71 | +| 03:52 | Task 11.3 complete: sim isolation test (3-layer audit, 0 violations found), 29/29 scheduler + 33/33 project pass | src/scheduler/testsuite/test_sim_isolation.c, meson.build x2 | GREEN, self-review injection-tested | ~2800 | +| 03:59 | Edited src/scheduler/include/sim.h | modified struct() | ~132 | +| 04:01 | Edited src/scheduler/src/sim_loop.c | 6→7 lines | ~55 | +| 04:01 | Edited src/scheduler/src/sim_loop.c | modified sim_print_decision() | ~49 | + +## Session: 2026-08-07 17:07 + +| Time | Action | File(s) | Outcome | ~Tokens | +|------|--------|---------|---------|--------| +| 17:19 | Edited .kiro/specs/scheduler-core/tasks.md | modified struct() | ~491 | +| 17:36 | Created src/scheduler/src/sim_verify.c | — | ~4451 | +| 17:36 | Edited src/scheduler/src/sim_loop.c | modified 5() | ~279 | +| 17:36 | Edited src/scheduler/src/sim_loop.c | expanded (+14 lines) | ~230 | +| 17:37 | Edited src/scheduler/src/sim_loop.c | added 4 condition(s) | ~1202 | +| 17:37 | Edited src/scheduler/meson.build | 2→3 lines | ~15 | +| 17:37 | Edited src/scheduler/testsuite/test_sim_event_loop.c | 3→4 lines | ~30 | +| 17:37 | Edited src/scheduler/testsuite/test_sim_event_loop.c | expanded (+6 lines) | ~179 | +| 17:41 | Created src/scheduler/testsuite/test_sim_verify.c | — | ~9009 | +| 17:41 | Edited src/scheduler/testsuite/meson.build | modified verification() | ~692 | +| 17:41 | Edited src/scheduler/testsuite/meson.build | 3→6 lines | ~68 | +| 17:42 | Edited src/scheduler/src/sim_verify.c | 2→2 lines | ~38 | +| 17:43 | Edited src/scheduler/src/sim_verify.c | modified sim_decision_eq() | ~82 | +| 17:43 | Edited src/scheduler/src/sim_verify.c | modified sim_decision_eq() | ~56 | +| 17:50 | Edited src/scheduler/src/sim_verify.c | modified switch() | ~57 | +| 17:51 | Edited src/scheduler/src/sim_verify.c | modified switch() | ~36 | +| 17:55 | Edited .kiro/specs/scheduler-core/tasks.md | 5→6 lines | ~700 | +| 18:10 | Edited src/scheduler/include/sim.h | modified struct() | ~688 | +| 18:10 | Edited src/scheduler/src/sim.c | modified returns() | ~125 | +| 18:11 | Edited src/scheduler/src/recording_session.c | modified sched_record_session_ctx_new() | ~396 | +| 18:11 | Edited src/scheduler/src/recording_session.c | modified plus() | ~816 | +| 18:11 | Edited src/scheduler/include/recording.h | modified above() | ~271 | +| 18:11 | Edited src/scheduler/include/recording.h | modified above() | ~404 | +| 18:12 | Created src/scheduler/src/sim_record.c | — | ~2490 | +| 18:12 | Edited src/scheduler/src/sim_loop.c | modified struct() | ~632 | +| 18:12 | Edited src/scheduler/src/sim_loop.c | added 2 condition(s) | ~897 | +| 18:13 | Edited src/scheduler/src/sim_loop.c | added 4 condition(s) | ~833 | +| 18:13 | Edited src/scheduler/src/sim_loop.c | 6→7 lines | ~98 | +| 18:13 | Edited src/scheduler/src/sim_loop.c | added 1 condition(s) | ~460 | +| 18:13 | Edited src/scheduler/src/sim_loop.c | added 1 condition(s) | ~281 | +| 18:14 | Edited src/scheduler/src/sim_loop.c | added 2 condition(s) | ~468 | +| 18:14 | Edited src/scheduler/src/sim_loop.c | added 1 condition(s) | ~204 | +| 18:14 | Edited src/scheduler/src/sim_loop.c | expanded (+10 lines) | ~362 | +| 18:15 | Edited src/scheduler/src/sim_loop.c | reduced (-6 lines) | ~272 | +| 18:15 | Edited src/scheduler/src/sim_loop.c | modified struct() | ~283 | +| 18:15 | Edited src/scheduler/src/sim_loop.c | modified returns() | ~209 | +| 18:15 | Edited src/scheduler/src/sim_loop.c | modified 19() | ~292 | +| 18:15 | Edited src/scheduler/src/sim_loop.c | modified 19() | ~103 | +| 18:16 | Edited src/scheduler/src/sim_loop.c | position() → call() | ~147 | +| 18:16 | Edited src/scheduler/src/sim_loop.c | modified 19() | ~104 | +| 18:16 | Edited src/scheduler/src/sim_loop.c | modified returns() | ~128 | +| 18:16 | Edited src/scheduler/meson.build | 3→4 lines | ~21 | +| 18:16 | Edited src/scheduler/meson.build | modified 3() | ~255 | +| 18:16 | Edited src/scheduler/testsuite/test_sim_isolation.c | 9→14 lines | ~266 | +| 18:17 | Edited src/scheduler/testsuite/test_sim_isolation.c | modified XCE_SIM_LOOP_C_PATH() | ~179 | +| 18:17 | Edited src/scheduler/testsuite/test_sim_isolation.c | modified test_sim_source_paths_are_readable() | ~753 | +| 18:17 | Edited src/scheduler/testsuite/test_sim_isolation.c | 3→4 lines | ~63 | +| 18:17 | Edited src/scheduler/testsuite/test_sim_isolation.c | 4→4 lines | ~73 | +| 18:17 | Edited src/scheduler/testsuite/meson.build | modified Observable() | ~532 | +| 18:19 | Created src/scheduler/testsuite/test_sim_alt_events.c | — | ~4522 | +| 18:19 | Edited src/scheduler/testsuite/test_sim_alt_events.c | 6→8 lines | ~125 | +| 18:20 | Created src/scheduler/testsuite/test_sim_record_output.c | — | ~5695 | +| 18:21 | Edited src/scheduler/testsuite/meson.build | modified branching() | ~670 | +| 18:21 | Edited src/scheduler/testsuite/meson.build | expanded (+6 lines) | ~106 | +| 18:23 | Edited src/scheduler/src/sim_record.c | modified Boundary() | ~40 | +| 18:25 | Edited src/scheduler/testsuite/test_sim_event_loop.c | modified for() | ~237 | +| 18:25 | Edited src/scheduler/testsuite/test_sim_event_loop.c | 9→11 lines | ~93 | +| 18:26 | Edited src/scheduler/testsuite/test_sim_event_loop.c | expanded (+6 lines) | ~284 | +| 18:26 | Edited src/scheduler/testsuite/test_sim_verify.c | 9→11 lines | ~93 | +| 18:26 | Edited src/scheduler/testsuite/test_sim_verify.c | 4→7 lines | ~98 | +| 18:26 | Edited src/scheduler/testsuite/test_sim_alt_events.c | 11→12 lines | ~203 | +| 18:26 | Edited src/scheduler/testsuite/test_sim_alt_events.c | 9→11 lines | ~93 | +| 18:41 | Edited src/scheduler/src/sim_loop.c | 2→1 lines | ~24 | +| 19:29 | Edited src/scheduler/src/sim_loop.c | modified PROBE() | ~68 | +| 19:29 | Edited src/scheduler/src/sim_loop.c | modified PROBE() | ~121 | +| 19:29 | Edited src/scheduler/src/sim_loop.c | modified PROBE() | ~91 | +| 19:29 | Edited src/scheduler/src/sim_loop.c | modified PROBE() | ~123 | +| 19:31 | Edited src/scheduler/src/sim_loop.c | 4→2 lines | ~40 | +| 19:31 | Edited src/scheduler/src/sim_loop.c | modified if() | ~70 | +| 19:31 | Edited src/scheduler/src/sim_loop.c | modified if() | ~40 | +| 19:31 | Edited src/scheduler/src/sim_loop.c | modified if() | ~44 | +| 19:35 | RED-phase re-probe: temp in-place edits (not stash) on sim_loop.c only, AC16/AC19 defeated, meson test --suite scheduler = 30/32 real fail (sched_sim_alt_events_test, sched_sim_record_output_test) | src/scheduler/src/sim_loop.c | verified, reverted exactly, 32/32 green restored | ~9500 | +| 19:37 | Edited src/scheduler/src/sim_loop.c | ERROR() → PROBE() | ~90 | +| 19:37 | Edited src/scheduler/src/sim_loop.c | modified PROBE() | ~51 | +| 19:38 | Edited src/scheduler/src/sim_loop.c | PROBE() → ERROR() | ~118 | +| 19:38 | Edited src/scheduler/src/sim_loop.c | 3→2 lines | ~40 | +| 19:42 | Edited .kiro/specs/scheduler-core/tasks.md | 6→7 lines | ~722 | +| 20:07 | Created src/scheduler/testsuite/test_sim_equivalence.c | — | ~8239 | +| 20:08 | Edited src/scheduler/testsuite/meson.build | expanded (+26 lines) | ~479 | +| 20:08 | Edited src/scheduler/testsuite/meson.build | 3→6 lines | ~73 | +| 20:09 | Edited src/scheduler/testsuite/test_sim_equivalence.c | inline fix | ~20 | +| 20:10 | Edited src/scheduler/testsuite/test_sim_equivalence.c | inline fix | ~9 | +| 20:10 | Edited src/scheduler/testsuite/test_sim_equivalence.c | makeString() → strdup() | ~114 | +| 20:10 | Edited src/scheduler/testsuite/test_sim_equivalence.c | 3→7 lines | ~123 | +| 20:11 | Edited src/scheduler/testsuite/test_sim_equivalence.c | modified PROBE() | ~110 | +| 20:11 | Edited src/scheduler/testsuite/test_sim_equivalence.c | reduced (-6 lines) | ~28 | +| 03:15 | Task 11.6 complete: test_sim_equivalence.c (740 lines) records real normal-mode session, replays via real sim driver, asserts decision equivalence; 33/33 scheduler suite green; fixed bug-1503 (makeString bad-free) | src/scheduler/testsuite/test_sim_equivalence.c, src/scheduler/testsuite/meson.build | READY_FOR_REVIEW | ~2200 | +| 20:24 | Edited src/scheduler/testsuite/test_sim_equivalence.c | 2→6 lines | ~89 | +| 20:25 | Edited src/scheduler/testsuite/test_sim_equivalence.c | 6→2 lines | ~35 | +| 20:28 | Edited .kiro/specs/scheduler-core/tasks.md | 5→6 lines | ~590 | +| 20:52 | Edited src/scheduler/include/sim.h | modified struct() | ~683 | +| 20:52 | Created src/scheduler/include/repl.h | — | ~1037 | +| 20:53 | Created src/scheduler/src/repl.c | — | ~3692 | +| 20:54 | Edited src/scheduler/src/sim_loop.c | 6→7 lines | ~39 | +| 20:54 | Edited src/scheduler/src/sim_loop.c | modified if() | ~43 | +| 20:54 | Edited src/scheduler/src/sim_loop.c | modified core_end_session() | ~43 | +| 20:54 | Edited src/scheduler/meson.build | 5→6 lines | ~31 | +| 20:54 | Edited src/scheduler/meson.build | modified 1() | ~178 | +| 20:54 | Edited src/scheduler/meson.build | modified 3() | ~271 | +| 20:54 | Edited src/scheduler/testsuite/test_sim_isolation.c | modified pattern() | ~504 | +| 20:55 | Edited src/scheduler/testsuite/test_sim_isolation.c | modified XCE_SIM_RECORD_C_PATH() | ~169 | +| 20:55 | Edited src/scheduler/testsuite/test_sim_isolation.c | modified XCE_SIM_LIB_PATH() | ~71 | +| 20:55 | Edited src/scheduler/testsuite/test_sim_isolation.c | modified test_sim_source_paths_are_readable() | ~226 | +| 20:55 | Edited src/scheduler/testsuite/test_sim_isolation.c | modified test_sim_never_includes_forbidden_headers() | ~454 | +| 20:55 | Edited src/scheduler/testsuite/test_sim_isolation.c | modified test_sim_never_calls_forbidden_symbols() | ~451 | +| 20:56 | Edited src/scheduler/testsuite/test_sim_isolation.c | 5→5 lines | ~93 | +| 20:56 | Edited src/scheduler/testsuite/meson.build | expanded (+6 lines) | ~141 | +| 20:56 | Edited src/scheduler/testsuite/meson.build | expanded (+18 lines) | ~368 | +| 20:56 | Edited src/scheduler/testsuite/meson.build | 3→6 lines | ~76 | +| 20:57 | Created src/scheduler/testsuite/test_repl_execution_control.c | — | ~6067 | +| 20:59 | Edited src/scheduler/testsuite/test_sim_alt_events.c | 12→15 lines | ~262 | +| 20:59 | Edited src/scheduler/testsuite/test_sim_event_loop.c | 3→8 lines | ~135 | +| 20:59 | Edited src/scheduler/testsuite/test_sim_verify.c | 7→7 lines | ~111 | +| 21:00 | Edited src/scheduler/testsuite/test_sim_record_output.c | 5→6 lines | ~86 | +| 21:00 | Edited src/scheduler/testsuite/test_sim_equivalence.c | 8→10 lines | ~181 | +| 21:01 | Edited src/scheduler/testsuite/meson.build | modified ncurses_dep() | ~202 | + +## Session: 2026-08-07 21:27 + +| Time | Action | File(s) | Outcome | ~Tokens | +|------|--------|---------|---------|--------| +| 23:51 | Edited src/nixSupport/xce.nix | modified replacement() | ~107 | +| 23:51 | Edited src/controller/meson.build | modified replacement() | ~193 | +| 23:51 | Edited src/controller/testsuite/test.c | 2→3 lines | ~58 | +| 23:52 | Edited src/scheduler/meson.build | modified 1() | ~189 | +| 23:52 | Edited src/scheduler/testsuite/meson.build | ncurses_dep() → libedit_dep() | ~188 | +| 23:52 | Edited src/scheduler/testsuite/meson.build | 9→9 lines | ~114 | +| 23:52 | Edited src/scheduler/src/repl.c | 4→3 lines | ~26 | +| 23:53 | Edited src/scheduler/src/repl.c | modified text() | ~563 | +| 23:58 | Session end: 8 writes across 4 files (xce.nix, meson.build, test.c, repl.c) | 13 reads | ~114462 tok | +| 00:37 | Edited src/scheduler/src/snapshot.c | added 1 condition(s) | ~286 | +| 00:52 | Edited src/schema/controller.capnp | modified 3() | ~682 | +| 00:52 | Edited src/include/typemap.h | modified struct() | ~496 | +| 00:54 | Edited src/controller/testsuite/test_e2e_capnp.c | 11→13 lines | ~260 | +| 00:54 | Edited src/controller/testsuite/test_e2e_capnp.c | removed 12 lines | ~28 | +| 00:54 | Edited src/controller/testsuite/test_e2e_capnp.c | 10→7 lines | ~108 | +| 00:57 | Session end: 14 writes across 8 files (xce.nix, meson.build, test.c, repl.c, snapshot.c) | 18 reads | ~177159 tok | +| 01:00 | Edited src/scheduler/src/snapshot.c | removed 16 lines | ~9 | +| 01:01 | Edited src/scheduler/src/snapshot_load.c | 3→3 lines | ~51 | +| 01:06 | Edited .kiro/specs/scheduler-core/tasks.md | added error handling | ~1124 | +| 01:12 | Edited src/scheduler/src/repl.c | modified if() | ~45 | +| 01:12 | Edited src/scheduler/src/repl.c | modified if() | ~36 | +| 01:15 | Edited src/scheduler/src/repl.c | modified if() | ~46 | +| 01:15 | Edited src/scheduler/src/repl.c | modified if() | ~36 | +| 01:16 | Edited .kiro/specs/scheduler-core/tasks.md | 6→7 lines | ~427 | +| 01:20 | Committed task 12.1 (repl execution control) | 17 files, commit 02f7f48f | ~1491 ins | +| 01:18 | Session end: 22 writes across 10 files (xce.nix, meson.build, test.c, repl.c, snapshot.c) | 20 reads | ~207261 tok | +| 01:35 | Edited src/scheduler/include/repl.h | modified Plan() | ~568 | +| 01:35 | Edited src/scheduler/include/repl.h | added 3 condition(s) | ~936 | +| 01:36 | Edited src/scheduler/src/repl.c | modified scope() | ~1105 | +| 01:36 | Edited src/scheduler/src/repl.c | modified enum() | ~1029 | +| 01:37 | Edited src/scheduler/src/repl.c | added 25 condition(s) | ~3439 | +| 01:38 | Edited src/scheduler/src/repl.c | 2→3 lines | ~32 | +| 01:38 | Edited src/scheduler/src/repl.c | added 2 condition(s) | ~602 | +| 01:39 | Edited src/scheduler/src/repl.c | added 52 condition(s) | ~5170 | +| 01:40 | Edited src/scheduler/src/repl.c | modified repl_print_help() | ~384 | +| 01:40 | Edited src/scheduler/src/repl.c | added 24 condition(s) | ~1952 | +| 01:41 | Edited src/scheduler/src/repl.c | added 9 condition(s) | ~1290 | +| 01:44 | Created src/scheduler/src/repl_internal.h | — | ~1974 | +| 01:45 | Created src/scheduler/src/repl.c | — | ~4320 | +| 01:46 | Created src/scheduler/src/repl_break.c | — | ~6211 | +| 01:47 | Created src/scheduler/src/repl_info.c | — | ~6080 | +| 01:48 | Edited src/scheduler/src/sim_loop.c | modified thread() | ~687 | +| 01:49 | Edited src/scheduler/src/sim_loop.c | modified if() | ~33 | +| 01:49 | Edited src/scheduler/src/sim_loop.c | modified 18() | ~59 | +| 01:49 | Edited src/scheduler/src/sim_loop.c | 2→3 lines | ~27 | +| 01:50 | Edited src/scheduler/meson.build | 3→5 lines | ~27 | +| 01:50 | Edited src/scheduler/meson.build | modified 3() | ~350 | +| 01:51 | Edited src/scheduler/testsuite/test_sim_event_loop.c | 2→4 lines | ~30 | +| 01:51 | Edited src/scheduler/testsuite/test_sim_equivalence.c | 2→4 lines | ~30 | +| 01:51 | Edited src/scheduler/testsuite/test_sim_record_output.c | 2→4 lines | ~30 | +| 01:51 | Edited src/scheduler/testsuite/test_sim_alt_events.c | 2→4 lines | ~30 | +| 01:51 | Edited src/scheduler/testsuite/test_sim_verify.c | 2→4 lines | ~30 | +| 01:51 | Edited src/scheduler/testsuite/test_repl_execution_control.c | 2→4 lines | ~30 | +| 01:52 | Edited src/scheduler/testsuite/test_sim_isolation.c | 14→17 lines | ~327 | +| 01:52 | Edited src/scheduler/testsuite/test_sim_isolation.c | 3→3 lines | ~59 | +| 01:52 | Edited src/scheduler/testsuite/test_sim_isolation.c | modified XCE_SIM_REPL_H_PATH() | ~306 | +| 01:53 | Edited src/scheduler/testsuite/test_sim_isolation.c | modified test_sim_source_paths_are_readable() | ~370 | +| 01:53 | Edited src/scheduler/testsuite/test_sim_isolation.c | modified test_sim_never_includes_forbidden_headers() | ~750 | +| 01:53 | Edited src/scheduler/testsuite/test_sim_isolation.c | modified test_sim_never_calls_forbidden_symbols() | ~747 | +| 01:54 | Edited src/scheduler/testsuite/test_sim_isolation.c | 5→6 lines | ~104 | +| 01:54 | Edited src/scheduler/testsuite/test_sim_isolation.c | 12→11 lines | ~177 | +| 01:54 | Edited src/scheduler/testsuite/meson.build | expanded (+9 lines) | ~194 | +| 01:55 | Edited src/scheduler/testsuite/meson.build | modified slice() | ~744 | +| 01:55 | Edited src/scheduler/testsuite/meson.build | 3→6 lines | ~82 | +| 02:00 | Created src/scheduler/testsuite/test_repl_breakpoints_inspection.c | — | ~9034 | +| 02:01 | Edited src/scheduler/src/repl_internal.h | 3→3 lines | ~58 | +| 02:01 | Edited src/scheduler/src/repl_break.c | inline fix | ~20 | +| 02:05 | Edited src/scheduler/testsuite/test_repl_breakpoints_inspection.c | expanded (+6 lines) | ~136 | +| 02:07 | Edited src/scheduler/src/repl_break.c | modified may() | ~326 | +| 02:08 | Edited src/scheduler/src/repl_internal.h | 3→8 lines | ~144 | +| 02:09 | Edited src/scheduler/src/repl_break.c | modified repl_find_event_breakpoint() | ~115 | +| 02:09 | Edited src/scheduler/src/repl_break.c | modified repl_find_event_breakpoint() | ~91 | +| 02:10 | Edited src/scheduler/src/repl_info.c | modified repl_job_state_name() | ~54 | +| 02:11 | Edited src/scheduler/src/repl_info.c | modified repl_job_state_name() | ~31 | +| 02:13 | Edited .kiro/specs/scheduler-core/tasks.md | modified cap() | ~987 | +| 02:29 | Edited src/scheduler/src/repl_info.c | 3→4 lines | ~23 | +| 02:30 | Edited src/scheduler/src/repl_info.c | modified Boundary() | ~163 | +| 02:30 | Edited src/scheduler/src/repl_info.c | 10→12 lines | ~210 | +| 02:30 | Edited src/scheduler/src/repl_info.c | added 2 condition(s) | ~444 | +| 02:32 | Edited src/scheduler/testsuite/test_repl_breakpoints_inspection.c | 3→4 lines | ~45 | +| 02:34 | Edited src/scheduler/testsuite/test_repl_breakpoints_inspection.c | expanded (+19 lines) | ~452 | +| 02:35 | Edited .kiro/specs/scheduler-core/tasks.md | inline fix | ~243 | +| 02:41 | Edited src/scheduler/testsuite/test_repl_breakpoints_inspection.c | 3→4 lines | ~47 | +| 02:41 | Edited src/scheduler/testsuite/test_repl_breakpoints_inspection.c | 4→3 lines | ~30 | +| 02:58 | Edited src/scheduler/src/repl_internal.h | modified logic() | ~792 | +| 02:58 | Edited src/scheduler/src/repl_internal.h | expanded (+26 lines) | ~520 | +| 02:58 | Edited src/scheduler/src/repl_internal.h | expanded (+8 lines) | ~196 | +| 02:59 | Created src/scheduler/testsuite/test_repl_snapshot_params.c | — | ~8064 | +| 03:00 | Edited src/scheduler/testsuite/test_repl_snapshot_params.c | 7→4 lines | ~24 | +| 03:00 | Edited src/scheduler/testsuite/test_repl_snapshot_params.c | 5→4 lines | ~27 | +| 03:00 | Edited src/scheduler/meson.build | 4→5 lines | ~27 | +| 03:00 | Edited src/scheduler/meson.build | modified 3() | ~386 | +| 03:00 | Edited src/scheduler/testsuite/meson.build | 6→9 lines | ~106 | +| 03:01 | Edited src/scheduler/testsuite/meson.build | modified slice() | ~468 | +| 03:01 | Edited src/scheduler/testsuite/meson.build | 3→6 lines | ~81 | +| 03:02 | Created src/scheduler/src/repl_params.c | — | ~4223 | +| 03:02 | Edited src/scheduler/src/repl.c | 12→14 lines | ~255 | +| 03:02 | Edited src/scheduler/src/repl.c | modified repl_print_help() | ~468 | +| 03:02 | Edited src/scheduler/src/repl.c | 6→7 lines | ~105 | +| 03:02 | Edited src/scheduler/src/repl.c | added 2 condition(s) | ~88 | +| 03:03 | Edited src/scheduler/src/repl_info.c | 9→13 lines | ~219 | +| 03:03 | Edited src/scheduler/src/repl_info.c | added 2 condition(s) | ~491 | +| 03:03 | Edited src/scheduler/src/repl_info.c | added 1 condition(s) | ~306 | +| 03:04 | Edited src/scheduler/src/repl_params.c | modified sched_record_start() | ~174 | +| 03:06 | Edited src/scheduler/testsuite/test_sim_isolation.c | 22→24 lines | ~456 | +| 03:06 | Edited src/scheduler/testsuite/test_sim_isolation.c | modified XCE_SIM_REPL_INTERNAL_H_PATH() | ~200 | +| 03:06 | Edited src/scheduler/testsuite/test_sim_isolation.c | modified test_sim_source_paths_are_readable() | ~421 | +| 03:07 | Edited src/scheduler/testsuite/test_sim_isolation.c | modified test_sim_never_includes_forbidden_headers() | ~856 | +| 03:07 | Edited src/scheduler/testsuite/test_sim_isolation.c | modified test_sim_never_calls_forbidden_symbols() | ~853 | +| 03:07 | Edited src/scheduler/testsuite/test_sim_isolation.c | 4→5 lines | ~80 | +| 03:08 | Edited src/scheduler/testsuite/test_sim_isolation.c | 7→7 lines | ~119 | +| 03:08 | Edited src/scheduler/testsuite/test_sim_isolation.c | 4→4 lines | ~62 | +| 03:08 | Edited src/scheduler/testsuite/test_sim_isolation.c | 6→6 lines | ~109 | +| 03:10 | Edited src/scheduler/src/repl_params.c | 4→6 lines | ~89 | +| 03:10 | Edited src/scheduler/src/repl_params.c | 6→4 lines | ~56 | +| 03:11 | Edited .kiro/specs/scheduler-core/tasks.md | modified c() | ~1212 | +| 03:13 | task 12.3: implemented REPL snapshot/set/info-config commands (repl_params.c new file, repl.c/repl_info.c/repl_internal.h wired) + test_repl_snapshot_params.c (7 tests) + test_sim_isolation.c extended | src/scheduler/src/repl_params.c, repl.c, repl_info.c, repl_internal.h; src/scheduler/testsuite/test_repl_snapshot_params.c, test_sim_isolation.c, meson.build x2; 7 sim-family test files got +1 include line | 36/36 scheduler suite GREEN (was 35), RED-phase build failure + synthetic probe both confirmed | ~9500 | +| 03:20 | Edited src/scheduler/src/repl_params.c | 4→2 lines | ~15 | +| 03:20 | Edited src/scheduler/src/repl_params.c | 2→4 lines | ~56 | +| 03:21 | Edited src/scheduler/src/repl_params.c | 5→5 lines | ~34 | +| 03:21 | Edited src/scheduler/src/repl_params.c | 5→5 lines | ~36 | diff --git a/src/scheduler/meson.build b/src/scheduler/meson.build index 7aa94c95..bc8905cd 100644 --- a/src/scheduler/meson.build +++ b/src/scheduler/meson.build @@ -21,6 +21,7 @@ scheduler_srcs = [ 'src/repl.c', 'src/repl_break.c', 'src/repl_info.c', + 'src/repl_params.c', 'src/schedobj.c', 'src/plugin.c', 'src/resource.c', @@ -80,23 +81,24 @@ sched_core_isolation_lib = static_library( # Task 11.3 (scheduler-core spec, Req 5 AC 17): isolated compile target for # the simulation driver's own files, consumed by # testsuite/test_sim_isolation.c to prove at build time that -# sim.c/sim_loop.c/sim_record.c/repl.c/repl_break.c/repl_info.c reference -# zero controller-facing symbols (client/netio/xceapi/delay/idgen). All -# files are compiled together into ONE archive -- task 11.2 added -# sim_loop.c as a sibling file to task 11.1's sim.c, task 11.5 added -# sim_record.c as a third, task 12.1 added repl.c as a fourth, and task -# 12.2 adds repl_break.c/repl_info.c as a fifth/sixth (task 12.2's own -# 800-line-cap split of repl.c's breakpoint/state-inspection logic -- +# sim.c/sim_loop.c/sim_record.c/repl.c/repl_break.c/repl_info.c/ +# repl_params.c reference zero controller-facing symbols (client/netio/ +# xceapi/delay/idgen). All files are compiled together into ONE archive -- +# task 11.2 added sim_loop.c as a sibling file to task 11.1's sim.c, task +# 11.5 added sim_record.c as a third, task 12.1 added repl.c as a fourth, +# task 12.2 added repl_break.c/repl_info.c as a fifth/sixth (task 12.2's +# own 800-line-cap split of repl.c's breakpoint/state-inspection logic -- # repl_internal.h's own "Why a split at all" section has the full -# rationale); covering only a subset of them would repeat core.c's own -# siblings' (core_events.c/core_session.c/core_clock.c) -# never-mechanically-covered gap for the sim driver. Same "compiling to an -# object is enough, no real linking needed" rationale as +# rationale), and task 12.3 adds repl_params.c as a seventh (the same +# split, extended again for `snapshot`/`set`); covering only a subset of +# them would repeat core.c's own siblings' (core_events.c/core_session.c/ +# core_clock.c) never-mechanically-covered gap for the sim driver. Same +# "compiling to an object is enough, no real linking needed" rationale as # sched_core_isolation_lib above. sched_sim_isolation_lib = static_library( 'sched_sim_isolation', ['src/sim.c', 'src/sim_loop.c', 'src/sim_record.c', 'src/repl.c', - 'src/repl_break.c', 'src/repl_info.c'], + 'src/repl_break.c', 'src/repl_info.c', 'src/repl_params.c'], include_directories: incdir, dependencies: scheduler_deps, install: false) diff --git a/src/scheduler/src/repl.c b/src/scheduler/src/repl.c index e5fd32e1..750c9fe1 100644 --- a/src/scheduler/src/repl.c +++ b/src/scheduler/src/repl.c @@ -20,11 +20,13 @@ * This file's own scope: `step [N]`/`continue`/`quit`/`help`, the * pre-replay pause, the blocking interaction model, and repl_dispatch()'s * command table (which now also hands off to repl_break.c's `break`/ - * `delete` and repl_info.c's `info` -- task 12.2, Req 5 AC 22 - * "Breakpoints"/"State inspection"). Breakpoint matching/storage and state- - * inspection dumps are NOT in this file -- task 12.2's full scope did not - * fit in one file under this project's 800-line cap, so it is split across - * repl.c (this file)/repl_break.c/repl_info.c, sharing private state via + * `delete`, repl_info.c's `info` -- task 12.2, Req 5 AC 22 + * "Breakpoints"/"State inspection" -- and repl_params.c's `snapshot`/`set` + * -- task 12.3, Req 5 AC 22 "Snapshots"/"Simulation parameters"). Breakpoint + * matching/storage, state-inspection dumps, and snapshot/parameter commands + * are NOT in this file -- their combined scope does not fit in one file + * under this project's 800-line cap, so it is split across repl.c (this + * file)/repl_break.c/repl_info.c/repl_params.c, sharing private state via * repl_internal.h -- see that header's own "Why a split at all" section for * the full rationale, matching the precedent snapshot.c/snapshot_wire.c/ * snapshot_load.c (task 8.1) already established for the identical @@ -186,10 +188,8 @@ static void repl_print_banner(const xce_sched_sim_driver_t* sim) /* * repl_print_help - Req 5 AC 22's "help" command: "Print available commands * with one-line descriptions." Covers execution control (task 12.1), - * breakpoints, and state inspection (task 12.2) -- AC 22's remaining rows - * (`snapshot`, `set`/`info config`) are task 12.3's job and stay unlisted - * until implemented, matching task 12.1's own "don't over-build entries for - * commands that don't exist yet" convention. + * breakpoints and state inspection (task 12.2), and snapshot capture plus + * simulation parameters (task 12.3) -- every AC 22 command is now listed. */ static void repl_print_help(void) { @@ -207,6 +207,14 @@ static void repl_print_help(void) printf(" info worker |all Dump worker(s).\n"); printf(" info resource |all Dump resource(s).\n"); printf(" info summary Quick overview.\n"); + printf(" snapshot [FILE] Write a standalone " + "snapshot.\n"); + printf(" set speed Set simulation speed.\n"); + printf(" set output |off Start/stop writing " + "decisions.\n"); + printf(" set verify on|off Toggle decision " + "verification.\n"); + printf(" info config Show sim parameters.\n"); } /* @@ -293,8 +301,9 @@ static bool repl_cmd_step(xce_sched_sim_driver_t* sim, const char* line) * unrecognized command, or a `step` parse error). `quit` never returns from * repl_cmd_quit() -- its own return value below is unreachable, kept only * so every branch has a well-typed value. `break`/`delete`/`info` are - * implemented in repl_break.c/repl_info.c (repl_internal.h's own extern - * declarations) -- this file only routes to them. + * implemented in repl_break.c/repl_info.c, and `snapshot`/`set` in + * repl_params.c (repl_internal.h's own extern declarations) -- this file + * only routes to them. */ static bool repl_dispatch(xce_sched_sim_driver_t* sim, const char* line) { @@ -328,6 +337,12 @@ static bool repl_dispatch(xce_sched_sim_driver_t* sim, const char* line) if (strcmp(cmd, "info") == 0) { return repl_cmd_info(sim, line); } + if (strcmp(cmd, "snapshot") == 0) { + return repl_cmd_snapshot(sim, line); + } + if (strcmp(cmd, "set") == 0) { + return repl_cmd_set(sim, line); + } printf("[sim] unknown command: '%s' (type 'help' for a list)\n", cmd); diff --git a/src/scheduler/src/repl_info.c b/src/scheduler/src/repl_info.c index 6a901b52..2cc39e25 100644 --- a/src/scheduler/src/repl_info.c +++ b/src/scheduler/src/repl_info.c @@ -9,14 +9,18 @@ /* * repl_info.c - state inspection (scheduler-core spec, task 12.2, Req 5 AC - * 22 "State inspection"). Sibling of repl.c/repl_break.c -- see - * repl_internal.h's own "Why a split at all" section for the full - * rationale behind this three-file layout. + * 22 "State inspection"; `info config` added by task 12.3, Req 5 AC 22 + * "Simulation parameters"). Sibling of repl.c/repl_break.c/repl_params.c -- + * see repl_internal.h's own "Why a split at all" section for the full + * rationale behind this four-file layout. * * Owns: `info session`/`info job`/`info queue`/`info worker`/ - * `info resource`/`info summary`/`info break`, and the `repl_cmd_info()` - * dispatcher repl.c's repl_dispatch() hands off to (repl_internal.h's own - * extern declaration). + * `info resource`/`info summary`/`info break`/`info config`, and the + * `repl_cmd_info()` dispatcher repl.c's repl_dispatch() hands off to + * (repl_internal.h's own extern declaration). `info config` is the + * read-only counterpart to repl_params.c's `set speed`/`set output`/ + * `set verify` -- grouped here with every other `info` dump rather than in + * repl_params.c, per this file's own domain-ownership scope. * * Every dump below reads core->jobs/queues/workers/resmgr the SAME way * repl.c's own repl_count_jobs() and snapshot.c's @@ -586,6 +590,43 @@ static void repl_info_session(xce_sched_sim_driver_t* sim) } } +/* + * repl_info_config - AC 22's "info config": "Show current sim parameters: + * speed=N, output=, verify=on|off, events_processed=N, + * current_ts=, sessions=N." Read-only counterpart to + * repl_params.c's `set speed`/`set output`/`set verify` (task 12.3) -- + * reads sim->sim_speed_num (the driver-level shadow copy sim.h's own doc + * comment says exists precisely "for the REPL/diagnostics to read without + * reaching into core", kept in sync with the real core.speed_num/speed_den + * atomics by every `set speed` call, repl_params.c's repl_set_speed()), + * sim->record_file/rs->output_path (repl_params.c's own output_path field + * doc comment explains the "path unknown" fallback for a --sim-output + * stream this REPL session never itself opened), sim->verify, and the SAME + * rs->event_index/rs->session_count `info session` already reports + * (repl_info_session() above). + */ +static void repl_info_config(xce_sched_sim_driver_t* sim) +{ + xce_sched_repl_state_t* rs = sim->repl_state; + const char* output_desc; + + if (sim->record_file == NULL) { + output_desc = "off"; + } else if ((rs != NULL) && (rs->output_path != NULL)) { + output_desc = rs->output_path; + } else { + output_desc = "(active, path unknown)"; + } + + printf("[sim] config: speed=%llu output=%s verify=%s " + "events_processed=%llu current_ts=%llu sessions=%llu\n", + (unsigned long long)sim->sim_speed_num, output_desc, + sim->verify ? "on" : "off", + (unsigned long long)((rs != NULL) ? rs->event_index : 0), + (unsigned long long)xce_core_now(&(sim->core)), + (unsigned long long)((rs != NULL) ? rs->session_count : 0)); +} + /* repl_info_break - AC 22's "info break": "List all breakpoints with their * IDs, event types, and filters." */ static void repl_info_break(xce_sched_sim_driver_t* sim) @@ -612,8 +653,8 @@ static void repl_info_break(xce_sched_sim_driver_t* sim) /* * repl_cmd_info - Req 5 AC 22's "info ..." family: dispatches on the second - * token to repl_info_break()/repl_info_session()/repl_info_job()/ - * repl_info_queue()/repl_info_worker()/repl_info_resource()/ + * token to repl_info_break()/repl_info_session()/repl_info_config()/ + * repl_info_job()/repl_info_queue()/repl_info_worker()/repl_info_resource()/ * repl_info_summary() (all above). No `info` subcommand ever hands control * back to the driver loop -- inspecting state is never a reason to advance * the simulation. repl.c's repl_dispatch() calls this (repl_internal.h's @@ -625,8 +666,8 @@ bool repl_cmd_info(xce_sched_sim_driver_t* sim, const char* line) int ntok = repl_split_tokens(line, tokens, XCE_REPL_MAX_TOKENS); if (ntok < 2) { - printf("[sim] info: usage: info " - " [arg]\n"); + printf("[sim] info: usage: info [arg]\n"); return false; } @@ -634,6 +675,8 @@ bool repl_cmd_info(xce_sched_sim_driver_t* sim, const char* line) repl_info_break(sim); } else if (strcmp(tokens[1], "session") == 0) { repl_info_session(sim); + } else if (strcmp(tokens[1], "config") == 0) { + repl_info_config(sim); } else if (strcmp(tokens[1], "summary") == 0) { repl_info_summary(sim); } else if (strcmp(tokens[1], "job") == 0) { diff --git a/src/scheduler/src/repl_internal.h b/src/scheduler/src/repl_internal.h index fd6efe2b..57e552eb 100644 --- a/src/scheduler/src/repl_internal.h +++ b/src/scheduler/src/repl_internal.h @@ -20,31 +20,45 @@ * fit in one file under this project's 800-line-per-file cap * (CLAUDE.md/coding-style.md). Split along the same domain boundaries * task 8.1 already used for snapshot.c/snapshot_wire.c/snapshot_load.c - * (that file's own "Three-file split" header section is the precedent): + * (that file's own "Three-file split" header section is the precedent). + * Task 12.3 (snapshot capture + `set speed`/`set output`/`set verify`) + * adds a FOURTH file, repl_params.c, for the identical reason: neither + * repl_break.c nor repl_info.c had enough remaining headroom under the + * 800-line cap to absorb this task's own real chunk of new logic (both + * were already within ~130-160 lines of the cap after task 12.2): * * - repl.c: the command loop itself (readline()/dispatch), execution * control (`step`/`continue`/`quit`/`help`), and the two task-12.1 * driver-facing entry points (xce_repl_start()/ * xce_repl_post_session_check()). Unchanged in scope from task 12.1 - * except for three new repl_dispatch() branches that hand off to the - * other two files. + * except for repl_dispatch() branches that hand off to the other + * sibling files (task 12.2's `break`/`delete`/`info`, task 12.3's + * `snapshot`/`set`). * - repl_break.c: breakpoint kind/filter matching, the breakpoint list * (create/list/delete), and the three task-12.2 driver-facing hooks * (xce_repl_on_event()/xce_repl_on_begin_round()/xce_repl_on_decision(), * declared in repl.h, implemented here since they are breakpoint-domain * logic through and through). * - repl_info.c: the `info job`/`queue`/`worker`/`resource`/`summary`/ - * `session`/`break` state-inspection dumps. + * `session`/`break`/`config` state-inspection dumps (`info config` is + * task 12.3's own read-only counterpart to repl_params.c's `set` + * commands -- grouped here, not in repl_params.c, since it belongs + * domain-wise with every other `info` dump, matching this file's own + * "Owns" doc comment). + * - repl_params.c (task 12.3): the `snapshot [FILE]` and `set + * speed|output|verify ...` command handlers -- mutating the simulation + * parameters `info config` (repl_info.c) reports. * * `struct xce_sched_repl_state_s` (the full definition of sim.h's opaque * forward declaration) moves here from repl.c for the same reason: - * repl_break.c's breakpoint list and repl_info.c's `info session`/ - * `info break` both need direct field access, not just the opaque pointer - * sim_loop.c gets via sim.h. This does NOT weaken the "opaque outside the - * owning module" property that struct always had -- repl_internal.h is - * never included outside src/scheduler/src/repl*.c, so sim_loop.c and - * every other production consumer still see only the pointer (via sim.h), - * exactly as before this split. + * repl_break.c's breakpoint list, repl_info.c's `info session`/`info + * break`/`info config`, and repl_params.c's `set output` bookkeeping all + * need direct field access, not just the opaque pointer sim_loop.c gets via + * sim.h. This does NOT weaken the "opaque outside the owning module" + * property that struct always had -- repl_internal.h is never included + * outside src/scheduler/src/repl*.c, so sim_loop.c and every other + * production consumer still see only the pointer (via sim.h), exactly as + * before this split. */ typedef enum { @@ -132,6 +146,32 @@ struct xce_sched_repl_state_s { int last_n_dispatch; int last_n_pend; int last_n_preempt; + /* + * Task 12.3 fields (`set output`/`info config`, Req 5 AC 22 + * "Simulation parameters") -- see repl_params.c's + * repl_set_output_file()/repl_set_output_off() for the full field + * contracts. + */ + char* output_path; /* strdup'd path of the output file + * THIS REPL session opened via `set + * output FILE`; NULL when off, OR + * when sim->record_file is active + * but was opened via --sim-output + * before any `set output` command + * ran this session (repl_params.c's + * own doc comment has the full + * "path unknown" rationale). Freed + * and reset to NULL only by + * repl_params.c (`set output off`, + * or a subsequent `set output + * FILE` replacing it). */ + uint64_t output_start_event_index; /* event_index snapshot taken when + * `set output FILE` opened the + * file -- `set output off` + * subtracts this from the CURRENT + * event_index to report "N events + * written". Meaningless while + * output_path == NULL. */ }; /* repl_split_tokens/XCE_REPL_MAX_TOKENS/XCE_REPL_TOKEN_LEN (repl.c) - @@ -159,4 +199,12 @@ extern bool repl_cmd_break(xce_sched_sim_driver_t* sim, const char* line); extern bool repl_cmd_delete(xce_sched_sim_driver_t* sim, const char* line); extern bool repl_cmd_info(xce_sched_sim_driver_t* sim, const char* line); +/* repl_cmd_snapshot/repl_cmd_set (repl_params.c, task 12.3) -- the two + * task-12.3 command handlers repl.c's repl_dispatch() hands off to. Same + * "returns false, re-prompt" contract every other repl_cmd_* above already + * follows -- neither taking a snapshot nor changing a parameter is ever a + * reason to hand control back to the driver loop. */ +extern bool repl_cmd_snapshot(xce_sched_sim_driver_t* sim, const char* line); +extern bool repl_cmd_set(xce_sched_sim_driver_t* sim, const char* line); + #endif diff --git a/src/scheduler/src/repl_params.c b/src/scheduler/src/repl_params.c new file mode 100644 index 00000000..5633a912 --- /dev/null +++ b/src/scheduler/src/repl_params.c @@ -0,0 +1,378 @@ +#include +#include +#include +#include + +#include "repl.h" +#include "repl_internal.h" +#include "utils.h" + +/* + * repl_params.c - snapshot capture and simulation parameter commands + * (scheduler-core spec, task 12.3, Req 5 AC 22 "Snapshots" + "Simulation + * parameters"). Fourth sibling in the repl.c/repl_break.c/repl_info.c + * family -- see repl_internal.h's own "Why a split at all" section for the + * full rationale: this task's own real chunk of new logic (`snapshot` plus + * three `set` sub-commands) does not fit into either repl_break.c or + * repl_info.c's remaining headroom under this project's 800-line cap (both + * were already within ~130-160 lines of the cap after task 12.2), so it + * gets its own file, matching task 12.2's own precedent for the identical + * problem. + * + * Owns: the `snapshot [FILE]` and `set speed|output|verify ...` command + * handlers (repl_cmd_snapshot()/repl_cmd_set(), repl_internal.h's own + * extern declarations; repl.c's repl_dispatch() hands off to both). `info + * config` -- the read-only counterpart that reports the SAME parameters + * this file's `set` command mutates -- lives in repl_info.c instead, + * alongside every other `info` subcommand (that file's own "Owns" doc + * comment), not here. + * + * Boundary (Req 5 AC 17): same as repl.c/repl_break.c/repl_info.c -- only + * xce_sched_sim_driver_t/xce_sched_core_t (via repl.h/repl_internal.h -> + * sim.h -> core.h) plus stdio/string.h/stdlib.h/errno.h. + * + * Reuse, not reimplementation (this task's own explicit instruction): both + * `snapshot` and `set output`'s file-open path write the SAME + * ReplayHeader+SnapshotEntry preamble sim_loop.c's own --sim-output startup + * path writes, via sim_record_write_header_and_snapshot() (sim_record.c, + * task 11.5) -- redeclared here via a local `extern`, the SAME "extern + * declared where consumed" idiom sim_loop.c/sim_record.c themselves already + * use for write_capnp_message()/write_snapshot_entry() (see sim_record.c's + * own file header comment for the precedent this follows). Neither command + * hand-rolls capnp encoding. + * + * `set speed`'s atomic_store_explicit()/memory_order_release calls need no + * `#include ` of their own -- core.h (pulled in transitively + * via repl.h -> sim.h -> core.h) already includes it, the SAME reason + * sim.c's own sim_set_virtual_clock() (the function this file's + * repl_set_speed() mirrors) needs no direct include either. + */ + +extern int sim_record_write_header_and_snapshot(FILE* f, xce_sched_core_t* core, + uint64_t snapshot_ts); + +/* ------------------------------------------------------------------------ + * snapshot [FILE] + * ---------------------------------------------------------------------- */ + +/* XCE_REPL_SNAPSHOT_AUTO_BASENAME - see repl_snapshot_default_path()'s own + * doc comment for why this is a fixed, documented stand-in for the real + * replay basename AC 22's auto-naming rule asks for. */ +#define XCE_REPL_SNAPSHOT_AUTO_BASENAME "sim" + +/* + * repl_snapshot_default_path - AC 22's `snapshot [FILE]` auto-naming rule: + * "-snap-.replay" when FILE is omitted. + * + * Deviation note (this task's own "your judgment, document the deviation" + * allowance -- the same discipline repl.c's repl_count_jobs() doc comment + * already establishes for an identical AC-22-pseudocode gap): + * xce_sched_sim_driver_t (sim.h) retains no string form of the replay + * file's own path -- only a bare FILE* (`replay_file`), opened once at + * startup and never named again; sched_sim_driver_start()'s own `args` + * (which DOES carry `replay_path`) is a local, non-retained parameter, gone + * by the time any REPL command runs. Adding a retained path field to + * xce_sched_sim_driver_t purely to serve this one auto-naming convenience + * was judged not worth the wider blast radius (a sim.h/sim.c change + * touching two OTHER tasks' files, for a fallback branch of a debugging + * convenience) -- instead this uses a fixed, documented basename ("sim") + * in place of the unavailable real one. `event_index` still makes the + * generated name unique across repeated `snapshot` calls in one session + * (never collides); an operator who cares about the base filename can + * always pass one explicitly. + */ +static void repl_snapshot_default_path(xce_sched_sim_driver_t* sim, char* out, + size_t out_sz) +{ + uint64_t event_index + = (sim->repl_state != NULL) ? sim->repl_state->event_index : 0; + + snprintf(out, out_sz, "%s-snap-%llu.replay", XCE_REPL_SNAPSHOT_AUTO_BASENAME, + (unsigned long long)event_index); +} + +/* + * repl_cmd_snapshot - Req 5 AC 22's "snapshot [FILE]": "Write a full + * save_snapshot() to FILE as a standalone .replay file (contains + * ReplayHeader + SnapshotEntry) ... The operator can restart simulation + * from this snapshot via --sim --sim-start-ts + * ." Uses `xce_core_now(&sim->core)` -- the CURRENT + * simulation position -- as both the ReplayHeader's recording_start_ts and + * the SnapshotEntry's own ts (sim_record_write_header_and_snapshot()'s + * `snapshot_ts` parameter), so a `--sim-start-ts ` reload + * lands on exactly this SnapshotEntry (AC 13's own "most recent + * SnapshotEntry with ts <= sim_start_ts" selection rule, satisfied + * trivially since this file's sole SnapshotEntry is the only candidate). + * Deliberately NOT `sim->snapshot_ts` (the ORIGINAL load-time value, sim.h's + * own task 11.5 field) -- AC 22's own wording is "at the current simulation + * position", which has moved on since load if any session has already run. + * + * fopen()s the target path directly -- no sched_validate_record_filename() + * TOCTOU hardening, unlike recording.c's sched_record_start(): that + * hardening exists to protect a LIVE production daemon's --record-dir + * against an adversarial symlink/hardlink swap, and needs an + * xce_scheduler_t pointer plus a record_dir this file family deliberately + * never has (Req 5 AC 17). This REPL command is a trusted operator typing + * a path at an interactive debugging prompt -- the same trust boundary + * sim.c's own --sim-output/--sim-alt-events fopen() calls + * (sched_sim_driver_start(), sim.c) already accept without TOCTOU + * hardening of their own. + * + * Always returns false -- taking a snapshot is never a reason to hand + * control back to the driver loop. + */ +bool repl_cmd_snapshot(xce_sched_sim_driver_t* sim, const char* line) +{ + char tokens[XCE_REPL_MAX_TOKENS][XCE_REPL_TOKEN_LEN]; + int ntok = repl_split_tokens(line, tokens, XCE_REPL_MAX_TOKENS); + char auto_path[XCE_REPL_TOKEN_LEN + 32]; + const char* path; + FILE* f; + uint64_t ts; + int rc; + + if (ntok >= 2) { + path = tokens[1]; + } else { + repl_snapshot_default_path(sim, auto_path, sizeof(auto_path)); + path = auto_path; + } + + f = fopen(path, "w"); + if (f == NULL) { + printf("[sim] snapshot: failed to open '%s': %s\n", path, + strerror(errno)); + return false; + } + + ts = xce_core_now(&(sim->core)); + rc = sim_record_write_header_and_snapshot(f, &(sim->core), ts); + + fflush(f); + fclose(f); + + if (rc < 0) { + printf("[sim] snapshot: failed writing to '%s': rc=%d\n", path, rc); + return false; + } + + printf("[sim] snapshot written: %s (ts=%llu)\n", path, + (unsigned long long)ts); + + return false; +} + +/* ------------------------------------------------------------------------ + * set speed|output|verify ... + * ---------------------------------------------------------------------- */ + +/* + * repl_set_speed - AC 22's "set speed ": mutates BOTH + * sim->sim_speed_num/sim_speed_den (the driver-level shadow copy sim.h's + * own doc comment says exists "for the REPL/diagnostics to read without + * reaching into `core`" -- `info config`'s own speed= field, repl_info.c, + * reads these) AND sim->core.speed_num/speed_den -- the atomic pair + * core_clock.c's xce_core_now() actually consults, FRESH, on every single + * call (no caching) -- via the SAME two atomic_store_explicit() calls + * sim_set_virtual_clock() (sim.c, Req 5 AC 13 Step 4) already performs at + * startup for the identical pair. Mutating only the shadow copy would leave + * `info config` and reality disagreeing forever; sim_loop.c needs no new + * call site for this to take effect -- xce_core_now() is already called + * throughout its event loop on every iteration, so the very next call + * observes the new speed. + * + * `N` = 0 (freeze / "as-fast-as-possible", AC 22's own table wording) is + * accepted here, unlike `step`'s N (repl_cmd_step(), repl.c), which + * rejects N<=0 -- speed=0 is a legitimate, documented mode (core.h's own + * speed_num/speed_den field doc comment: "discrete-simulation freezes via + * speed_num=0"), not a user error. + * + * Deliberately does NOT re-anchor virtual_clock_base/real_clock_base: + * those are only ever re-anchored by sim_advance_virtual_clock() + * (sim_loop.c) at each event/session-boundary jump, and the REPL only ever + * regains control AT one of those jump points (the pre-replay pause, a + * post-session pause, or a breakpoint hit) -- so leaving them untouched + * here introduces no discontinuity in xce_core_now()'s reported value. + */ +static bool repl_set_speed(xce_sched_sim_driver_t* sim, const char* value) +{ + char* endptr; + uint64_t n; + + if (value[0] == '\0') { + printf("[sim] set speed: usage: set speed \n"); + return false; + } + + n = strtoull(value, &endptr, 10); + if (*endptr != '\0') { + printf("[sim] set speed: '%s' is not a valid non-negative integer\n", + value); + return false; + } + + atomic_store_explicit(&(sim->core.speed_num), n, memory_order_release); + atomic_store_explicit(&(sim->core.speed_den), 1, memory_order_release); + sim->sim_speed_num = n; + sim->sim_speed_den = 1; + + printf("[sim] speed set to %llu\n", (unsigned long long)n); + + return false; +} + +/* + * repl_set_output_off - AC 22's "set output off": "Close the current + * output file. info config confirms the path and event count at close + * time." Read here as: the confirmation IS this function's own printed + * line, given at the moment of closing -- not a new obligation for a + * SUBSEQUENT `info config` call to remember a now-closed stream's history + * (which would need its own separate tracking for no real benefit; `info + * config` legitimately reports output=off afterward, per the table's own + * literal format). + * + * "event count" is rs->event_index's delta since this output file was + * opened (rs->output_start_event_index, set by repl_set_output_file() + * below) -- an honest, REPL-visible proxy: sim_loop.c's actual per-event + * write happens inside its own loop + * (sim_process_replay_event_message()/sched_record_session_add_decision()), + * never observed directly by this file; event_index already increments in + * exact lockstep with every event write while sim->record_file stays + * non-NULL (xce_repl_on_event(), repl_break.c), so the delta is exact FOR + * an output file this REPL command itself opened. If --sim-output was + * already active at startup (CLI flag, before this session ever ran `set + * output`), rs->output_path is NULL (repl_set_output_file() is the only + * place that ever sets it) -- reported honestly below as unavailable, not + * fabricated. + */ +static bool repl_set_output_off(xce_sched_sim_driver_t* sim) +{ + xce_sched_repl_state_t* rs = sim->repl_state; + + if (sim->record_file == NULL) { + printf("[sim] set output: already off\n"); + return false; + } + + fflush(sim->record_file); + fclose(sim->record_file); + sim->record_file = NULL; + + if ((rs != NULL) && (rs->output_path != NULL)) { + uint64_t n = rs->event_index - rs->output_start_event_index; + + printf("[sim] output stopped: %s (%llu event(s) written)\n", + rs->output_path, (unsigned long long)n); + free(rs->output_path); + rs->output_path = NULL; + } else { + printf("[sim] output stopped (path/count unavailable -- was opened " + "via --sim-output before this REPL session set one)\n"); + } + + return false; +} + +/* + * repl_set_output_file - AC 22's "set output ": "Start writing + * simulation decisions to FILE in the same .replay capnp stream format as + * --sim-output. Opens ReplayHeader+SnapshotEntry at the current position if + * the file is new." "The current position" is `xce_core_now(&sim->core)` at + * the moment this command runs -- the same "current simulation position" + * reading repl_cmd_snapshot() above uses, for the identical reason (AC 22's + * own wording, not the original load-time `sim->snapshot_ts`). + * + * If sim->record_file is already active (either a prior `set output FILE` + * this session, or --sim-output from the CLI), it is flushed and closed + * first: AC 22's table does not literally describe re-running `set output` + * while already active, but silently leaking the old FILE* or interleaving + * writes into two files at once would both be worse than the documented + * "switch cleanly" behavior implemented here. + */ +static bool repl_set_output_file(xce_sched_sim_driver_t* sim, const char* path) +{ + xce_sched_repl_state_t* rs = sim->repl_state; + FILE* f; + uint64_t ts; + int rc; + + if (sim->record_file != NULL) { + fflush(sim->record_file); + fclose(sim->record_file); + sim->record_file = NULL; + if ((rs != NULL) && (rs->output_path != NULL)) { + free(rs->output_path); + rs->output_path = NULL; + } + } + + f = fopen(path, "w"); + if (f == NULL) { + printf("[sim] set output: failed to open '%s': %s\n", path, + strerror(errno)); + return false; + } + + ts = xce_core_now(&(sim->core)); + rc = sim_record_write_header_and_snapshot(f, &(sim->core), ts); + if (rc < 0) { + printf("[sim] set output: failed writing header/snapshot to '%s': " + "rc=%d\n", + path, rc); + fclose(f); + return false; + } + + sim->record_file = f; + if (rs != NULL) { + rs->output_path = strdup(path); + rs->output_start_event_index = rs->event_index; + } + + printf("[sim] output started: %s\n", path); + + return false; +} + +/* + * repl_cmd_set - Req 5 AC 22's "set ..." family: "set speed " / + * "set output |off" / "set verify on|off". `line` re-tokenized via + * repl_split_tokens() (repl.c), same as break/delete/info. + */ +bool repl_cmd_set(xce_sched_sim_driver_t* sim, const char* line) +{ + char tokens[XCE_REPL_MAX_TOKENS][XCE_REPL_TOKEN_LEN]; + int ntok = repl_split_tokens(line, tokens, XCE_REPL_MAX_TOKENS); + + if (ntok < 3) { + printf("[sim] set: usage: set speed | set output |off | " + "set verify on|off\n"); + return false; + } + + if (strcmp(tokens[1], "speed") == 0) { + return repl_set_speed(sim, tokens[2]); + } + if (strcmp(tokens[1], "output") == 0) { + if (strcmp(tokens[2], "off") == 0) { + return repl_set_output_off(sim); + } + return repl_set_output_file(sim, tokens[2]); + } + if (strcmp(tokens[1], "verify") == 0) { + if (strcmp(tokens[2], "on") == 0) { + sim->verify = true; + } else if (strcmp(tokens[2], "off") == 0) { + sim->verify = false; + } else { + printf("[sim] set verify: usage: set verify on|off\n"); + return false; + } + printf("[sim] verify set to %s\n", sim->verify ? "on" : "off"); + return false; + } + + printf("[sim] set: unrecognized parameter '%s'\n", tokens[1]); + + return false; +} diff --git a/src/scheduler/testsuite/meson.build b/src/scheduler/testsuite/meson.build index b2340651..cd763636 100644 --- a/src/scheduler/testsuite/meson.build +++ b/src/scheduler/testsuite/meson.build @@ -560,6 +560,9 @@ sched_sim_isolation_test = executable( '-DXCE_SIM_REPL_INTERNAL_H_PATH="' + (meson.current_source_dir() / '..' / 'src' / 'repl_internal.h') + '"', + '-DXCE_SIM_REPL_PARAMS_C_PATH="' + + (meson.current_source_dir() / '..' / 'src' / 'repl_params.c') + + '"', '-DXCE_SIM_LIB_PATH="' + sched_sim_isolation_lib.full_path() + '"'], @@ -708,6 +711,31 @@ sched_repl_breakpoints_inspection_test = executable( dependencies: [scheduler_test_deps, libedit_dep], install: false) +# Contract test for the interactive simulation REPL's snapshot-capture and +# simulation-parameter command slice (scheduler-core spec, task 12.3, Req 5 +# AC 22 "Snapshots"/"Simulation parameters"): `snapshot [FILE]` writes a +# standalone .replay file a FRESH sched_sim_driver_start() can load, landing +# on the exact position the snapshot was taken at (the task's own central +# Observable, proved end to end via a forked child + a second, real +# sched_sim_driver_start() call in the parent); `set speed`/`set output`/ +# `set verify` mutate the parameters `info config` reports. Same white-box +# #include pattern as sched_repl_breakpoints_inspection_test above (core.c, +# core_clock.c, core_events.c, core_session.c, decision.c, plugin.c, +# plugins/builtin_fcfs.c, recording.c, recording_session.c, sim.c, +# sim_loop.c, sim_record.c, sim_verify.c, +# snapshot.c/snapshot_wire.c/snapshot_load.c, repl.c, repl_break.c, +# repl_info.c) PLUS the new ../src/repl_params.c under test. Split into its +# own file rather than appended to test_repl_breakpoints_inspection.c -- +# that file was already over 1000 lines after task 12.2, and this task's +# own fixture/assertion surface (a fork+reload round-trip plus five +# further unit-level command tests) does not belong crammed into it. +sched_repl_snapshot_params_test = executable( + 'sched_repl_snapshot_params_test', + ['test_repl_snapshot_params.c'], + include_directories:[incdir], + dependencies: [scheduler_test_deps, libedit_dep], + install: false) + # Register as meson tests so `meson test --suite scheduler` runs them all. # detect_leaks=0 matches the suite convention (fixtures are not torn down). test('scheduler test', scheduler_test, suite: 'scheduler', @@ -804,3 +832,6 @@ test('scheduler repl execution control test', test('scheduler repl breakpoints inspection test', sched_repl_breakpoints_inspection_test, suite: 'scheduler', env: ['ASAN_OPTIONS=detect_leaks=0']) +test('scheduler repl snapshot params test', + sched_repl_snapshot_params_test, suite: 'scheduler', + env: ['ASAN_OPTIONS=detect_leaks=0']) diff --git a/src/scheduler/testsuite/test_repl_breakpoints_inspection.c b/src/scheduler/testsuite/test_repl_breakpoints_inspection.c index e12c0a50..9c95c9d8 100644 --- a/src/scheduler/testsuite/test_repl_breakpoints_inspection.c +++ b/src/scheduler/testsuite/test_repl_breakpoints_inspection.c @@ -81,6 +81,7 @@ #include "../src/repl.c" #include "../src/repl_break.c" #include "../src/repl_info.c" +#include "../src/repl_params.c" #include "../src/sim.c" #include "../src/sim_loop.c" #include "../src/sim_record.c" diff --git a/src/scheduler/testsuite/test_repl_execution_control.c b/src/scheduler/testsuite/test_repl_execution_control.c index e4c72d9a..d3ef499f 100644 --- a/src/scheduler/testsuite/test_repl_execution_control.c +++ b/src/scheduler/testsuite/test_repl_execution_control.c @@ -96,6 +96,7 @@ #include "../src/repl.c" #include "../src/repl_break.c" #include "../src/repl_info.c" +#include "../src/repl_params.c" #include "../src/sim.c" #include "../src/sim_loop.c" #include "../src/sim_record.c" diff --git a/src/scheduler/testsuite/test_repl_snapshot_params.c b/src/scheduler/testsuite/test_repl_snapshot_params.c new file mode 100644 index 00000000..805a8ce0 --- /dev/null +++ b/src/scheduler/testsuite/test_repl_snapshot_params.c @@ -0,0 +1,845 @@ +/* + * test_repl_snapshot_params.c - contract test for the interactive + * simulation REPL's snapshot-capture and simulation-parameter command slice + * (scheduler-core spec, task 12.3, Req 5 AC 22 "Snapshots"/"Simulation + * parameters"). + * + * Observable under test (task 12.3's own text): "a snapshot command + * produces a file that the simulation driver can later load via a fresh + * `--sim` invocation starting from that exact position." Proved end to end + * (test_snapshot_written_file_is_loadable_by_fresh_sim_start below): a + * forked child drives a REAL sched_sim_driver_run() against a real + * fixture, issues `step 1` then `snapshot ` then `quit`, and the + * PARENT then calls the real sched_sim_driver_start() a SECOND time + * against the file the child wrote, asserting it succeeds and that the + * reloaded core state (snapshot_ts, the pushed job) matches what the + * child's session actually produced. + * + * The remaining tests cover `set speed`/`set output`/`set verify`/ + * `info config` and the auto-generated `snapshot` filename, at the same + * "unit-level precision where possible, integration-level proof for the + * one Observable that needs it" split test_repl_execution_control.c's own + * header comment establishes. + * + * Fixture strategy and white-box #include set: identical to + * test_repl_execution_control.c/test_repl_breakpoints_inspection.c (see + * either file's own header comment) PLUS the new ../src/repl_params.c + * under test (task 12.3's own file, repl_internal.h's "Why a split at + * all" section). + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "controller.capnp.h" +#include "kvmgr.h" +#include "scheduler.h" +#include "xstring.h" + +/* Pull in the real functions under test into this TU (white-box #include, + * same convention as test_repl_execution_control.c/ + * test_repl_breakpoints_inspection.c). */ +#include "../src/core.c" +#include "../src/core_clock.c" +#include "../src/core_events.c" +#include "../src/core_session.c" +#include "../src/decision.c" +#include "../src/plugin.c" +#include "../src/plugins/builtin_fcfs.c" +#include "../src/recording.c" +#include "../src/recording_session.c" +#include "../src/repl.c" +#include "../src/repl_break.c" +#include "../src/repl_info.c" +#include "../src/repl_params.c" +#include "../src/sim.c" +#include "../src/sim_loop.c" +#include "../src/sim_record.c" +#include "../src/sim_verify.c" +#include "../src/snapshot.c" +#include "../src/snapshot_load.c" +#include "../src/snapshot_wire.c" + +void kill_sched_job(void* arg) { (void)arg; } +void kill_sched_queue(void* arg) { (void)arg; } +void kill_sched_worker(void* arg) { (void)arg; } +void kill_sched_user(void* arg) { (void)arg; } +void kill_sched_project(void* arg) { (void)arg; } +void kill_sched_resval(xce_val_t* val) { (void)val; } + +void sched_worker_release_host_ctx(xce_sched_worker_t* w) { (void)w; } + +int sched_fill_limit_requests(xce_scheduler_t* sched, xvec_idval_t* vec, + xlist_t* limits) +{ + (void)sched; + (void)vec; + (void)limits; + return 0; +} + +/* Real reimplementation of schedobj.c's sched_resolve_job_picker -- same + * copy every sim-family white-box test carries (schedobj.c is not linked + * here: it would need plugin_register(), which requires a genuine + * xce_scheduler_t* neither this file's fixtures nor sim_loop.c's own + * core-scoped registration path have, Req 5 AC 17). */ +int sched_resolve_job_picker(xce_sched_core_t* core, xce_sched_plugin_t** out) +{ + xce_sched_plugin_t* plugin = NULL; + int rc = 0; + + if ((core == NULL) || (out == NULL)) { + return -EINVAL; + } + + *out = NULL; + + if (core->job_pick_plugin == NULL) { + return -EINVAL; + } + + lock(core->plugins, RTTI_LOCK_READ); + rc = key_find(core->plugins, core->job_pick_plugin, (void**)&plugin); + unlock(core->plugins); + + if (rc <= 0) { + return (rc < 0) ? rc : -ENOENT; + } + + if ((plugin == NULL) || (plugin->picker_iface == NULL) + || (plugin->picker_iface->next_job == NULL) + || (plugin->picker_iface->begin_session == NULL) + || (plugin->picker_iface->end_session == NULL) + || (plugin->picker_iface->on_dispatch == NULL) + || (plugin->picker_iface->on_pend == NULL)) { + return -EINVAL; + } + + *out = plugin; + + return 0; +} + +void setUp(void) { return; } +void tearDown(void) { return; } + +/* ---------------------------------------------------------------------- */ +/* Fixture helpers -- same techniques test_repl_execution_control.c/ */ +/* test_repl_breakpoints_inspection.c already establish. */ +/* ---------------------------------------------------------------------- */ + +static void make_tmp_dir(char* out, size_t out_len, const char* suffix) +{ + char tmpl[PATH_MAX]; + + snprintf(tmpl, sizeof(tmpl), "/tmp/sched_replparams%s_XXXXXX", suffix); + TEST_ASSERT_NOT_NULL(mkdtemp(tmpl)); + TEST_ASSERT_TRUE(strlen(tmpl) < out_len); + strcpy(out, tmpl); +} + +static void build_fixture_scheduler(xce_scheduler_t* sched, char* record_dir) +{ + int rc; + + memset(sched, 0, sizeof(*sched)); + mylock_init(&(sched->record_lock)); + + rc = xce_sched_core_init(&(sched->core)); + TEST_ASSERT_EQUAL_INT(0, rc); + + sched->core.job_pick_plugin = (char*)"builtin_fcfs"; + sched->record_dir = record_dir; +} + +static FILE* write_header_and_snapshot(char* out_path, size_t out_path_len, + const char* suffix) +{ + char record_dir[PATH_MAX]; + xce_scheduler_t sched; + int rc; + + make_tmp_dir(record_dir, sizeof(record_dir), suffix); + build_fixture_scheduler(&sched, record_dir); + + rc = sched_record_start(&sched, "fixture.replay"); + TEST_ASSERT_EQUAL_INT(0, rc); + TEST_ASSERT_TRUE(strlen(sched.record_file_path) < out_path_len); + strcpy(out_path, sched.record_file_path); + + return sched.record_file; +} + +static void append_queue_update_event(FILE* f, uint64_t ts, + const char* queue_name) +{ + xce_queue_t queue; + replay_event_t src; + struct capn c; + struct capn_segment* cs; + ReplayEvent_ptr p; + + memset(&queue, 0, sizeof(queue)); + queue.queue = (char*)queue_name; + queue.status = QUEUE_STAT_OPEN; + queue.priority = 1; + + memset(&src, 0, sizeof(src)); + src.ts = ts; + src.body_which = ReplayEvent_body_queueUpdate; + src.body.queue_update = &queue; + + capn_init_malloc(&c); + cs = capn_root(&c).seg; + encode_ReplayEvent_ptr(cs, &p, &src); + capn_setp(capn_root(&c), 0, p.p); + + TEST_ASSERT_EQUAL_INT(0, write_capnp_message(f, &c)); + capn_free(&c); +} + +static void append_jobs_push_event(FILE* f, uint64_t ts, uint64_t jobid, + const char* queue_name) +{ + job_submit_data_t submit; + xce_job_t job; + xce_job_t* jobs[1]; + xce_jobs_data_t jobs_data; + replay_event_t src; + struct capn c; + struct capn_segment* cs; + ReplayEvent_ptr p; + + memset(&submit, 0, sizeof(submit)); + submit.queue = (char*)queue_name; + submit.user = (char*)"alice"; + submit.mintasks = 1; + submit.maxtasks = 1; + + memset(&job, 0, sizeof(job)); + job.jobid = jobid; + job.state = JOB_STAT_PEND; + job.submit = &submit; + + jobs[0] = &job; + + memset(&jobs_data, 0, sizeof(jobs_data)); + jobs_data.n_total = 1; + jobs_data.count = 1; + jobs_data.jobs = jobs; + + memset(&src, 0, sizeof(src)); + src.ts = ts; + src.body_which = ReplayEvent_body_jobsPush; + src.body.jobs_push = &jobs_data; + + capn_init_malloc(&c); + cs = capn_root(&c).seg; + encode_ReplayEvent_ptr(cs, &p, &src); + capn_setp(capn_root(&c), 0, p.p); + + TEST_ASSERT_EQUAL_INT(0, write_capnp_message(f, &c)); + capn_free(&c); +} + +static void append_replay_session(FILE* f, uint64_t begin_round) +{ + struct capn c; + struct capn_segment* cs; + ReplaySession_ptr sp; + + capn_init_malloc(&c); + cs = capn_root(&c).seg; + sp = new_ReplaySession(cs); + ReplaySession_set_beginRound(sp, begin_round); + capn_setp(capn_root(&c), 0, sp.p); + + TEST_ASSERT_EQUAL_INT(0, write_capnp_message(f, &c)); + capn_free(&c); +} + +static void append_end_of_stream_marker(FILE* f) +{ + replay_event_t src; + struct capn c; + struct capn_segment* cs; + ReplayEvent_ptr p; + + memset(&src, 0, sizeof(src)); + src.body_which = ReplayEvent_body_endSession; + + capn_init_malloc(&c); + cs = capn_root(&c).seg; + encode_ReplayEvent_ptr(cs, &p, &src); + capn_setp(capn_root(&c), 0, p.p); + + TEST_ASSERT_EQUAL_INT(0, write_capnp_message(f, &c)); + capn_free(&c); +} + +static void write_text_file(const char* path, const char* content) +{ + FILE* f = fopen(path, "w"); + + TEST_ASSERT_NOT_NULL(f); + TEST_ASSERT_TRUE(fputs(content, f) >= 0); + fclose(f); +} + +static char* read_whole_file(const char* path) +{ + FILE* f = fopen(path, "rb"); + long len; + char* buf; + + TEST_ASSERT_NOT_NULL(f); + TEST_ASSERT_EQUAL_INT(0, fseek(f, 0, SEEK_END)); + len = ftell(f); + TEST_ASSERT_TRUE(len >= 0); + rewind(f); + + buf = (char*)malloc((size_t)len + 1); + TEST_ASSERT_NOT_NULL(buf); + if (len > 0) { + TEST_ASSERT_EQUAL_INT((int)len, (int)fread(buf, 1, (size_t)len, f)); + } + buf[len] = '\0'; + fclose(f); + + return buf; +} + +/* Same dup()/dup2()-around-STDOUT_FILENO capture technique + * test_repl_execution_control.c's own capture_stdout_around() establishes. */ +static char* capture_stdout_around(void (*fn)(void)) +{ + char tmp[] = "/tmp/xce_replparams_stdoutcap_XXXXXX"; + int fd = mkstemp(tmp); + int saved_stdout; + off_t len; + char* buf; + + TEST_ASSERT_TRUE_MESSAGE(fd >= 0, "mkstemp for stdout capture must " + "succeed"); + + fflush(stdout); + saved_stdout = dup(STDOUT_FILENO); + TEST_ASSERT_TRUE_MESSAGE(saved_stdout >= 0, "dup(STDOUT_FILENO) must " + "succeed"); + TEST_ASSERT_TRUE_MESSAGE(dup2(fd, STDOUT_FILENO) >= 0, + "dup2 redirect of STDOUT_FILENO must succeed"); + + fn(); + + fflush(stdout); + dup2(saved_stdout, STDOUT_FILENO); + close(saved_stdout); + + len = lseek(fd, 0, SEEK_CUR); + TEST_ASSERT_TRUE(len >= 0); + + buf = (char*)calloc(1, (size_t)len + 1); + TEST_ASSERT_NOT_NULL(buf); + TEST_ASSERT_TRUE(lseek(fd, 0, SEEK_SET) == 0); + if (len > 0) { + ssize_t nread = read(fd, buf, (size_t)len); + TEST_ASSERT_TRUE(nread == len); + } + close(fd); + unlink(tmp); + + return buf; +} + +static bool file_exists_nonempty(const char* path) +{ + FILE* f = fopen(path, "rb"); + long len; + + if (f == NULL) { + return false; + } + if (fseek(f, 0, SEEK_END) != 0) { + fclose(f); + return false; + } + len = ftell(f); + fclose(f); + + return len > 0; +} + +/* ---------------------------------------------------------------------- */ +/* Test: `snapshot ` writes a standalone .replay file a FRESH */ +/* sched_sim_driver_start() can load, landing on the exact position the */ +/* snapshot was taken at -- the task's own central Observable. */ +/* ---------------------------------------------------------------------- */ + +static void child_step_snapshot_quit(const char* replay_path, + const char* script_path, + const char* capture_path, + const char* output_dir) +{ + xce_sched_sim_driver_t sim; + sched_sim_args_t args; + + if (chdir(output_dir) != 0) { + _exit(119); + } + if (freopen(script_path, "r", stdin) == NULL) { + _exit(120); + } + if (freopen(capture_path, "w", stdout) == NULL) { + _exit(121); + } + + memset(&sim, 0, sizeof(sim)); + memset(&args, 0, sizeof(args)); + args.replay_path = replay_path; + args.sim_start_ts = SCHED_SIM_START_TS_LATEST; + args.sim_speed = 0; /* frozen virtual clock -- deterministic ts */ + args.interactive = true; + + (void)sched_sim_driver_run(&sim, &args); + + /* Unreachable in a correct implementation -- "quit" must have called + * exit(0) already. */ + fflush(stdout); + _exit(111); +} + +static void test_snapshot_written_file_is_loadable_by_fresh_sim_start(void) +{ + char replay_path[PATH_MAX]; + char output_dir[PATH_MAX]; + char aux_dir[PATH_MAX]; + char script_path[PATH_MAX]; + char capture_path[PATH_MAX]; + char snap_path[PATH_MAX]; + FILE* f; + pid_t pid; + int status; + char* captured; + xce_sched_sim_driver_t sim2; + sched_sim_args_t args2; + int rc; + xce_sched_job_t* job; + + f = write_header_and_snapshot(replay_path, sizeof(replay_path), + "_snapload"); + TEST_ASSERT_NOT_NULL(f); + + append_queue_update_event(f, 1000ULL, "normal"); + append_jobs_push_event(f, 1500ULL, 555ULL, "normal"); + append_replay_session(f, 2000ULL); /* session boundary #1 -- `step 1` + * pauses here, frozen at ts=2000 */ + append_jobs_push_event(f, 2500ULL, 999ULL, "normal"); + append_replay_session(f, 3000ULL); /* must NEVER be reached */ + append_end_of_stream_marker(f); + fclose(f); + + make_tmp_dir(output_dir, sizeof(output_dir), "_snaploadout"); + make_tmp_dir(aux_dir, sizeof(aux_dir), "_snaploadaux"); + snprintf(script_path, sizeof(script_path), "%s/script.txt", aux_dir); + snprintf(capture_path, sizeof(capture_path), "%s/capture.out", aux_dir); + write_text_file(script_path, "step 1\nsnapshot snap_out.replay\nquit\n"); + + pid = fork(); + TEST_ASSERT_TRUE_MESSAGE(pid >= 0, "fork() must succeed"); + + if (pid == 0) { + child_step_snapshot_quit(replay_path, script_path, capture_path, + output_dir); + _exit(112); /* unreachable */ + } + + TEST_ASSERT_EQUAL_INT_MESSAGE(pid, waitpid(pid, &status, 0), + "waitpid must reap the REPL child"); + TEST_ASSERT_TRUE_MESSAGE(WIFEXITED(status), + "the REPL child must exit normally via quit's " + "exit(0)"); + TEST_ASSERT_EQUAL_INT_MESSAGE(0, WEXITSTATUS(status), + "quit's exit(0) is the only correct exit " + "path"); + + captured = read_whole_file(capture_path); + TEST_ASSERT_NOT_NULL_MESSAGE(strstr(captured, "snapshot written"), + "the snapshot command must print a " + "confirmation line"); + free(captured); + + snprintf(snap_path, sizeof(snap_path), "%s/snap_out.replay", output_dir); + TEST_ASSERT_TRUE_MESSAGE(file_exists_nonempty(snap_path), + "the snapshot command must have created a " + "non-empty file at the requested path"); + + /* The central Observable: a FRESH sched_sim_driver_start() against the + * snapshot the REPL just wrote must succeed and land exactly where the + * `snapshot` command was issued -- after session 1, NOT session 2. */ + memset(&sim2, 0, sizeof(sim2)); + memset(&args2, 0, sizeof(args2)); + args2.replay_path = snap_path; + args2.sim_start_ts = SCHED_SIM_START_TS_LATEST; + + rc = sched_sim_driver_start(&sim2, &args2); + TEST_ASSERT_EQUAL_INT_MESSAGE(0, rc, + "a fresh --sim invocation must load the " + "REPL-written snapshot successfully"); + TEST_ASSERT_EQUAL_UINT64_MESSAGE(2000ULL, sim2.snapshot_ts, + "the reloaded snapshot's ts must be " + "exactly the simulation position at " + "the moment `snapshot` was issued " + "(session 1's begin_round, frozen " + "clock)"); + + job = sched_get_job(&(sim2.core), 555ULL); + TEST_ASSERT_NOT_NULL_MESSAGE(job, "job 555 (pushed before session 1) " + "must be present in the reloaded " + "snapshot's state"); + TEST_ASSERT_NULL_MESSAGE(sched_get_job(&(sim2.core), 999ULL), + "job 999 (pushed AFTER session 1, never " + "reached) must NOT be present -- proves the " + "snapshot captured the position at `snapshot` " + "time, not the whole file"); + + printf("test_snapshot_written_file_is_loadable_by_fresh_sim_start: " + "PASS\n"); +} + +/* ---------------------------------------------------------------------- */ +/* Test: `snapshot` with no FILE argument auto-generates a name using the */ +/* REPL's own event_index position counter. */ +/* ---------------------------------------------------------------------- */ + +static void child_step_snapshot_noarg_quit(const char* replay_path, + const char* script_path, + const char* capture_path, + const char* output_dir) +{ + xce_sched_sim_driver_t sim; + sched_sim_args_t args; + + if (chdir(output_dir) != 0) { + _exit(119); + } + if (freopen(script_path, "r", stdin) == NULL) { + _exit(120); + } + if (freopen(capture_path, "w", stdout) == NULL) { + _exit(121); + } + + memset(&sim, 0, sizeof(sim)); + memset(&args, 0, sizeof(args)); + args.replay_path = replay_path; + args.sim_start_ts = SCHED_SIM_START_TS_LATEST; + args.sim_speed = 0; + args.interactive = true; + + (void)sched_sim_driver_run(&sim, &args); + + fflush(stdout); + _exit(111); +} + +static void test_snapshot_auto_generates_filename_from_event_index(void) +{ + char replay_path[PATH_MAX]; + char output_dir[PATH_MAX]; + char aux_dir[PATH_MAX]; + char script_path[PATH_MAX]; + char capture_path[PATH_MAX]; + char expect_path[PATH_MAX]; + FILE* f; + pid_t pid; + int status; + + f = write_header_and_snapshot(replay_path, sizeof(replay_path), + "_snapauto"); + TEST_ASSERT_NOT_NULL(f); + + /* Two translatable events (QUEUE_UPDATE, JOBS_PUSH) before the one + * session boundary -- event_index == 2 by the time `step 1` re-pauses. */ + append_queue_update_event(f, 1000ULL, "normal"); + append_jobs_push_event(f, 1500ULL, 777ULL, "normal"); + append_replay_session(f, 2000ULL); + append_end_of_stream_marker(f); + fclose(f); + + make_tmp_dir(output_dir, sizeof(output_dir), "_snapautoout"); + make_tmp_dir(aux_dir, sizeof(aux_dir), "_snapautoaux"); + snprintf(script_path, sizeof(script_path), "%s/script.txt", aux_dir); + snprintf(capture_path, sizeof(capture_path), "%s/capture.out", aux_dir); + write_text_file(script_path, "step 1\nsnapshot\nquit\n"); + + pid = fork(); + TEST_ASSERT_TRUE_MESSAGE(pid >= 0, "fork() must succeed"); + + if (pid == 0) { + child_step_snapshot_noarg_quit(replay_path, script_path, + capture_path, output_dir); + _exit(112); + } + + TEST_ASSERT_EQUAL_INT_MESSAGE(pid, waitpid(pid, &status, 0), + "waitpid must reap the REPL child"); + TEST_ASSERT_TRUE_MESSAGE(WIFEXITED(status), + "the REPL child must exit normally via quit's " + "exit(0)"); + TEST_ASSERT_EQUAL_INT_MESSAGE(0, WEXITSTATUS(status), + "quit's exit(0) is the only correct exit " + "path"); + + snprintf(expect_path, sizeof(expect_path), "%s/sim-snap-2.replay", + output_dir); + TEST_ASSERT_TRUE_MESSAGE(file_exists_nonempty(expect_path), + "an omitted FILE argument must auto-generate " + "'sim-snap-.replay' in the " + "current directory"); + + printf("test_snapshot_auto_generates_filename_from_event_index: " + "PASS\n"); +} + +/* ---------------------------------------------------------------------- */ +/* Test: `set speed ` mutates BOTH the driver-level shadow copy AND the */ +/* atomic core.speed_num/speed_den pair xce_core_now() actually reads. */ +/* ---------------------------------------------------------------------- */ + +static void test_set_speed_updates_shadow_and_core_atomics(void) +{ + xce_sched_sim_driver_t sim; + bool handled; + + memset(&sim, 0, sizeof(sim)); + + handled = repl_cmd_set(&sim, "set speed 7"); + TEST_ASSERT_FALSE_MESSAGE(handled, "`set` never hands control back"); + TEST_ASSERT_EQUAL_UINT64(7ULL, sim.sim_speed_num); + TEST_ASSERT_EQUAL_UINT64(1ULL, sim.sim_speed_den); + TEST_ASSERT_EQUAL_UINT64( + 7ULL, atomic_load_explicit(&(sim.core.speed_num), memory_order_acquire)); + TEST_ASSERT_EQUAL_UINT64( + 1ULL, atomic_load_explicit(&(sim.core.speed_den), memory_order_acquire)); + + /* speed=0 (freeze / as-fast-as-possible) is a legitimate value, unlike + * `step`'s N (which rejects <= 0). */ + handled = repl_cmd_set(&sim, "set speed 0"); + TEST_ASSERT_FALSE(handled); + TEST_ASSERT_EQUAL_UINT64(0ULL, sim.sim_speed_num); + TEST_ASSERT_EQUAL_UINT64( + 0ULL, atomic_load_explicit(&(sim.core.speed_num), memory_order_acquire)); + + /* Malformed input must be rejected AND leave state unchanged. */ + handled = repl_cmd_set(&sim, "set speed abc"); + TEST_ASSERT_FALSE(handled); + TEST_ASSERT_EQUAL_UINT64_MESSAGE(0ULL, sim.sim_speed_num, + "a malformed `set speed` argument must " + "not change the current speed"); + + printf("test_set_speed_updates_shadow_and_core_atomics: PASS\n"); +} + +/* ---------------------------------------------------------------------- */ +/* Test: `set verify on|off` toggles sim->verify, the exact flag */ +/* sim_loop.c's own decision-verification branch already reads fresh. */ +/* ---------------------------------------------------------------------- */ + +static void test_set_verify_toggles_flag(void) +{ + xce_sched_sim_driver_t sim; + bool handled; + + memset(&sim, 0, sizeof(sim)); + TEST_ASSERT_FALSE(sim.verify); + + handled = repl_cmd_set(&sim, "set verify on"); + TEST_ASSERT_FALSE(handled); + TEST_ASSERT_TRUE(sim.verify); + + handled = repl_cmd_set(&sim, "set verify off"); + TEST_ASSERT_FALSE(handled); + TEST_ASSERT_FALSE(sim.verify); + + /* Malformed value leaves the flag unchanged. */ + handled = repl_cmd_set(&sim, "set verify maybe"); + TEST_ASSERT_FALSE(handled); + TEST_ASSERT_FALSE_MESSAGE(sim.verify, "a malformed `set verify` " + "argument must not change the " + "current flag"); + + printf("test_set_verify_toggles_flag: PASS\n"); +} + +/* ---------------------------------------------------------------------- */ +/* Test: `set output ` opens the file, writes a real */ +/* ReplayHeader+SnapshotEntry preamble (reusing sim_record.c's own writer, */ +/* not hand-rolled), and `set output off` closes it cleanly. */ +/* ---------------------------------------------------------------------- */ + +static void test_set_output_opens_writes_preamble_and_off_closes(void) +{ + xce_sched_sim_driver_t sim; + char tmp_dir[PATH_MAX]; + char out_path[PATH_MAX]; + char cmd[PATH_MAX + 32]; + bool handled; + FILE* check; + struct capn c; + capn_ptr root; + + memset(&sim, 0, sizeof(sim)); + TEST_ASSERT_EQUAL_INT(0, xce_sched_core_init(&(sim.core))); + sim.repl_state = repl_state_ensure(&sim); + TEST_ASSERT_NOT_NULL(sim.repl_state); + + make_tmp_dir(tmp_dir, sizeof(tmp_dir), "_setoutput"); + snprintf(out_path, sizeof(out_path), "%s/out.replay", tmp_dir); + snprintf(cmd, sizeof(cmd), "set output %s", out_path); + + handled = repl_cmd_set(&sim, cmd); + TEST_ASSERT_FALSE(handled); + TEST_ASSERT_NOT_NULL_MESSAGE(sim.record_file, + "`set output FILE` must open sim->record_file"); + TEST_ASSERT_NOT_NULL(sim.repl_state->output_path); + TEST_ASSERT_EQUAL_STRING(out_path, sim.repl_state->output_path); + + /* Flush so the reader below sees the two messages already written. */ + fflush(sim.record_file); + + check = fopen(out_path, "rb"); + TEST_ASSERT_NOT_NULL_MESSAGE(check, "the opened output file must exist " + "on disk"); + TEST_ASSERT_EQUAL_INT_MESSAGE(0, capn_init_fp(&c, check, 0), + "first message must be readable capnp"); + TEST_ASSERT_TRUE_MESSAGE( + msg_root_has_shape(&c, &root, ReplayHeader_word_count, + ReplayHeader_pointer_count), + "first message must be a ReplayHeader"); + capn_free(&c); + + TEST_ASSERT_EQUAL_INT_MESSAGE(0, capn_init_fp(&c, check, 0), + "second message must be readable capnp"); + TEST_ASSERT_TRUE_MESSAGE( + msg_root_has_shape(&c, &root, SnapshotEntry_word_count, + SnapshotEntry_pointer_count), + "second message must be a SnapshotEntry"); + capn_free(&c); + fclose(check); + + handled = repl_cmd_set(&sim, "set output off"); + TEST_ASSERT_FALSE(handled); + TEST_ASSERT_NULL_MESSAGE(sim.record_file, "`set output off` must close " + "sim->record_file"); + TEST_ASSERT_NULL_MESSAGE(sim.repl_state->output_path, + "`set output off` must clear the tracked path"); + + /* A second `off` while already off must be a harmless no-op. */ + handled = repl_cmd_set(&sim, "set output off"); + TEST_ASSERT_FALSE(handled); + TEST_ASSERT_NULL(sim.record_file); + + printf("test_set_output_opens_writes_preamble_and_off_closes: PASS\n"); +} + +/* ---------------------------------------------------------------------- */ +/* Test: `info config` reports speed/output/verify/events_processed/ */ +/* current_ts/sessions, both while output is off and while it is active */ +/* with a known path. */ +/* ---------------------------------------------------------------------- */ + +static xce_sched_sim_driver_t g_cfg_sim; + +static void run_info_config(void) { repl_cmd_info(&g_cfg_sim, "info config"); } + +static void test_info_config_reports_current_parameters(void) +{ + char* captured; + char tmp_dir[PATH_MAX]; + char out_path[PATH_MAX]; + FILE* real_out; + + memset(&g_cfg_sim, 0, sizeof(g_cfg_sim)); + g_cfg_sim.repl_state = repl_state_ensure(&g_cfg_sim); + TEST_ASSERT_NOT_NULL(g_cfg_sim.repl_state); + + g_cfg_sim.sim_speed_num = 3; + g_cfg_sim.sim_speed_den = 1; + g_cfg_sim.verify = true; + g_cfg_sim.repl_state->event_index = 7; + g_cfg_sim.repl_state->session_count = 2; + g_cfg_sim.record_file = NULL; + + captured = capture_stdout_around(run_info_config); + TEST_ASSERT_NOT_NULL(strstr(captured, "speed=3")); + TEST_ASSERT_NOT_NULL(strstr(captured, "output=off")); + TEST_ASSERT_NOT_NULL(strstr(captured, "verify=on")); + TEST_ASSERT_NOT_NULL(strstr(captured, "events_processed=7")); + TEST_ASSERT_NOT_NULL(strstr(captured, "sessions=2")); + TEST_ASSERT_NOT_NULL(strstr(captured, "current_ts=")); + free(captured); + + /* Now with an active, known output path. */ + make_tmp_dir(tmp_dir, sizeof(tmp_dir), "_infocfg"); + snprintf(out_path, sizeof(out_path), "%s/active.replay", tmp_dir); + real_out = fopen(out_path, "w"); + TEST_ASSERT_NOT_NULL(real_out); + + g_cfg_sim.record_file = real_out; + g_cfg_sim.repl_state->output_path = strdup(out_path); + + captured = capture_stdout_around(run_info_config); + TEST_ASSERT_NOT_NULL_MESSAGE(strstr(captured, out_path), + "info config must report the active " + "output path when known"); + free(captured); + + fclose(real_out); + free(g_cfg_sim.repl_state->output_path); + + printf("test_info_config_reports_current_parameters: PASS\n"); +} + +/* ---------------------------------------------------------------------- */ +/* Test: repl_print_help() lists the task 12.3 commands. */ +/* ---------------------------------------------------------------------- */ + +static void run_print_help(void) { repl_print_help(); } + +static void test_help_lists_snapshot_and_set_commands(void) +{ + char* captured = capture_stdout_around(run_print_help); + + TEST_ASSERT_NOT_NULL(strstr(captured, "snapshot")); + TEST_ASSERT_NOT_NULL(strstr(captured, "set speed")); + TEST_ASSERT_NOT_NULL(strstr(captured, "set output")); + TEST_ASSERT_NOT_NULL(strstr(captured, "set verify")); + TEST_ASSERT_NOT_NULL(strstr(captured, "info config")); + free(captured); + + printf("test_help_lists_snapshot_and_set_commands: PASS\n"); +} + +int main(void) +{ + UNITY_BEGIN(); + + RUN_TEST(test_snapshot_written_file_is_loadable_by_fresh_sim_start); + RUN_TEST(test_snapshot_auto_generates_filename_from_event_index); + RUN_TEST(test_set_speed_updates_shadow_and_core_atomics); + RUN_TEST(test_set_verify_toggles_flag); + RUN_TEST(test_set_output_opens_writes_preamble_and_off_closes); + RUN_TEST(test_info_config_reports_current_parameters); + RUN_TEST(test_help_lists_snapshot_and_set_commands); + + return UNITY_END(); +} diff --git a/src/scheduler/testsuite/test_sim_alt_events.c b/src/scheduler/testsuite/test_sim_alt_events.c index 9519632a..b3b549ab 100644 --- a/src/scheduler/testsuite/test_sim_alt_events.c +++ b/src/scheduler/testsuite/test_sim_alt_events.c @@ -89,6 +89,7 @@ #include "../src/repl.c" #include "../src/repl_break.c" #include "../src/repl_info.c" +#include "../src/repl_params.c" #include "../src/sim.c" #include "../src/sim_loop.c" #include "../src/sim_record.c" diff --git a/src/scheduler/testsuite/test_sim_equivalence.c b/src/scheduler/testsuite/test_sim_equivalence.c index 2f0b3a09..60d42da7 100644 --- a/src/scheduler/testsuite/test_sim_equivalence.c +++ b/src/scheduler/testsuite/test_sim_equivalence.c @@ -158,6 +158,7 @@ #include "../src/repl.c" #include "../src/repl_break.c" #include "../src/repl_info.c" +#include "../src/repl_params.c" #include "../src/sim.c" #include "../src/sim_loop.c" #include "../src/sim_record.c" diff --git a/src/scheduler/testsuite/test_sim_event_loop.c b/src/scheduler/testsuite/test_sim_event_loop.c index c1e4fc86..0ff4ee5f 100644 --- a/src/scheduler/testsuite/test_sim_event_loop.c +++ b/src/scheduler/testsuite/test_sim_event_loop.c @@ -104,6 +104,7 @@ #include "../src/repl.c" #include "../src/repl_break.c" #include "../src/repl_info.c" +#include "../src/repl_params.c" #include "../src/sim.c" #include "../src/sim_loop.c" #include "../src/sim_record.c" diff --git a/src/scheduler/testsuite/test_sim_isolation.c b/src/scheduler/testsuite/test_sim_isolation.c index 705a65b4..17ed432c 100644 --- a/src/scheduler/testsuite/test_sim_isolation.c +++ b/src/scheduler/testsuite/test_sim_isolation.c @@ -5,22 +5,24 @@ * from the simulation driver's compiled object" -- plus the AC's own * "confirmed by the struct definition itself, not runtime checks alone" * clause. Scope: sim.h + sim.c + sim_loop.c + sim_record.c + repl.c/repl.h/ - * repl_break.c/repl_info.c/repl_internal.h together (task 11.1 + task - * 11.2's sibling file + task 11.5's sibling file, the --sim-output writer + - * task 12.1's REPL files + task 12.2's own repl_break.c/repl_info.c/ - * repl_internal.h, its 800-line-cap split of repl.c -- see repl_internal.h's - * own "Why a split at all" section) -- NOT a re-audit of core.c (task 4.1's - * own territory, test_core_isolation.c) and NOT plugin.c/builtin_fcfs.c - * (shared normal-mode infra, out of this task's boundary). sim_verify.c - * (task 11.4) is likewise out of scope here -- it is never compiled into + * repl_break.c/repl_info.c/repl_internal.h/repl_params.c together (task + * 11.1 + task 11.2's sibling file + task 11.5's sibling file, the + * --sim-output writer + task 12.1's REPL files + task 12.2's own + * repl_break.c/repl_info.c/repl_internal.h + task 12.3's own repl_params.c, + * each an 800-line-cap split of repl.c -- see repl_internal.h's own "Why a + * split at all" section) -- NOT a re-audit of core.c (task 4.1's own + * territory, test_core_isolation.c) and NOT plugin.c/builtin_fcfs.c (shared + * normal-mode infra, out of this task's boundary). sim_verify.c (task 11.4) + * is likewise out of scope here -- it is never compiled into * sched_sim_isolation_lib below and carries no isolation-sensitive logic of * its own (pure struct comparison, no I/O); extending this guard to it was * not part of task 11.4's own text and is left as-is. repl.c/repl.h were * added per task 12.1's own text (extending this exact 3-file precedent to - * a 5-file one); repl_break.c/repl_info.c/repl_internal.h extend it again - * (task 12.2, to an 8-file one) -- all touch xce_sched_sim_driver_t - * directly or read/write stdin/stdout, the same category of driver-level - * code this guard already covers. + * a 5-file one); repl_break.c/repl_info.c/repl_internal.h extended it again + * (task 12.2, to an 8-file one); repl_params.c extends it once more (task + * 12.3, to a 9-file one) -- all touch xce_sched_sim_driver_t directly or + * read/write stdin/stdout, the same category of driver-level code this + * guard already covers. * * Three layers, extending test_core_isolation.c's two-layer pattern (same * genre: source-text scans of the real, current on-disk files, not a frozen @@ -112,9 +114,13 @@ #error \ "XCE_SIM_REPL_INTERNAL_H_PATH must be defined by meson.build to repl_internal.h's absolute path" #endif +#ifndef XCE_SIM_REPL_PARAMS_C_PATH +#error \ + "XCE_SIM_REPL_PARAMS_C_PATH must be defined by meson.build to repl_params.c's absolute path" +#endif #ifndef XCE_SIM_LIB_PATH #error \ - "XCE_SIM_LIB_PATH must be defined by meson.build to the absolute path of sim.c+sim_loop.c+sim_record.c+repl.c+repl_break.c+repl_info.c's isolated static_library() archive" + "XCE_SIM_LIB_PATH must be defined by meson.build to the absolute path of sim.c+sim_loop.c+sim_record.c+repl.c+repl_break.c+repl_info.c+repl_params.c's isolated static_library() archive" #endif /* Forbidden headers (design.md "Allowed Dependencies", same list @@ -384,15 +390,16 @@ void tearDown(void) { return; } * failure mode this whole test exists to avoid). */ static void test_sim_source_paths_are_readable(void) { - char* h = read_whole_file(XCE_SIM_H_PATH); - char* c = read_whole_file(XCE_SIM_C_PATH); - char* loop = read_whole_file(XCE_SIM_LOOP_C_PATH); - char* record = read_whole_file(XCE_SIM_RECORD_C_PATH); - char* repl_c = read_whole_file(XCE_SIM_REPL_C_PATH); - char* repl_h = read_whole_file(XCE_SIM_REPL_H_PATH); - char* repl_break_c = read_whole_file(XCE_SIM_REPL_BREAK_C_PATH); - char* repl_info_c = read_whole_file(XCE_SIM_REPL_INFO_C_PATH); - char* repl_int_h = read_whole_file(XCE_SIM_REPL_INTERNAL_H_PATH); + char* h = read_whole_file(XCE_SIM_H_PATH); + char* c = read_whole_file(XCE_SIM_C_PATH); + char* loop = read_whole_file(XCE_SIM_LOOP_C_PATH); + char* record = read_whole_file(XCE_SIM_RECORD_C_PATH); + char* repl_c = read_whole_file(XCE_SIM_REPL_C_PATH); + char* repl_h = read_whole_file(XCE_SIM_REPL_H_PATH); + char* repl_break_c = read_whole_file(XCE_SIM_REPL_BREAK_C_PATH); + char* repl_info_c = read_whole_file(XCE_SIM_REPL_INFO_C_PATH); + char* repl_int_h = read_whole_file(XCE_SIM_REPL_INTERNAL_H_PATH); + char* repl_params_c = read_whole_file(XCE_SIM_REPL_PARAMS_C_PATH); TEST_ASSERT_NOT_NULL_MESSAGE(h, XCE_SIM_H_PATH); TEST_ASSERT_NOT_NULL_MESSAGE(c, XCE_SIM_C_PATH); @@ -403,6 +410,7 @@ static void test_sim_source_paths_are_readable(void) TEST_ASSERT_NOT_NULL_MESSAGE(repl_break_c, XCE_SIM_REPL_BREAK_C_PATH); TEST_ASSERT_NOT_NULL_MESSAGE(repl_info_c, XCE_SIM_REPL_INFO_C_PATH); TEST_ASSERT_NOT_NULL_MESSAGE(repl_int_h, XCE_SIM_REPL_INTERNAL_H_PATH); + TEST_ASSERT_NOT_NULL_MESSAGE(repl_params_c, XCE_SIM_REPL_PARAMS_C_PATH); free(h); free(c); @@ -413,19 +421,21 @@ static void test_sim_source_paths_are_readable(void) free(repl_break_c); free(repl_info_c); free(repl_int_h); + free(repl_params_c); } static void test_sim_never_includes_forbidden_headers(void) { - char* h_raw = read_whole_file(XCE_SIM_H_PATH); - char* c_raw = read_whole_file(XCE_SIM_C_PATH); - char* loop_raw = read_whole_file(XCE_SIM_LOOP_C_PATH); - char* record_raw = read_whole_file(XCE_SIM_RECORD_C_PATH); - char* repl_c_raw = read_whole_file(XCE_SIM_REPL_C_PATH); - char* repl_h_raw = read_whole_file(XCE_SIM_REPL_H_PATH); - char* repl_break_c_raw = read_whole_file(XCE_SIM_REPL_BREAK_C_PATH); - char* repl_info_c_raw = read_whole_file(XCE_SIM_REPL_INFO_C_PATH); - char* repl_int_h_raw = read_whole_file(XCE_SIM_REPL_INTERNAL_H_PATH); + char* h_raw = read_whole_file(XCE_SIM_H_PATH); + char* c_raw = read_whole_file(XCE_SIM_C_PATH); + char* loop_raw = read_whole_file(XCE_SIM_LOOP_C_PATH); + char* record_raw = read_whole_file(XCE_SIM_RECORD_C_PATH); + char* repl_c_raw = read_whole_file(XCE_SIM_REPL_C_PATH); + char* repl_h_raw = read_whole_file(XCE_SIM_REPL_H_PATH); + char* repl_break_c_raw = read_whole_file(XCE_SIM_REPL_BREAK_C_PATH); + char* repl_info_c_raw = read_whole_file(XCE_SIM_REPL_INFO_C_PATH); + char* repl_int_h_raw = read_whole_file(XCE_SIM_REPL_INTERNAL_H_PATH); + char* repl_params_c_raw = read_whole_file(XCE_SIM_REPL_PARAMS_C_PATH); char* h; char* c; char* loop; @@ -435,6 +445,7 @@ static void test_sim_never_includes_forbidden_headers(void) char* repl_break_c; char* repl_info_c; char* repl_int_h; + char* repl_params_c; TEST_ASSERT_NOT_NULL(h_raw); TEST_ASSERT_NOT_NULL(c_raw); @@ -445,16 +456,18 @@ static void test_sim_never_includes_forbidden_headers(void) TEST_ASSERT_NOT_NULL(repl_break_c_raw); TEST_ASSERT_NOT_NULL(repl_info_c_raw); TEST_ASSERT_NOT_NULL(repl_int_h_raw); + TEST_ASSERT_NOT_NULL(repl_params_c_raw); - h = strip_c_comments(h_raw); - c = strip_c_comments(c_raw); - loop = strip_c_comments(loop_raw); - record = strip_c_comments(record_raw); - repl_c = strip_c_comments(repl_c_raw); - repl_h = strip_c_comments(repl_h_raw); - repl_break_c = strip_c_comments(repl_break_c_raw); - repl_info_c = strip_c_comments(repl_info_c_raw); - repl_int_h = strip_c_comments(repl_int_h_raw); + h = strip_c_comments(h_raw); + c = strip_c_comments(c_raw); + loop = strip_c_comments(loop_raw); + record = strip_c_comments(record_raw); + repl_c = strip_c_comments(repl_c_raw); + repl_h = strip_c_comments(repl_h_raw); + repl_break_c = strip_c_comments(repl_break_c_raw); + repl_info_c = strip_c_comments(repl_info_c_raw); + repl_int_h = strip_c_comments(repl_int_h_raw); + repl_params_c = strip_c_comments(repl_params_c_raw); TEST_ASSERT_NOT_NULL(h); TEST_ASSERT_NOT_NULL(c); TEST_ASSERT_NOT_NULL(loop); @@ -464,6 +477,7 @@ static void test_sim_never_includes_forbidden_headers(void) TEST_ASSERT_NOT_NULL(repl_break_c); TEST_ASSERT_NOT_NULL(repl_info_c); TEST_ASSERT_NOT_NULL(repl_int_h); + TEST_ASSERT_NOT_NULL(repl_params_c); assert_no_forbidden_includes(h, "sim.h"); assert_no_forbidden_includes(c, "sim.c"); @@ -474,6 +488,7 @@ static void test_sim_never_includes_forbidden_headers(void) assert_no_forbidden_includes(repl_break_c, "repl_break.c"); assert_no_forbidden_includes(repl_info_c, "repl_info.c"); assert_no_forbidden_includes(repl_int_h, "repl_internal.h"); + assert_no_forbidden_includes(repl_params_c, "repl_params.c"); free(h_raw); free(c_raw); @@ -484,6 +499,7 @@ static void test_sim_never_includes_forbidden_headers(void) free(repl_break_c_raw); free(repl_info_c_raw); free(repl_int_h_raw); + free(repl_params_c_raw); free(h); free(c); free(loop); @@ -493,19 +509,21 @@ static void test_sim_never_includes_forbidden_headers(void) free(repl_break_c); free(repl_info_c); free(repl_int_h); + free(repl_params_c); } static void test_sim_never_calls_forbidden_symbols(void) { - char* h_raw = read_whole_file(XCE_SIM_H_PATH); - char* c_raw = read_whole_file(XCE_SIM_C_PATH); - char* loop_raw = read_whole_file(XCE_SIM_LOOP_C_PATH); - char* record_raw = read_whole_file(XCE_SIM_RECORD_C_PATH); - char* repl_c_raw = read_whole_file(XCE_SIM_REPL_C_PATH); - char* repl_h_raw = read_whole_file(XCE_SIM_REPL_H_PATH); - char* repl_break_c_raw = read_whole_file(XCE_SIM_REPL_BREAK_C_PATH); - char* repl_info_c_raw = read_whole_file(XCE_SIM_REPL_INFO_C_PATH); - char* repl_int_h_raw = read_whole_file(XCE_SIM_REPL_INTERNAL_H_PATH); + char* h_raw = read_whole_file(XCE_SIM_H_PATH); + char* c_raw = read_whole_file(XCE_SIM_C_PATH); + char* loop_raw = read_whole_file(XCE_SIM_LOOP_C_PATH); + char* record_raw = read_whole_file(XCE_SIM_RECORD_C_PATH); + char* repl_c_raw = read_whole_file(XCE_SIM_REPL_C_PATH); + char* repl_h_raw = read_whole_file(XCE_SIM_REPL_H_PATH); + char* repl_break_c_raw = read_whole_file(XCE_SIM_REPL_BREAK_C_PATH); + char* repl_info_c_raw = read_whole_file(XCE_SIM_REPL_INFO_C_PATH); + char* repl_int_h_raw = read_whole_file(XCE_SIM_REPL_INTERNAL_H_PATH); + char* repl_params_c_raw = read_whole_file(XCE_SIM_REPL_PARAMS_C_PATH); char* h; char* c; char* loop; @@ -515,6 +533,7 @@ static void test_sim_never_calls_forbidden_symbols(void) char* repl_break_c; char* repl_info_c; char* repl_int_h; + char* repl_params_c; TEST_ASSERT_NOT_NULL(h_raw); TEST_ASSERT_NOT_NULL(c_raw); @@ -525,16 +544,18 @@ static void test_sim_never_calls_forbidden_symbols(void) TEST_ASSERT_NOT_NULL(repl_break_c_raw); TEST_ASSERT_NOT_NULL(repl_info_c_raw); TEST_ASSERT_NOT_NULL(repl_int_h_raw); + TEST_ASSERT_NOT_NULL(repl_params_c_raw); - h = strip_c_comments(h_raw); - c = strip_c_comments(c_raw); - loop = strip_c_comments(loop_raw); - record = strip_c_comments(record_raw); - repl_c = strip_c_comments(repl_c_raw); - repl_h = strip_c_comments(repl_h_raw); - repl_break_c = strip_c_comments(repl_break_c_raw); - repl_info_c = strip_c_comments(repl_info_c_raw); - repl_int_h = strip_c_comments(repl_int_h_raw); + h = strip_c_comments(h_raw); + c = strip_c_comments(c_raw); + loop = strip_c_comments(loop_raw); + record = strip_c_comments(record_raw); + repl_c = strip_c_comments(repl_c_raw); + repl_h = strip_c_comments(repl_h_raw); + repl_break_c = strip_c_comments(repl_break_c_raw); + repl_info_c = strip_c_comments(repl_info_c_raw); + repl_int_h = strip_c_comments(repl_int_h_raw); + repl_params_c = strip_c_comments(repl_params_c_raw); TEST_ASSERT_NOT_NULL(h); TEST_ASSERT_NOT_NULL(c); TEST_ASSERT_NOT_NULL(loop); @@ -544,6 +565,7 @@ static void test_sim_never_calls_forbidden_symbols(void) TEST_ASSERT_NOT_NULL(repl_break_c); TEST_ASSERT_NOT_NULL(repl_info_c); TEST_ASSERT_NOT_NULL(repl_int_h); + TEST_ASSERT_NOT_NULL(repl_params_c); assert_no_forbidden_symbols(h, "sim.h"); assert_no_forbidden_symbols(c, "sim.c"); @@ -554,6 +576,7 @@ static void test_sim_never_calls_forbidden_symbols(void) assert_no_forbidden_symbols(repl_break_c, "repl_break.c"); assert_no_forbidden_symbols(repl_info_c, "repl_info.c"); assert_no_forbidden_symbols(repl_int_h, "repl_internal.h"); + assert_no_forbidden_symbols(repl_params_c, "repl_params.c"); free(h_raw); free(c_raw); @@ -564,6 +587,7 @@ static void test_sim_never_calls_forbidden_symbols(void) free(repl_break_c_raw); free(repl_info_c_raw); free(repl_int_h_raw); + free(repl_params_c_raw); free(h); free(c); free(loop); @@ -573,6 +597,7 @@ static void test_sim_never_calls_forbidden_symbols(void) free(repl_break_c); free(repl_info_c); free(repl_int_h); + free(repl_params_c); } /* @@ -617,11 +642,12 @@ static void test_sim_driver_struct_has_no_forbidden_fields(void) /* * test_sim_object_has_no_forbidden_undefined_symbols - the literal * build-time/link-time proof: sim.c + sim_loop.c + sim_record.c + repl.c + - * repl_break.c + repl_info.c, compiled together in isolation into - * sched_sim_isolation_lib (meson.build), must not reference (as an - * undefined external symbol in the compiled archive) anything matching the - * forbidden prefixes above. Independent of what any header transitively - * declares -- reflects only what this driver's compiled code actually + * repl_break.c + repl_info.c + repl_params.c, compiled together in + * isolation into sched_sim_isolation_lib (meson.build), must not reference + * (as an undefined external symbol in the compiled archive) anything + * matching the forbidden prefixes above. Independent of what any header + * transitively declares -- reflects only what this driver's compiled code + * actually * calls. Same technique and same any_undefined_seen vacuous-pass guard as * test_core_isolation.c's test_core_object_has_no_forbidden_undefined_symbols(). */ @@ -646,9 +672,9 @@ static void test_sim_object_has_no_forbidden_undefined_symbols(void) /* posix format for an undefined symbol: " U" (no * value/size columns). Skip anything that doesn't parse as * exactly that -- e.g. a blank line, or an archive member header - * some nm versions print (this archive has six member objects, + * some nm versions print (this archive has seven member objects, * sim.c.o, sim_loop.c.o, sim_record.c.o, repl.c.o, repl_break.c.o, - * and repl_info.c.o). */ + * repl_info.c.o, and repl_params.c.o). */ if (sscanf(line, "%511s %c", symbol, &type) != 2) { continue; } diff --git a/src/scheduler/testsuite/test_sim_record_output.c b/src/scheduler/testsuite/test_sim_record_output.c index 3b547818..dc2c9724 100644 --- a/src/scheduler/testsuite/test_sim_record_output.c +++ b/src/scheduler/testsuite/test_sim_record_output.c @@ -76,6 +76,7 @@ #include "../src/repl.c" #include "../src/repl_break.c" #include "../src/repl_info.c" +#include "../src/repl_params.c" #include "../src/sim.c" #include "../src/sim_loop.c" #include "../src/sim_record.c" diff --git a/src/scheduler/testsuite/test_sim_verify.c b/src/scheduler/testsuite/test_sim_verify.c index 5a4f86bd..3ed30563 100644 --- a/src/scheduler/testsuite/test_sim_verify.c +++ b/src/scheduler/testsuite/test_sim_verify.c @@ -96,6 +96,7 @@ #include "../src/repl.c" #include "../src/repl_break.c" #include "../src/repl_info.c" +#include "../src/repl_params.c" #include "../src/sim.c" #include "../src/sim_loop.c" #include "../src/sim_record.c"