Commit graph

1159 commits

Author SHA1 Message Date
5ab344e1ae
fix(apiserver): propagate submit user as job execuser (bug-1780)
modelJobToSubmit() never set job_startinfo_t.execuser when converting
a submitted job to the C struct sent to the controller/worker, so
every job ran as the worker's 'nobody' default and failed to exec
(exit 126). Default execuser to the submitting user via a new
resolveExecUser() pure helper, and free the newly allocated C string.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Hb6Edtj4KbS8CrgEWYdA2E
2026-09-07 08:10:10 +00:00
0d3f925c4d
fix(scheduler): retire reconciled jobs from picker candidates (bug-1759/1779)
sim_reconcile_unaccounted_jobs() (the mechanism that replays historical
DISPATCH decisions in --sim mode) reconstructed a job's allocation but
never called the picker's on_dispatch() hook, unlike the live allocator
path which always does. The job therefore stayed a live candidate in the
picker's own pendJobs bookkeeping, and sim_picker_thread_fn() (which
unconditionally PENDs whatever it is still offered) kept re-offering and
re-PENDing it every subsequent round -- live-observed as an already-
dispatched job receiving a bogus PEND decision (bug-1759: 22/50 jobs in
one batch; bug-1779: 5/10 array-job elements).

This directly contradicts scheduler-sim-decision-parity's own design.md,
which already documented "the picker never re-decides an already-resolved
job" as the correct assumption -- the implementation had drifted from it.
Fixed by calling on_dispatch() immediately after reconstructing the job's
allocation, mirroring sched_allocator()'s own live-dispatch ordering.

test_sim_verify_no_false_mismatch_across_round_separated_dispatch's own
fixture had baked in the stale-PEND symptom as "expected" data; updated to
match the corrected behavior (dispatch-only round 1, empty filler rounds).
Both design docs updated in place with correction notes.
2026-09-07 07:32:18 +00:00
ef3c4670a6
fix(scheduler): exclude terminal jobs from assigned-jobs listing (bug-1778)
info worker's assigned-jobs listing gated only on job->allocs != NULL,
so a job already at a terminal state (DONE/EXIT) could still appear as
"assigned" if its allocs pointer was ever left stale -- live-observed via
a real captured replay, three DONE jobs kept showing as assigned to a
worker despite the worker's own slot-usage accounting correctly excluding
them. Root cause of the stale allocs itself is not yet pinned (suspected
snapshot save/load inconsistency); this adds the same defensive "terminal
is permanent" exclusion job_state_regresses_terminal()/sim_reconcile_
unaccounted_jobs() already apply elsewhere, at the display layer.
2026-09-07 07:32:08 +00:00
e395eaa3ad
fix(frontend): count host job-state columns and EXEC_HOST by real slots
NJOBS/RUN/SSUSP/USUSP (xcecli hosts list -l) and EXEC_HOST (xcecli jobs
list) always showed a hardcoded 1 slot per host regardless of a job's
actual per-host allocation -- rawJobToModel()'s ExecHosts JSONB builder
never read the real per-host "slots" resource from the C job allocation,
so the already-correct downstream slot-counting logic (computeHostSlotUsage,
bug-1743) and EXEC_HOST's "N*host" formatter never had real data to work
with. convert.go now extracts each host's real slots allocation via a new
CGO indexing helper, mirroring the scheduler's own get_nslots_in_alloc()
convention.

Also wires hosts list -l's "CURRENT LOAD USED FOR SCHEDULING" table (tmp/
swap/mem/slots and friends) to the host's real live resource values --
previously entirely hardcoded placeholder strings, never connected to any
backend data.
2026-09-07 07:32:00 +00:00
a35f597e06
fix(worker): real maxswap/maxtmp detection; feat(scheduler): annotate multi-slot assigned jobs
bug-1554: init_host_attrs() replaces hardcoded placeholder stubs with real
detection -- maxswap from sysinfo()'s totalswap (reusing the same syscall
snapshot already taken for maxmem), maxtmp from a fresh statvfs("/tmp")
total-capacity measurement. No synthetic fallback on detection failure, so
a system with swap genuinely disabled correctly reports 0.

bug-1776: the scheduler simulator's "info worker" assigned-jobs listing now
annotates a job consuming more than one "slots" unit (e.g. "jobid*2"),
using per-host allocation data that was already being walked but discarded.
Falls back to the pre-existing per-job listing when jobs on the same worker
have differing slot counts, since a single suffix on a compressed range
would be ambiguous.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Hb6Edtj4KbS8CrgEWYdA2E
2026-09-07 04:39:22 +00:00
c454f0652d
test(resreq-maxtasks-wiring): fix deferred queue-priority assertion
test_info_job_reports_state_queue_priority now expects "Tasks: min =
1, max = 1" instead of the pre-fix "min = 0, max = 0" incidental
byproduct (a fixture's parse_select() failing on an unregistered
"type" resource used to leave the primary's task-count fields at
memset-zero; apply_job_task_count() now correctly overwrites them to
the job's real submitted default of 1/1). This was the one deferred
item tracked in tasks.md's Implementation Notes since task 2.1.

Full-suite regression confirmed clean: scheduler/controller/worker
suites pass except the pre-existing, unrelated bug-1758 (predates
this feature, attributed to a separate spec). A full /xce-build all
also surfaces bug-1723, a pre-existing lib-suite fixture-teardown
leak dating to before this feature and outside Requirement 5.1's
named scope (scheduler/controller/worker) -- confirmed untouched by
any of this feature's commits.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XYdtrEWWZfKRRGED7raEYg
2026-09-06 10:07:49 +00:00
d1849c94d8
test(resreq-maxtasks-wiring): retire multislot test's bug-1770 workaround
test_parjob_e2e_multislot.c's standing "bug-1770" assertion now
expects the correct, real primary->minTasks/maxTasks (1/JOB_MAXTASKS)
instead of the buggy always-1 value it used to document. The
PATCH_MAXTASKS_FOR_BUG_1770 block that manually overwrote the
already-ingested resreq's task-count fields is removed entirely --
the full multi-host allocation scenario (2-host commit, per-host
slots) now runs end to end through unmodified real ingest. Header
comment updated to record that bug-1770 is fixed by this feature
rather than describing a live gap.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XYdtrEWWZfKRRGED7raEYg
2026-09-06 07:49:38 +00:00
c36014c072
test(resreq-maxtasks-wiring): verify driver parity, update-immunity, cache
Extends test_maxtasks_wiring_ingest.c with two tests: (1) a resource-
request update never changes a job's already-dispatched task count,
because core_events.c's live JOB_UPDATE handler never rebuilds
job->schedobj (Requirement 1.4); (2) two otherwise-identical jobs
differing only in requested task count don't collide in the
unsatisfiable-resource-request cache on the fresh-ingest path
(Requirement 5.4).

Adds a new file, test_maxtasks_wiring_sim_driver_parity.c, proving the
simulation driver's real static translator chain
(sim_process_replay_event_message()/replay_event_to_core_event())
applies this feature's wiring identically to the real driver -- a
genuine white-box test of the real sim_loop.c code (following
test_sim_event_loop.c's established #include technique), not a mirror
of its logic. This was split into its own file because
test_maxtasks_wiring_ingest.c already links the full scheduler_srcs
object list for its own task-3.1 test, which collides at link time
with white-box #include-ing core_events.c/sim_loop.c in one
translation unit; see tasks.md's Implementation Notes.

No production code changed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XYdtrEWWZfKRRGED7raEYg
2026-09-06 07:24:04 +00:00
4644d20cf0
test(resreq-maxtasks-wiring): verify ingest wiring parity and no-regression
Adds test_maxtasks_wiring_ingest.c proving: (1) fresh ingest's
dispatched primary resreq reflects a real multi-task job's requested
count via the real core_build_job()/JOBS_PUSH path; (2) fresh ingest
and snapshot-restore ingest derive the identical task count for an
equivalent job, asserted both against expected constants and via
direct cross-comparison (Requirement 5.6); (3) a single-task job
reaches identical real host-selection/allocation behavior through the
live allocator dispatch chain, proving the feature is a true no-op
for maxTasks==1 (Requirements 3.1/3.2). No production code changed --
tasks 2.1/2.2 already wired the fix; this adds cross-call-site and
behavioral proof neither of those isolated tests attempted.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XYdtrEWWZfKRRGED7raEYg
2026-09-06 06:01:00 +00:00
f02bedc52b
test(resreq-maxtasks-wiring): close worker launch-type dispatch coverage gap
Adds white-box tests for job_start_remote_task()'s launch-type gate
(MPI types defer to their own remote-launch mechanism; default type
fans out via xcecli_runtask() across every allocated host, verified
via a #define-before-#include mock capturing taskrun/taskhost
contents) and create_mpi_cmdline()'s host-list/command construction
for openmpi and mpich. Zero production code changes -- this closes a
previously zero-coverage gap in existing, unmodified worker code
(Requirement 4.4), independent of this feature's resreq wiring work.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XYdtrEWWZfKRRGED7raEYg
2026-09-06 05:26:32 +00:00
6577e04196
feat(resreq-maxtasks-wiring): wire snapshot-restore ingest to real task count
snapshot_load.c's job-restore path now calls apply_job_task_count()
with the job's already-restored mintasks/maxtasks, right after its
resreq parse succeeds and before snapshot_build_schedobj() reads the
resreq's task count for its fingerprint. This is the simulation/replay
driver's only live call site (xce_sched_core_load_snapshot(), reached
via --sim), independent of and parallel to task 2.1's fresh-ingest
wiring in core_events.c.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XYdtrEWWZfKRRGED7raEYg
2026-09-06 04:28:48 +00:00
0aadb49493
feat(resreq-maxtasks-wiring): wire fresh job-ingest to real task count
core_build_job() now calls apply_job_task_count() right after its
resreq parse succeeds and before core_build_schedobj() reads the
resreq's task count for resreq_fp — so a freshly-submitted multi-task
job's dispatched primary resreq reflects its real requested min/max
task count instead of the previous always-one-task hardcode.

Tracked follow-up (not blocking, see tasks.md Implementation Notes):
test_repl_breakpoints_inspection.c:735 hardcodes an incidental
"Tasks: min = 0, max = 0" byproduct of a fixture's parse_select()
failure; now correctly reports (1,1) per Requirement 1.2. Fix folded
into task 5.1's full-suite regression pass.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XYdtrEWWZfKRRGED7raEYg
2026-09-06 04:01:23 +00:00
799a71ecb1
feat(resreq-maxtasks-wiring): add task-count derivation helper
Adds apply_job_task_count() to overwrite a standalone primary resreq's
minTasks/maxTasks with a job's real requested task count, no-op for
COMPOUND/ALTER nested primaries, with a defensive min>max clamp.
Closes the gap that lets scheduler-allocation's multi-host allocator
ever be reached (bug-1770). Wiring at live ingest call sites follows
in subsequent tasks.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XYdtrEWWZfKRRGED7raEYg
2026-09-06 03:26:32 +00:00
c009f108b7
test(scheduler-allocation): end-to-end multi-host parallel job proof
Drive a real multi-slot parallel job through the actual production
dispatch path -- real sched_init(), real queue/worker/job ingest via
xce_core_push_events(), a real scheduling session (exercising task 5.2's
cache-clear wiring), the real sched_allocator() thread, cache lookup,
parjob_allocator's real my_alloc(), host_iter, and commit -- and confirm
the resulting job allocation genuinely spans 2 hosts with correct
per-host slot counts (Req 4.1, 4.2, 4.3).

Discovered and logged bug-1770: parse_resreq_primary() unconditionally
hardcodes primary->minTasks/maxTasks to 1 regardless of what a job
actually requests, and nothing in the real ingest pipeline ever copies
the job's own submitted task count (correctly threaded from the
apiserver's num_processors/maxnum_processors all the way to
xce_sched_job_t.mintasks/maxtasks) onto the nested xce_primary_resreq_t
fields the allocator dispatch actually reads. This predates the feature
by ~7 months and is out of this spec's scope to fix, but it means
parjob_allocator has zero live production path until a dedicated
follow-up lands. The test compensates with one clearly-labeled patch of
the real, already-ingested resreq's task-count fields immediately after
real parsing; everything else in the pipeline is unmodified production
code.

Also logged bug-1771 (xce_sched_fcfs's fcfs_del_job() dereferences
job->queue->name with no NULL guard) discovered while building this
task's fixture; worked around by registering the job's queue before
push, mirroring the apiserver's own pre-submission queue validation.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XYdtrEWWZfKRRGED7raEYg
2026-09-05 01:56:54 +00:00
e8c2087d5e
feat(scheduler-allocation): clear unsatisfiable-resreq cache per session
Clear the cache inside core_begin_session_internal() so a job cached as
unsatisfiable in one session is re-evaluated fresh in the next (Req 7.2).

An initial weak-symbol cast-based approach was rejected: it corrupted
xce_sched_sim_driver_t.alt_events_file, since core_begin_session_internal()
is shared between driver.c (real xce_scheduler_t) and sim_loop.c (a
structurally different, deliberately isolated sim driver struct with no
cache field at the same byte offset) -- the strong cache_clear() symbol
won unconditionally for both, invisible to ASAN and untested by any
test_sim_*.c file.

Fixed via a driver-registered callback: xce_sched_core_t gains a
cache_clear_cb hook (NULL by default, per xce_sched_core_init()'s
full-struct memset), set only in main.c's driver-mode branch --
mirroring the existing on_decision/xce_core_set_decision_cb() pattern.
--sim mode returns before that registration is ever reached, so its hook
stays NULL and core_begin_session_internal()'s guarded call is a no-op.
The real xce_scheduler_t-casting adapter lives in alloc_cache.c rather
than driver.c, since ~11 tests white-box-include driver.c without
linking alloc_cache.c.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XYdtrEWWZfKRRGED7raEYg
2026-09-05 00:57:46 +00:00
5c022d0b4f
feat(scheduler-allocation): wire unsatisfiable-resreq cache into dispatch
Wrap sched_make_primary_alloc_() -- the single choke point all three
primary-resreq shapes (standalone, compound-nested, alter-nested via
compound delegation) already funnel through -- with a cache lookup
before allocation and a cache insert on exhaustion. On a hit, pend the
job immediately via the same make_pendreason()/add_job_preason() path
every exhaustion case already uses, without invoking either allocator
(Req 7.3, 7.4, 7.5).

A nested primary has no real per-primary schedobj, so
sched_primary_cache_obj_() synthesizes one on the stack per dispatch for
cache-key purposes; verified memory-safe since alloc_cache.c's
build_key_() copies the fingerprint/identity by value synchronously
before the stack frame unwinds.

Log bug-1768 (compound/alter-nested primaries never get real slot/rusage
availability checks -- seqjob/parjob's prepare_allocation hooks no-op
whenever ctx->schedobj->tag != XCE_RESREQ_PRIMARY) and bug-1769
(sched_make_alter_allocation() type-confuses xce_compound_schedobj_t*
as xce_sched_obj_t*, a ~112-byte heap over-read, currently untested).
Both are pre-existing and out of this feature's scope; discovered and
confirmed while building this task's dispatch tests.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XYdtrEWWZfKRRGED7raEYg
2026-09-04 23:27:56 +00:00
9d23060b76
test(scheduler-allocation): prove parjob's shared resource commits once
Task 4.3's multi-host merge itself was already implemented and verified
by task 4.2. The one unproven clause -- job-level shared-resource
allocation computed once regardless of host count -- is now covered by
test_shared_resource_debited_once_regardless_of_host_count, using a
PER_JOB (not PER_TASK) global resource so a per-bucket-repeated
computation can't coincidentally sum to the correct total. Mutation
testing (a deliberately injected duplicate per-host shared debit)
confirms the test fails exactly as expected; production code required
no change.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XYdtrEWWZfKRRGED7raEYg
2026-09-04 21:53:12 +00:00
3d5e1c32c0
feat(scheduler-allocation): implement parjob's per-bucket allocation loop
Replace parjob's return-0 stub with a real multi-host allocator: for each
candidate plan, walk buckets in the given (descending) order, claiming a
distinct host per bucket via a fresh host_iter excluding hosts already
claimed elsewhere in the same plan attempt. On a bucket failure, reverse
every earlier successful claim's provisional debit via
sched_ctx_uncommit_host() before advancing to the next plan (Req 4.2, 4.4,
4.5, 4.6, 4.7).

Fix parjob_prepare_allocation() to compute each bucket's want from that
bucket's own slot count (ctx->nslots) instead of a fixed value, and reset
ctx->alloc_req.host at the top of each bucket's scan so host_iter's
pre-filter can't inherit a stale want from a differently-sized prior
bucket (safe: sched_try_alloc_on_host_() always repopulates it from the
real per-bucket want before its own authoritative check runs).

Add a maxTasks<=1 guard matching Req 4 AC1's precondition, needed because
a snapshot-restored job with unpopulated maxTasks==0 (pre-existing,
tracked separately as bug-1767) would otherwise reach parjob's now-real
allocator instead of being silently ignored as before.

On full success, merges every claimed bucket into one xce_job_alloc_t via
make_job_alloc()'s existing runtime-sized host array, satisfying task
4.3's multi-host commit scope as well.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XYdtrEWWZfKRRGED7raEYg
2026-09-04 21:25:25 +00:00
6ca717fb86
feat(scheduler-allocation): implement parjob's combinatorial plan generation
Add the real xce_alloc_plan_iter_t for parjob: for host-count k=1..maxTasks
(ascending, most-preferred-first), generate one balanced partition of
maxTasks into k buckets (floor/ceil split, remainder to the first rem
buckets), which is descending by construction. Pure function of maxTasks
alone -- never dereferences sched -- with a real pthread_testcancel()
cancellation point and stable post-exhaustion NULL (Req 2.1-2.6, 4.1).

my_alloc()/parjob_prepare_allocation() remain untouched stubs; wiring the
iterator into real allocation is task 4.2.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XYdtrEWWZfKRRGED7raEYg
2026-09-04 20:04:44 +00:00
9d64ba0d69
test(scheduler-allocation): prove seqjob rewrap preserves host selection
Add a dedicated equivalence suite (test_seqjob_rewrap_equivalence.c)
covering multi-host fixtures the trivial single-host test doesn't reach:
select-filter-only rejection, resource-only rejection, a combined
select-then-resource-then-success ordering, and per-host debit
correctness. The iteration-order test is grounded in the deleted
pre-rewrap sched_collect_workers() code path (verified via git show
against the parent commit) and the unmodified xlist.c primitive, not
merely in host_iter.c's own traversal, so it proves equivalence to the
old behavior rather than internal self-consistency.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XYdtrEWWZfKRRGED7raEYg
2026-09-04 19:35:38 +00:00
b18c5f780d
feat(scheduler-allocation): rewrap seqjob allocation onto shared iterators
Restructure seqjob's my_alloc() as a trivial single-bucket case over the
shared xce_alloc_plan_iter_t/xce_host_iter_t contracts, replacing its own
inline worker-collection loop. Guard seqjob_prepare_allocation() to
maxTasks == 1, closing the fan-out gap for the parallel allocator's later
fix (Req 9.1, 4.6).

Export sched_try_alloc_on_host_() from resource.c (drop static/va_list,
plain (worker, sched, ctx) signature) so seqjob calls the real per-host
try-alloc leaf instead of a private duplicate; delete the now-dead
sched_try_alloc_on_hosts() variadic wrapper. Also wires host_iter.c into
the production scheduler_srcs list (task 2.1 only added it to testsuite).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XYdtrEWWZfKRRGED7raEYg
2026-09-04 18:48:49 +00:00
86c86352d1
feat(scheduler-allocation): implement the unsatisfiable-resreq cache
Adds alloc_cache.c: xce_cache_key_t, cache_key_ops (the corrected
three-way, 0-means-equal .cmp -- an inverted-boolean version was a
real bug found during design review), and cache_lookup/insert/clear
against a real kvtab_t (constructed matching xce_select.c's
sel_node_table_init() precedent). Cache values are a boxed reason-
category int, not a bare hit/miss sentinel, satisfying Req 7 AC5's
requirement that a hit reproduce the original pend-reason category.
unsatisfiable_cache lives on xce_scheduler_t per Req 7 AC1.

Task 2.2 (scheduler-allocation spec). Requirements 7.1, 7.3, 7.4, 7.5.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XYdtrEWWZfKRRGED7raEYg
2026-09-04 17:23:59 +00:00
d9831b0bc8
feat(scheduler-allocation): implement the shared host-candidate iterator
Adds host_iter.c: the one shared xce_host_iter_t implementation,
lazily filtering sched->core.workers by select expression
(sched_meet_host_selection, unchanged) and resource availability
(sched_check_host_satisfy, unchanged), with a real cancellation
point per candidate visited. xce_alloc_plan_t gains a ctx field
(xce_sched_ctx_t*) so the iterator can reach the scheduler/select
expression/staged resource-want -- additive to task 1.1's approved
interface, NULL-guarded at init(). Test proves the core design
property this spec exists to establish: a host too small for one
search remains a valid candidate for a later, smaller search via a
fresh iterator instance over the same worker set.

Task 2.1 (scheduler-allocation spec). Requirements 3.1-3.6.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XYdtrEWWZfKRRGED7raEYg
2026-09-04 16:46:03 +00:00
d0024eb7e8
feat(scheduler-allocation): add per-host resource-debit reversal
Adds sched_ctx_uncommit_host() (resource.c), the structural inverse
of sched_try_alloc_on_host_()'s existing XVEC_ADD_FLOAT commit call,
scoped to ctx->uncomitted only per requirements.md's Rollback scope
discussion. Test exercises the real sched_ctx_get_uncomit_host()
(schedobj.c) via white-box #include rather than a private
reimplementation, matching test_picker_config_fields.c's established
precedent.

Task 1.4 (scheduler-allocation spec). Requirement 4.2.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XYdtrEWWZfKRRGED7raEYg
2026-09-04 16:06:59 +00:00
d8b5cf8718
feat(scheduler-allocation): add forward-compatible allocation-mode field
Adds xce_alloc_mode_t (XCE_ALLOC_NORMAL=0, XCE_ALLOC_PREEMPT, with
commented-out RESERVE/BACKFILL placeholders) and alloc_mode on
xce_sched_ctx_t, zero-defaulted by sched_ctx_init()'s existing
memset. Purely additive: nothing branches on the field yet -- this
spec only declares the extension point scheduler-preemption needs.

Task 1.3 (scheduler-allocation spec). Requirement 9.5.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XYdtrEWWZfKRRGED7raEYg
2026-09-04 15:17:30 +00:00
5a6706761d
feat(scheduler-allocation): add resreq fingerprinting for the cache
Adds rusage_hash to xce_primary_resreq_t (computed once by
parse_resreq_primary() via a new hash_rusages() helper, hashing
rusage content rather than raw struct bytes since resname is a
non-interned strdup'd pointer), the xce_resreq_fingerprint_t value
type, and resreq_fp on xce_primary_schedobj_t, populated once at real
job-ingest time in core_build_primary_schedobj().

Task 1.2 (scheduler-allocation spec). Requirement 7.1.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XYdtrEWWZfKRRGED7raEYg
2026-09-04 15:00:12 +00:00
d5d2c1caf5
feat(scheduler-allocation): add lazy plan/host iterator contracts
Defines xce_alloc_plan_t (partition of a job's task count into
per-host slot buckets), xce_alloc_plan_iter_t, and xce_host_iter_t
(init/next/fini vtables, stack-allocate-then-init-in-place convention
matching sched_ctx_init) as reusable abstractions any allocator can
adopt. Proven usable by a trivial no-op reference implementation and
a fresh-per-bucket host_iter composition test, no allocator wired in
yet.

Task 1.1 (scheduler-allocation spec). Requirements 2.1, 3.1.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XYdtrEWWZfKRRGED7raEYg
2026-09-04 13:50:31 +00:00
c4f2fec0d5
Merge branch 'remove_extlib' into scheduler-framework 2026-09-04 06:49:06 +00:00
88b0ac9937
fix(scheduler): close post-failover rejoin detection gap (bug-1760)
An idle scheduler (nothing queued to publish) never noticed a dead
controller connection: sched_needs_rejoin was only ever set as a side
effect of an active client-library call, and an idle round makes none.
After any controller failover with nothing already in flight, jobs
submitted to the new primary stayed PEND indefinitely.

Adds xcecli_is_connection_broken() (libxce) -- a pure, socket-free read
of the transport's already-async-maintained dead/alive flag, set for
free by the existing RDHUP disconnect handler with zero application
calls. sched_check_pending_rejoin() now retries xcecli_join() on either
the existing reactive flag or this new passive signal, so a healthy
idle scheduler still makes zero extra network calls while a genuinely
dead connection self-heals within one ~1s round -- live-verified
against a 3-node HA cluster (rejoin fired ~300ms after new-primary
election with nothing submitted in between).

Also closes out bug-1735 (job memory/CPU usage reporting): confirmed
already fixed on this branch since 2026-08-30, just never marked in
the bug log.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XYdtrEWWZfKRRGED7raEYg
2026-09-04 04:37:11 +00:00
0b1e310749
remove extralib 2026-09-04 11:24:43 +08:00
2cb6953d4f
Merge branch 'simplify_app_image' into scheduler-framework 2026-09-04 10:27:42 +08:00
6fabdba961
feat(eliminate-frontend-makefile): task 4.2 - delete src/frontend/Makefile
The terminal step: src/frontend/Makefile (194 lines, the second,
independently-maintained build/test path this whole spec exists to
eliminate) is deleted. Re-ran the full task 4.1 validation suite with
the file genuinely absent (build, meson test --suite apiserver, and
-- going beyond citing task 3.5 -- a real live run-resource-verify.sh
end-to-end run: VERDICT result=PASS, closing design.md's Testing
Strategy "Discovery-path regression" check specifically). Full-repo
sweep: 181 hits, zero unaccounted, byte-identical to task 4.1's
non-Makefile hits (the only delta is the Makefile's own 8
self-referential lines, now gone since the file doesn't exist).
Independently re-verified by the task reviewer.

Meson is now the single build/test system for apiserver and xcecli.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XYdtrEWWZfKRRGED7raEYg
2026-09-03 15:52:13 +00:00
12c6a59c03
feat(eliminate-frontend-makefile): task 3.4 - update source code comments
Comment-only edits in 4 files: test_ha_bootstrap.c/test_ha_ctrlhello.c
rename their "make test-integration" pointers to reference the
regression suite (tests/regression/run.sh) directly; apiserver/xcecli
cmd/main.go drop the Makefile clause from their ldflags-injection
comments, keeping only src/tools/go_build. Zero production/test
logic changed -- verified via full controller unit-test suite rerun
(1261 tests, 0 failures) and xcecli build.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XYdtrEWWZfKRRGED7raEYg
2026-09-03 14:34:48 +00:00
f5fbc6bcac
feat(eliminate-frontend-makefile): task 3.3 - update live documentation (11 files)
Removes every live current-fact reference to src/frontend/Makefile
targets or Makefile-specific fixed paths across 11 docs (steering,
frontend/CLAUDE.md, root CLAUDE.md, deployment docs, and two skill
docs), replacing with the Meson-based equivalents established by
tasks 1-3.2: /xce-build apiserver, meson test --suite apiserver,
direct tests/regression/run.sh invocation.

Round-1 review rejected the first pass: xce-deploy-ha/SKILL.md had
over-corrected by deleting all mention of deploy-ha.sh's still-real
/tmp/apiserver legacy fallback (that script's discovery logic is
unchanged, unlike run-resource-verify.sh's, which genuinely lost its
fallback in task 3.1 -- xce-test-resources/SKILL.md's identical-
looking edit was correct for that reason). Fixed by restoring an
accurate, Makefile-free description of the real fallback at all four
touched sites. Re-reviewed and approved.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XYdtrEWWZfKRRGED7raEYg
2026-09-03 14:22:54 +00:00
6b10512502
feat(eliminate-frontend-makefile): task 3.2 - fix deploy-ha.sh/run.sh messages
Rewords deploy-ha.sh's block comment and two fallback echo lines, and
run.sh's XCE-not-found error, to stop naming the now-deleted
Makefile's build-server/build-cli targets, pointing at /xce-build
apiserver instead. Discovery/control-flow logic in both files is
unchanged (verified line-by-line: no if/elif/else lines touched);
run.sh's XCE default placeholder is untouched by design.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XYdtrEWWZfKRRGED7raEYg
2026-09-03 14:00:22 +00:00
3c199f376a
feat(eliminate-frontend-makefile): task 2 - migrate xce-build.sh off Make
Replaces both `cd frontend && make test` call sites (apiserver and
all cases) with direct `go test` invocations covering the identical
package set/flags, mirroring the xcecli case's already-Makefile-free
style. Verified: apiserver case end-to-end, matching the captured
pre-change baseline exactly; all case verified by proxy (byte-
identical replacement line, apiserver instance fully verified) since
a full /xce-build all run is currently blocked earlier in the
pipeline by two pre-existing, unrelated C-test bugs (bug-1723,
bug-1758) -- noted in Implementation Notes for task 4's awareness.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XYdtrEWWZfKRRGED7raEYg
2026-09-03 13:48:35 +00:00
6c3ff2012b
feat(eliminate-frontend-makefile): task 1.1 - Meson backend CGO test target
Adds a new `test('apiserver backend', ...)` in apiserver/meson.build
reusing the existing apiserver_cgo_deps-derived CGO flags (resolving
their @CURRENT_SOURCE_DIR@/@BUILD_ROOT@ placeholder tokens via
Meson's own .replace(), since this target invokes go_compiler
directly rather than through tools/go_build). Adds timeout: 300
(matching the existing controller test's convention) since a
cold-cache CGO+ASAN build exceeds Meson's 30s default.

Verified cold and warm cache; confirmed CGO_ENABLED=1 selects the
real backend/ CGO path, not the //go:build !cgo stub.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XYdtrEWWZfKRRGED7raEYg
2026-09-03 13:17:06 +00:00
4a224e8c8d
Simplify AppImage build to nix build .#xce.appImage
Expose the AppImage as a flake output via the nix-appimage input's
mkAppImage (passthru on the xce package) instead of requiring an
external 'nix bundle --bundler github:ralismark/nix-appimage' call.
Update README and build-deployment steering to match.
2026-09-03 18:36:12 +08:00
b823cd9f99
Merge branch 'go_build_improve' into scheduler-framework 2026-09-03 08:37:26 +00:00
9dcffce8d8
test(scheduler-sim-reconcile-terminal-guard): task 2.2 - final REPL-level verification
Adds test_reconcile_info_worker_and_summary_hide_completed_job_after_stray_dispatch,
mirroring bug-1764's actual live-observed shape: a worker with one job
driven to a terminal state and two jobs genuinely still RUN, all real
allocations to the SAME worker. After reconciling a stray DISPATCH
entry for the terminal job's id, info worker excludes it while still
listing the two running jobs, and info summary's running count agrees.

Round 1 review rejected an initial version for using a mismatched host
string on the stray entry ("workerarr-stray" instead of "workerarr"),
which made the info worker/info summary assertions pass vacuously
regardless of whether the fix existed, since repl_info_worker_one()
filters assigned jobs by an exact host-string match. Round 2 fixed the
host string and confirmed via an independently-reproduced mutation
test (fix reverted, the test's own accounting-level pre-check
bypassed) that the REPL-level assertions now genuinely, independently
detect the bug.

design.md's Testing Strategy is corrected: it originally said no new
test was needed, reasoning the existing display-path suites already
covered this; tasks.md's own task 2.2 correctly required a
purpose-built test for this exact scenario instead.

Full scheduler test suite: 63/64 OK, sole failure the pre-existing,
unrelated bug-1758. This is the final task of the
scheduler-sim-reconcile-terminal-guard spec; all implementation tasks
(1.1-2.2) are now complete.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YAWMhfRJFnmmnG8h97yUDd
2026-09-03 08:31:37 +00:00
b722de4f08
fix(scheduler-sim-reconcile-terminal-guard): task 1.2 - bug-1764 terminal-state guard
sim_reconcile_unaccounted_jobs()'s per-job trigger check (sim_loop.c)
now also excludes JOB_IS_DONE(job->state) || JOB_IS_EXIT(job->state),
alongside the existing job->accounts != NULL skip. A terminal job is
never searched for a matching recorded DISPATCH entry, so a leftover
or duplicate DISPATCH decision from a later replay round can no
longer resurrect allocs/accounts on a job that has already completed
- the exact mechanism behind info worker's assigned-jobs listing
showing a finished job as still consuming a worker's resources.

The exclusion mirrors job_state_regresses_terminal()'s existing
"terminal is permanent" invariant (core_events.c) rather than
inventing a new rule. The function's doc comment gains a paragraph
clarifying the terminal exclusion is complementary to, not a reversal
of, the existing PEND/RUN state-independence rationale that motivated
this trigger's original widening.

Both RED-phase tests (task 1.1) pass; the pre-existing PEND
eager-capture regression test and core_events.c's JOBS_PUSH
resync-merge terminal-state tests are unaffected. The analogous
resync-merge branch was investigated and confirmed already correct.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YAWMhfRJFnmmnG8h97yUDd
2026-09-03 07:15:42 +00:00
ff05c37bca
test(scheduler-sim-reconcile-terminal-guard): task 1.1 - RED tests for terminal guard
Adds two failing tests formalizing bug-1764's reproduction: a job
driven to JOB_STAT_DONE (and, separately, JOB_STAT_EXIT) via a real
xce_core_push_events() JOB_STATE_CHANGE call correctly has its
allocs/accounts nulled by the existing end-of-life cleanup, but
sim_reconcile_unaccounted_jobs() then wrongly resurrects both fields
when presented with a stray DISPATCH entry for that same jobid, since
its trigger checks only job->accounts == NULL with no state check.

The pre-existing eager-capture regression test,
test_reconcile_captures_non_run_state_job_when_dispatch_recorded,
already covers Requirement 1.2's "PEND job's early DISPATCH still
captured" case explicitly and needed no duplicate. No production code
touched.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YAWMhfRJFnmmnG8h97yUDd
2026-09-03 06:56:25 +00:00
68bc86bc59
build: make Go and Python builds fully offline (hermetic Nix build)
The Nix build previously fetched Go modules from goproxy.cn and Python
build deps from PyPI during the build phase, relying on network leaking
into the sandbox. This is non-reproducible and breaks whenever the
sandbox resolver is unreachable (e.g. pc13v's loopback dnsmasq).

Go:
- Vendor all three modules (frontend/apiserver, frontend/xcecli,
  autoscaler/providers/baidu/src/bcctool) and commit vendor/ trees.
- tools/go_build now builds with GOFLAGS=-mod=vendor and GOPROXY=off
  instead of GOPROXY=https://goproxy.cn.

Python (xceadmin):
- Add setuptools + wheel to xce.nix nativeBuildInputs.
- meson-build.sh installs with pip --no-build-isolation --no-index
  --no-deps so no PyPI access happens at build time.

xceadmin install script:
- Invoke meson-install.sh via find_program('bash') instead of relying
  on its #!/usr/bin/env bash shebang, which fails in the Nix install
  sandbox (no /usr/bin/env -> 'could not be run').

Cleanup:
- Drop now-unneeded cacert and impureEnvVars=[HOME] from xce.nix and
  xcecli.nix (they only existed to enable the impure network fetch).

Docs:
- README build guide: drop '--option sandbox false' from nix build /
  nix bundle commands, note the build is now hermetic, and document the
  go mod vendor + git add vendor/ maintenance workflow.

Verified: nix build .#xcecore, .#xcecore_debug, .#xcecli, and .#xce all
succeed with the sandbox enabled.
2026-09-03 12:38:49 +08:00
56e007cb6b
feat(scheduler-sim-step-verbosity): task 1.2 - implement preamble deduplication
repl_print_event_summary() (repl_break.c) now compares the candidate
rendered timestamp and displayed session number against a new cache
on xce_sched_repl_state_s (repl_internal.h: last_echo_ts_human[32],
last_echo_session) before printing the "[sim] t=... session=..."
preamble. It prints and updates the cache only when either value has
changed, or no preamble has been printed yet this session (the cache's
zero-initialized default, which no real echoed event can produce);
otherwise the preamble is skipped and the cache is left untouched. The
event's own content line prints unconditionally either way
(Requirements 1.1-1.4, 2.2).

All 6 RED-phase tests (task 1.1) now pass, including the critical
raw-vs-rendered nuance test (Requirement 1.3). No regressions in the
Event Echo Gate suite or the sibling spec's breakpoints-inspection
suite (test_step_echoes_events_before_session_decisions specifically
confirmed still passing under the new dedup behavior).

repl_break.c grew from 949 to 980 lines (+31, 3.3% over the residual
already accepted in scheduler-sim-repl-verbosity task 9.2). Review
judged this small growth as accepted without a fresh empirical split
test: task 9.2's own split experiment (a larger +44-line growth)
already found extracting this file's lookup-table domain reaches only
838 lines (still over cap) while requiring 22 white-box test files'
#include lines to change - disproportionate churn this smaller
increment would only re-confirm.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YAWMhfRJFnmmnG8h97yUDd
2026-09-03 04:11:08 +00:00
6a2aa47f5e
test(scheduler-sim-step-verbosity): task 1.1 - RED tests for preamble dedup
Adds 6 failing tests for the Event Echo Preamble Deduplicator
(Requirements 1.1-1.4, 2.2): a second echoed event sharing its
predecessor's rendered timestamp+session omits its preamble; two
events across a session boundary both print their own preamble; the
critical raw-vs-rendered nuance (Requirement 1.3) - two events whose
raw nanosecond timestamps genuinely differ but whose rendered,
second-granularity timestamp and session are identical still dedupe,
with a fixture-premise self-check proving the two raw values really
do render identically; the first echoed event in a fresh session
always prints its preamble; a structural event under continue mode
after suppressed routine events follows the same rule; event content
and ordering are unaffected regardless of the preamble decision.

Lands in a new test_repl_step_verbosity.c (1010 lines, 6 tests) rather
than extending test_repl_event_echo.c (705 lines) - proactive split
per this file family's established practice, since appending would
have pushed that file well past the project's ~1000-line trigger.
Registered in testsuite/meson.build. No production code touched.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YAWMhfRJFnmmnG8h97yUDd
2026-09-03 03:54:01 +00:00
bd97397373
feat(scheduler-sim-repl-verbosity): task 9.2 - discoverable help and break event types
Adds repl_bp_kind_names(char* buf, size_t bufsz) (repl_break.c),
comma-joining BP_KIND_TABLE's tokens in table order into a caller
buffer. repl_print_help() (repl.c) is rewritten into four labeled
sections (Execution Control, Breakpoints, State Inspection, Snapshot &
Parameters) with a single shared "  %-38s%s\n" column format, info
config moved into State Inspection next to its siblings, set units
added to Snapshot & Parameters, and the break line's description now
built from repl_bp_kind_names() so it enumerates the complete event
type set. repl_cmd_break()'s unrecognized-event-type error calls the
same accessor, so help and the error message can never disagree with
each other or with BP_KIND_TABLE (Requirements 7.1-7.6). No command's
existence, argument parsing, or other output changed.

repl_break.c grew from 905 to 949 lines (18.6% over the 800-line cap,
up from task 3.2's already-accepted 13%-over residual). Review
empirically tested extracting the BP_KIND_TABLE lookup-table domain
into a new repl_bp_kind.c: it built and passed, but only reached 838
lines (still 4.75% over cap) while requiring 22 white-box test files'
#include lines to be updated - the same disproportionate-churn,
doesn't-achieve-compliance outcome already accepted at tasks 6.2/8.2.
Reverted; 949 lines accepted in place.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YAWMhfRJFnmmnG8h97yUDd
2026-09-02 20:30:45 +00:00
bd77eea0a8
test(scheduler-sim-repl-verbosity): task 9.1 - RED tests for help/break discoverability
Adds failing tests for Requirement 7 (Discoverable Help and Breakpoint
Event Types): help's four labeled sections in correct order, one
consistent column width across every listed command, info config's
placement alongside its info siblings, set units's placement alongside
set speed/output/verify, the break line's complete BP_KIND_TABLE-derived
event-type listing, and break's own unrecognized-event-type error
listing the same complete set. Each new test hardcodes BP_KIND_TABLE's
current 12 tokens rather than referencing the not-yet-implemented
repl_bp_kind_names() accessor, so RED comes from genuine assertion
failures against today's behavior rather than a link error.

New tests for Requirements 7.1-7.4 land in a new test_repl_help.c
rather than test_repl_execution_control.c (949 lines going in, within
reach of this spec's own ~1000-line split trigger already flagged at
task 2.1) - proactive split per this spec's established practice,
documented in design.md. break's Requirement 7.5 error-message test
lands in test_repl_breakpoints_inspection.c exactly as originally
planned, alongside the pre-existing test_break_unrecognized_event_type_rejected
(left unmodified).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YAWMhfRJFnmmnG8h97yUDd
2026-09-02 20:01:06 +00:00
9db99964f0
feat(scheduler-sim-repl-verbosity): task 8.2 - implement resource table rendering
Rewrites info worker's/info resource's resource-listing loops from a
flat, unconditional fprintf into a 3-pass, section-grouped table
(Consumable/Capacity, Load Metrics, Static Hardware Attributes),
units_mode-aware value rendering (raw/human/both, including swp's
MB->bytes conversion), whole-number formatting with no trailing .00
outside the consumable value=/used= pair, and jobid-range-compression
adoption for info worker's assigned-job listing (Requirements 6.4-6.6,
4.5). All 10 of task 8.1's tests pass. Preserves every named prior-bug
invariant (bug-1727's avail_v-as-total, bug-1613's consumable/
non-consumable split, bug-1593's GLOBAL/SHARED filter, bug-1688's
.caps-bound iteration).

Fixes one pre-existing test (bug-1688, predating this spec) whose
"value=5.00" assertion for a whole-number non-consumable resource
directly conflicts with Requirement 6.5's intentional no-trailing-.00
change - same class of fix as task 4.2's pre-existing-test updates.

repl_info.c grew from 985 to 1267 lines (58% over the 800-line cap,
worse than any other file this spec touched) - but unlike repl.c/
repl_break.c, it was already 23% over cap before this spec began.
Review empirically confirmed a narrow helper-only extraction doesn't
achieve compliance (still 41% over), and a compliance-achieving split
would mean restructuring ~690 lines of pre-existing display logic
this feature doesn't otherwise touch - a larger scheduler-core-level
decision, accepted as a residual rather than folded into this task.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YAWMhfRJFnmmnG8h97yUDd
2026-09-02 19:32:29 +00:00
e48e222bd1
feat(scheduler-sim-repl-verbosity): task 8.1 - RED-phase resource table tests
Adds 10 tests in a new test_repl_resource_table.c covering info
worker's/info resource's resource-listing table/grouping layout,
units_mode-aware rendering (raw/human/both, including swp's MB->bytes
conversion and a no-mutation check on the underlying stored value),
whole-number-vs-fractional formatting, and info worker's assigned-job
listing adopting the real repl_fmt_jobid_range() (Requirements
6.4-6.6, 4.5). All 10 fail as expected against today's flat,
unconditional per-resource fprintf loop and uncompressed listing.

Found and fixed a genuine ASAN stack-use-after-return in the test's
own fixture code (not production): WORKERS_PUSH's handler moves a raw
pointer into a job's resvals rather than deep-copying, so the fixture
must heap-allocate rather than stack-allocate the structures it feeds
through that path - independently reproduced by review.

New file rather than extending test_repl_breakpoints_inspection.c:
landing this content there first pushed it to 3215 lines, 3.2x this
file family's own ~1000-line split trigger already used to justify
splitting test_repl_round_rollup.c/test_repl_event_echo.c for
structurally identical reasons.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YAWMhfRJFnmmnG8h97yUDd
2026-09-02 18:47:14 +00:00
5ee1d6cb08
feat(scheduler-sim-repl-verbosity): task 7.2 - implement resource formatting helpers
Implements repl_is_bytes_valued_resource()/repl_format_bytes_human()
(mirroring xce_test's confirmed is_bytes_valued_resource()/
format_bytes_human() precedent exactly) and the units_mode setting
(explicitly defaulted to both, not left to zero-init) wired into
set units raw|human|both alongside the existing set speed/output/verify
branches (Requirements 6.1-6.3). All 8 of task 7.1's tests pass.

Split repl_format_ts_human(), the Jobid Range Compressor (task 1.2),
and these two new helpers out of repl.c into a new repl_fmt.c: adding
them in place pushed repl.c to 929 lines (16.1% over the 800-line
cap), and unlike task 6.2's single-function split attempt (which
didn't even achieve compliance), bundling these three pure-formatter
groups together does - repl.c settles at 587 lines, repl_fmt.c at 402,
both comfortably under cap. Required a matching #include line in the
20 test files that white-box #include repl.c, plus extending
test_sim_isolation.c's forbidden-include/symbol scan to cover the new
file, following the repl_rollup.c split precedent from task 2.2.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YAWMhfRJFnmmnG8h97yUDd
2026-09-02 17:45:06 +00:00