Development record
The load check: preflight and the eval harness warn about strays and extreme load, and never refuse
Summary
spc-2609231542463113 delivers itd-2609231434459890: before make preflight and
the eval harness run abcd's own tests, a check reads the machine's load
average and process table once. It warns when a program outside the running
lanes has used a near-full core for longer than the stray limit (30 minutes by
default), or when the one-minute load average is above the extreme limit (four
times the online core count by default). The caller's own strays are named in
full with the owned-group remedy the LOAD rule domain teaches; other
accounts' strays appear only as a count and a total CPU share. Inside an
autonomous run the same warning is also written to the run log as a load
event. The check always exits 0: it never refuses, never waits and never
kills.
The check is one core function, implement.CheckLoad, behind one thin
sub-verb, abcd implement load, in the implement family that already owns the
run log's single writer and the report-never-enforce verdict. Beneath it is
the one new primitive, a stdlib-only machine read in a new leaf package,
internal/core/machineload. make preflight runs it as its first
prerequisite (the pre-push hook inherits it); the eval harness's TestMain
runs it once after building the binary under test; CI runs a single step that
prints why the check is skipped there.
Two facts measured for this spec shape the harness wiring. go test in
package-list mode prints nothing from a passing package, TestMain output
included, and a cached eval lane starts no harness at all (both probed on this
machine, see Risks). The design accounts for both rather than assuming the
harness's output reaches a person.
Scope
In
- The machine read: One-minute, five-minute and fifteen-minute load averages, the online core count, and a process table (pid, parent pid, process group, effective uid, age, cumulative CPU time, executable name) on macOS and Linux, with no new dependency.
- The classifier: A pure function from a snapshot, the limits and the caller's identity to a verdict with two triggers (stray, extreme), own strays in full and other accounts' strays as a count and a CPU total.
- The machine settings file:
~/.abcd/load-limits, read-only, with defaults derived from the online core count and a loud report when it is malformed. - The core function and the verb:
implement.CheckLoadandabcd implement load --site preflight|eval-harness [--json], with four statuses (ok,warning,skipped,unchecked) and exit 0 on every one. - The private-names scrub: Own strays' executable names pass through the private banned-names layer, on the same engine the pre-commit guard uses, before they are printed or logged.
- The run-log event: A verb-owned
loadevent, written only when the check warns inside an autonomous run. - Wiring: The first prerequisite of
make preflight, one call inevals/harness_test.go'sTestMain, and one CI step per harness job that prints the skip reason. - Loud staging: A platform the reader does not cover, or a read that fails, says it could not check and carries on.
- Doc surfaces: The plugin command page, the CLI reference, the brief's implement and configuration pages, the compatibility snapshot, and the six surfaces that restate the preflight prerequisite list.
Out
Carried from the intent and not reopened here:
- Refusing to start, waiting for the load to fall, or any override flag: the product thinker ruled warn, never refuse (Decision 2), so nothing exists to override.
- Killing, pausing or renicing anything. The warning names the remedy; the caller acts.
- Running the check on CI, beyond the step that says it is skipped.
- Windows and every platform other than macOS and Linux, beyond the loud "could not check" line.
- Load experiments themselves, which the
LOADrule domain governs. - A check that keeps watching after the tests start: the check runs once at each start, so a burner launched mid-run is seen by the next start, not this one.
- An allowlist of the caller's deliberate long-running programs. Raising
stray-minutesis the only knob (see Risks).
Approach
1. Where the check lives
The seam is the implement family; the new primitive is only the machine
read. Nothing in internal/, cmd/ or evals/ reads the load average, the
core count or the process table today (a git grep for loadavg,
runtime.NumCPU, syscall.Sysctl and _NPROCESSORS finds only comments in
the banned-names code about /proc/<pid>/cmdline). So the read is new, and it
goes in a leaf package, internal/core/machineload, that imports only the
standard library.
The decision the read feeds is not new. internal/core/implement already owns
the two things this check needs from the record's side: the run log's one
writer (Run.append, reached through the run's lock, one O_APPEND line per
event) and the verdict shape that reports a limit without enforcing it
(Verdict.Ceiling in bounds.go, "recorded and reported, never enforced" in
session.go). The check is a sibling of implement check, so it lives there:
machineload(new leaf):Snapshot,Proc,Read() (Snapshot, error)with per-platform files, the pure parsers,Limits,DefaultLimits(cores),ReadLimits(home), andClassify(snap, limits, self) Verdict.implement.CheckLoad(LoadRequest) LoadResult(new,load.go): The one core function. It decides CI, reads the machine through an injectable reader, reads the limits, classifies, scrubs own names, and writes the run log event when a run is live. It never writes to stdout.abcd implement load(new sub-verb ininternal/surface/cli): Parses--siteand--json, callsCheckLoad, renders the result. No logic.
Rejected, with the reason each loses:
- A new
implement check loadstep.Run.Checkrequires a joined session and returns anErrRefused-classed error on a bound; preflight runs on no session's word, and a step whose verdict can never refuse would break the contract every other step keeps. - A guard entry. The guard is a stateless command-position token matcher held to a fixture corpus; "tests while the machine is loaded" is a machine state, not a command shape.
peers. It reads sibling worktrees for records and knows nothing of processes.- A new top-level verb. It would open a second front door for run-adjacent machinery, a second plugin page and a second surface family, for a check whose only record-side act is a line in the run log the implement family already writes.
2. Reading the machine
machineload.Read dispatches by build tag. Every parser is a pure function
over bytes or an fs.FS, so both platforms' parsers are unit-tested on both CI
legs.
macOS (read_darwin.go):
- Load:
syscall.Sysctl("vm.loadavg")returns the kernel'sloadavgstruct as a string with one trailing NUL stripped (23 bytes here): three little-endianuint32fixed-point values, four bytes of padding, then theint64scale. The parser restores the stripped byte, reads the scale from offset 16, and divides. Probed here in Go: 13.79, 18.40, 18.80, equal tosysctl -n vm.loadavg. - Online cores:
syscall.SysctlUint32("hw.activecpu"), the valuesysconf(_SC_NPROCESSORS_ONLN)reports. Probed here: 16, equal togetconf _NPROCESSORS_ONLN. - Processes:
/bin/ps -axo pid=,ppid=,pgid=,uid=,etime=,time=,comm=run throughos/execby absolute path withLC_ALL=C, so a plantedpsonPATHis never run. The first six fields split on whitespace;commis the rest of the line (names with spaces survive) and is reduced to its base name, because macOS prints the executable's full path, which can carry a home directory.etimeis[[dd-]hh:]mm:ss;timeis cumulative user plus system CPU as[dd-][hh:]mm:ss.ccwith minutes allowed past 59 (114:03.49seen here). Probed: all seven keywords accepted, other accounts' processes listed (45 distinct uids here), 63 ms for about 1,870 processes.etimesis not available on macOS, which is whyetimeis parsed.
Linux (read_linux.go), from /proc alone, so a minimal ps does not
matter:
- Load: The first three fields of
/proc/loadavg. - Online cores:
/sys/devices/system/cpu/online, a range list such as0-3,8-11, which is what glibc'ssysconf(_SC_NPROCESSORS_ONLN)reads. Notruntime.NumCPU, which counts the affinity mask and can be smaller. - Processes: Each numeric
/proc/<pid>/stat:commis the text between the first(and the last)(it may contain spaces and parentheses), then ppid, pgrp, utime, stime and starttime by field position. Age is/proc/uptimeminus starttime; CPU time is utime plus stime. Both are in clock ticks, converted withAT_CLKTCKread from/proc/self/auxv(100 when the entry is absent). The effective uid comes from theUid:line of/proc/<pid>/status, read only for processes that already pass the age and CPU tests, since the owner of the/proc/<pid>directory is root for a non-dumpable process. A process that exits between the listing and the read is skipped, not an error. Zombies (stateZ) are skipped.
Both: A core count that cannot be read falls back to runtime.NumCPU()
and the report says so. No go.mod change: golang.org/x/sys/unix.SysctlRaw
would read vm.loadavg more cleanly, and x/sys is already an indirect
requirement, but promoting it edits go.mod, which needs sign-off this change
does not need.
Elsewhere (read_other.go, //go:build !darwin && !linux): Read
returns ErrUnsupported, and its doc comment names itd-2609231434459890 and
the scope condition that excludes other platforms. See section 8.
3. Classifying what is running
Near full, and for how long. A process's CPU share is its cumulative CPU time divided by its age, in cores (1.0 is one core flat out). It is a stray candidate when its age exceeds the stray limit and its share is at least 0.9. The same definition holds on both platforms and needs no second sample, so the classifier is a pure function of one snapshot, and a fixture can express a two-day burner without spawning one. A multi-threaded process can exceed 1.0 and counts once, with its whole share.
abcd's own lanes, by ancestry and by time, never by name. Two things exempt a process:
- The invocation's own ancestry: The check's own pid and every ancestor up
the parent chain (the
psor/procread,make, the pre-push hook, the shell, the agent host). They are the run that asked; they cannot be strays from outside it. - Age: Everything else abcd's lanes start is short-lived.
go,compile,link,vetand the 57 package test binaries ofgo test ./...live for seconds to minutes, so a concurrent preflight in another worktree never reaches the stray limit. This is the intent's third scope condition made operational.
A name list (go, *.test, make) is rejected deliberately. It would exempt
exactly the hung test binary that the third scope condition says must read as
a stray, and a burner named go would pass it.
Same uid or other uid. A candidate whose effective uid equals the check's
os.Geteuid() is the caller's own; any other uid, root included, is another
account's. There is no third class and no allowlist of operating-system
daemons: a system process at a near-full core for over half an hour is real
load, and it appears only in the anonymous count.
The two triggers.
- Stray: At least one candidate outside the ancestry. Own and other candidates both trigger it.
- Extreme: The one-minute load average is strictly above the extreme limit. Only the one-minute value decides, because the five-minute and fifteen-minute averages remember preflights that finished a quarter of an hour ago; all three are reported.
The verdict type carries the privacy rule. Verdict.Own is a list of
OwnStray{Name, PID, PGID, Age, Share}; Verdict.Others is
OtherStrays{Count, Cores}, a type with no field that could hold a name, a
command line, a uid or an account. A renderer cannot leak what the verdict
cannot carry. The classifier also returns the remedy plan for own strays (see
section 5), computed from the snapshot's process groups.
Measured on this machine while the run's own lanes were busy (load 18 to 26 on 16 cores): no process older than 30 minutes had a share above 0.19, so the nearest normal program sits far below the 0.9 line.
4. The machine settings file
Name and home. ~/.abcd/load-limits, in the caller's own machine tier,
beside trusted-roots and local-transcript-roots. Load is a property of the
machine, so neither the repository's .abcd/rules.json nor an environment
variable is its home. The check never creates the file or ~/.abcd/.
Format. Line-oriented, following the sibling files: # starts a comment,
blank lines are ignored, every other line is <key><space or tab><value>.
# abcd's load check on this machine (itd-2609231434459890)
stray-minutes 30
extreme-load 64
stray-minutes: An integer from 1 to 10080 (one week).extreme-load: A decimal number above 0 and at most 100000, an absolute load in the_NPROCESSORS_ONLNunit theLOADrule's cap uses.- Either key may be omitted; the omitted one takes its default.
Defaults. stray-minutes 30; extreme-load four times the online core
count (64 on this 16-core machine).
Read. Through fsutil.ReadDeclaration, the guarded read the other
home-scope files use: a regular file, not a symlink, owned by the caller, not
writable by group or other, at most 4 KiB.
Malformed. An unknown key, a repeated key, a value that does not parse or
is out of range, or any ReadDeclaration refusal other than "absent" makes
the whole file unusable. The check then uses the defaults for both limits,
never a mix, and prints a loud line naming the file, the line number and the
fault class, even when there is otherwise nothing to warn about:
LOAD CHECK SETTINGS UNUSABLE: ~/.abcd/load-limits line 3: unknown key "stray-mins" (known: stray-minutes, extreme-load); using the defaults for both limits: 30 min, load 64 (4 x 16 online cores)
An offending key is echoed through termsafe.Sanitize, capped at 32 bytes; a
value is never echoed. An absent file is silent.
5. Output
The verb writes its report to stdout. Every name passes through
termsafe.Sanitize, because process names are free text, before the
private-names scrub below.
ok: One quiet line, so a log shows the check ran:
load check (preflight): nothing to warn about; load 18.1 on 16 online cores
warning: A loud block. Own strays are listed in full, up to 20 with an
"and N more" line; then the remedy; then the others' line; then the load line:
LOAD WARNING (preflight): programs that are not abcd's tests are keeping this machine busy.
Your programs at a near-full core for over 30 min:
yes pid 41233 group 41230 running 2d 07h CPU 99% of a core
yes pid 41234 group 41230 running 2d 07h CPU 99% of a core
To stop them, if you did not mean to leave them running, check each target first:
pgrep -g 41230 lists only 41233 41234, then: kill -- -41230
ps -o pid=,comm= -p 41250 still shows it, then: kill 41250 (its group holds other programs)
Never by pattern (pkill -f, killall): a pattern also matches other sessions' programs.
Other accounts: 38 programs at a near-full core for over 30 min, using about 14.1 cores.
Load 440.2 over 1 min (431.0 over 5, 425.7 over 15) is above the extreme limit of 64 (4 x 16 online cores).
abcd carries on; stopping them is your call.
The load line is always present; when only the stray trigger fires it reads "Load 12.3 on 16 online cores" instead. Limits taken from the settings file are marked "(set in ~/.abcd/load-limits)".
The remedy follows the LOAD rule domain. The domain teaches one owned
process group, killed as a group only after a re-check that the handle still
names what was started, and never a kill by pattern (the no-pattern rule and
the re-check are the loadtrust fix round's wording, not yet on main at this
tip). The warning never signals anything itself; it prints commands:
- Group form (
pgrep -g <pgid>, thenkill -- -<pgid>): Offered only when every live member of that group in the snapshot is a stray this warning names, and the group is neither the check's own group nor the group of any ancestor. So a group that holds the caller's shell, agent host or anything unnamed is never offered. - Process form (
ps -o pid=,comm= -p <pid>, thenkill <pid>): Every other own stray. - Never a pattern, and never anything about another account's programs.
Others. Only the count and the summed share, rounded to one decimal, as "about N cores". No name, command line, uid, account name or pid, by type (section 3).
The private-names scrub. The one free-text field left is the own strays'
executable names. CheckLoad asks a new banlist.MatchPrivate(repoRoot, names) ([]bool, error) which names match the private layer
(.abcd/.work.local/private-names.txt). It asks the enforcing engine itself,
exactly as grepAccepts and the pre-commit guard do: grep -iE under
LC_ALL=C, the patterns on stdin through -f - (never in argv, where
/proc/<pid>/cmdline would expose them), and the names in a 0600 temporary
file that is removed afterwards. So the scrub and the guard cannot disagree
about a match. A matching name becomes [private name]. An absent store
matches nothing. An unreadable or malformed store, or no usable grep, makes
every own name [name withheld: private-names layer unreadable], and the
block says so. The scrub runs before the verdict is rendered or logged, so
the printed text and the run-log event carry the same masked names. The
package comment that says banlist does "not the matching" gains one sentence
naming this one read-only matcher.
--json: The same result as data: status, site, load (three
values), cores, cores_source, limits (stray_minutes, extreme_load,
source of default, file or default-after-malformed, and malformed
when it is), triggers, own_strays, own_strays_more, other_strays
(count, cores), remedy, run_log (logged, session, error) and
reason for skipped and unchecked.
6. The run-log event and the autonomous run
Knowing it is in a run. CheckLoad resolves the repository's root
commit and calls implement.Peek on it, which creates nothing. The check is
in an autonomous run when the run state holds at least one joined session
record (Run.Sessions() non-empty). A session's record is removed when it
leaves, so a closed run is not a live one. Outside a repository, or with no
joined session, the check writes nothing anywhere.
When it writes. Only when the status is warning and a run is live
(criterion 5). ok, skipped and unchecked write no event.
The event. load joins verbOwnedEvents beside claim and
session_open, so implement log load is refused as a hand-written
imitation. It is written through the run's lock and Run.append, as every
other verb-owned event is:
{"ts":"2026-09-24T09:12:03Z","session":"804bdd81","event":"load",
"cores":16,"extreme_limit":64,"limits_source":"default",
"load1":440.2,"load5":431,"load15":425.7,
"other_strays":{"count":38,"cores":14.1},
"own_strays":[{"name":"yes","pid":41233,"pgid":41230,"age_s":198000,"cpu_pct":99}],
"own_strays_more":0,"site":"preflight","stray_limit_min":30,
"triggers":["stray","extreme"],"within_preflight":false}
session: The run's first-role session, else the earliest joined. The check runs inside a lane on no session's word, andRun.Check's role bounds deliberately never read a session from the environment; the line records the machine's state for the run, andsiterecords where it was seen.- "The same warning": The event holds the verdict's facts, not its prose;
the CLI's renderer is a pure function of them, so rendering a logged event
reproduces the printed block byte for byte (a test pins it). Own strays are
capped at 20 per line, as printed, which keeps the line far under the 16 KiB
maxLineBytes. - The hand-run samples: The run log already holds 67
loadlines from the run's hand-written sampler, which carryncpuandcpu_pct_by_userand notriggers. A reader tells the two apart bytriggers. The sampler's per-account CPU map is exactly what criterion 2 forbids in a warning, so the check does not copy its shape. - A failed write: Contention on the run lock (3 s) or an I/O error prints one loud line, "could not write the load warning to the run log: …; the warning above stands", and the exit stays 0.
7. Wiring
make preflight. A new phony target, first in the prerequisite list, and
a target-specific exported marker placed after the prerequisite line (the
gate-list parser in preflightgates_test.go reads the first line that starts
with preflight:):
preflight: load-check lint-reviews lint-issues lint-decisions record-lint docs-lint site-render smoke evals-cold-reading
preflight: export ABCD_LOAD_CHECKED := preflight
load-check:
-go run ./cmd/abcd implement load --site preflight
- Order: The pre-push hook runs plain
make preflight, and serial GNU make builds prerequisites left to right, so the check runs first. Undermake -jit may run beside the first gates; it reads nothing they change. - The pre-push hook inherits it with no edit to its logic.
- The leading
-: If the check cannot even be compiled, make reports the error as ignored and carries on; a broken build is thego buildgate's to fail, never the load check's. - The marker: GNU make passes a target-specific exported variable to the target's prerequisites and recipe, and not to a prerequisite run on its own (probed with the system's GNU Make 3.81). The eval harness reads it below.
The eval harness. TestMain in evals/harness_test.go runs the check
exactly once, right after building the binary under test and before
m.Run(): abcdBin implement load --site eval-harness. That is the start of
the one choke point both tagged lanes share, and it exercises the verb through
the built binary the harness exists to test. Its exit code is ignored and it
never panics. Where its output goes is decided by two facts probed for this
spec:
- Package-list mode hides it.
go test -tags smoke ./evals/...prints nothing from a passing package,TestMainincluded. - So the output goes to the person directly. With
ABCD_LOAD_CHECKEDunset (the harness started on its own),TestMainwrites the report to/dev/ttywhen that opens, else to stderr. WithABCD_LOAD_CHECKED=preflight, it writes to stderr only: the preflight's own check already put a warning on the terminal, so one preflight shows one warning, not three. The verb stampswithin_preflighton any event it writes in that case, so a reader can fold the three events of one preflight into one moment.
The check therefore runs once at each start and never per package: go test ./... builds 57 package test binaries, and no TestMain outside the eval
harness calls the check (a test enforces this).
CI. CI never runs make preflight: its check job runs go test ./...
and the race lane directly, and the eval lanes run as make smoke and make evals-cold-reading in their own jobs. Each of those two jobs gains one step
before the harness:
- name: Load check (skipped on CI, says why)
run: go run ./cmd/abcd implement load --site eval-harness
CheckLoad sees GITHUB_ACTIONS=true (or a CI value other than empty,
false or 0), reads nothing, and returns skipped with the reason, which
the step prints into the job log:
load check (eval-harness): skipped on a CI runner (GITHUB_ACTIONS=true): a fresh runner carries no programs left from earlier work, so there is nothing to warn about
The harness's own call on CI takes the same branch; its line is hidden by package-list mode, which is why the visible line is the step's. The step shares the Go build cache with the harness build that follows. The exemption is earned only while the runner is fresh, so a test pins that every job that starts the harness runs on a GitHub-hosted label; moving one to a self-hosted runner fails it.
8. Loud staging for the cannot-check case
Two paths lead to unchecked, and both print a loud line and exit 0:
- A platform the reader does not cover:
read_other.goreturnsErrUnsupported. The three disclosure sites the loud-staging principle asks for are the doc comment naming itd-2609231434459890 and its scope condition, the surface line below, and the intent's own out-of-scope entry for other platforms.
LOAD CHECK UNAVAILABLE (preflight): abcd reads load and processes on macOS and Linux only, and this is freebsd; carrying on without checking the machine's load
- A read that fails on a covered platform:
/bin/psmissing or exiting non-zero,/procunreadable, or a parse that fails. The line names what could not be read and the error, for example "could not read the process table (/bin/ps: exit status 1)". A load average that reads while the process table does not is still reported, and the extreme trigger still applies.
9. Exit status
abcd implement load exits 0 for all four statuses, and for a failed run-log
write and an unusable settings file. The one non-zero exit is a malformed
invocation (an unknown flag, or a --site outside the closed vocabulary),
which is cobra's usage error, not a load verdict; the placement tests pin the
only two invocations that exist, so it cannot arise from the wiring.
10. Doc surfaces
commands/implement.md: A "Check the machine's load" section and the sub-verb in its argument hint and table.docs/reference/cli/commands.md: Theimplement loadentry, the four statuses,~/.abcd/load-limitsand its format..abcd/development/brief/04-surfaces/27-implement.md: The sub-verb and theloadevent in the run-log vocabulary..abcd/development/brief/05-internals/03-configuration.md: The user scope's inventory gains the limits file. Its sentence "no home-scope one is read at all" names this one exception; the tree in04-surfaces/01-ahoy.md, which must agree, gains the same line..abcd/development/release/surface.json: Regenerated for the new sub-verb and its flags.- The preflight prerequisite list:
TestPreflightGateListIsNotRestatedWronglyrequires every prerequisite to be named where the list is restated, soload-checkis named in the Makefile comment,docs/how-to/install.md,CONTRIBUTING.md,AGENTS.md,CLAUDE.mdand.githooks/pre-push's comment, each as "the load check first (a warning, never a failure)". It is not counted among the gates: the gate count inAGENTS.mdstays six.
Footprint and estimate
About a day:
internal/core/machineload(new):machineload.go,parse.go,read_darwin.go,read_linux.go,read_other.go,limits.go,classify.go, tests andtestdata/. About three and a half hours.internal/core/implement:load.go(CheckLoad,LoadResult,LogLoad),log.go(EventLoadamong the verb-owned events). About an hour.internal/core/banlist:private_match.go(MatchPrivate). About half an hour.internal/surface/cli:implement.goregisters the sub-verb;implement_load.gorenders. About an hour.- Wiring:
Makefile,evals/harness_test.go,.github/workflows/ci.yml. About half an hour. - Doc surfaces (section 10). About an hour.
How the acceptance criteria are satisfied
Test names are the ones to be written; files are where they go. Fixture
snapshots are Go values in internal/core/machineload/classify_test.go built
from the recorded incidents and the run's own telemetry, so no test spawns a
long-lived burner.
- Own strays named with the remedy, and the check carries on. Sections 3
and 5. In
classify_test.go:TestMechanismFlagsIncidentOne(eightyesprocesses of the caller's uid, one owned group, 2 days 7 hours old at 99%: stray trigger, all eight named with pid, group, age and share, group form offered),TestStrayBoundaries(29 and 31 minutes; shares 0.89 and 0.90),TestAncestryIsNeverAStray(a two-day agent host at 100% in the check's parent chain is exempt),TestRemedyNeverGroupKillsAMixedGroupandTestRemedyNeverGroupKillsTheCallersGroup(process form only). Ininternal/surface/cli/implement_load_test.go:TestImplementLoadNamesOwnStraysWithTheRemedy(the block names each field, thepgrep -gre-check precedes everykill, the text holds nopkillorkillallexcept in the "Never by pattern" line, exit 0). - Other accounts' strays only as a count and total CPU. Sections 3 and 5.
TestMechanismFlagsIncidentTwoinclassify_test.go(38 other-uidzshloops since the Saturday: count 38, share summed). Inimplement_load_test.go:TestImplementLoadKeepsOtherAccountsAnonymous(the fixture gives the others a distinctive name, command path, uid and account; neither the text nor the--jsonoutput contains any of them, andother_strayshas exactly the keyscountandcores; exit 0).TestOtherStraysTypeCarriesNoIdentityreflects overOtherStraysand fails if a field is added.TestMatchPrivateMasksAMatchingNameandTestMatchPrivateFailsClosedininternal/core/banlist/private_match_test.gocover the scrub, including an unreadable store and a missinggrep. - Only abcd's own short-lived processes, however many: no warning, proven
by a test. Section 3. Two tests, a fixture and a live one:
TestEightConcurrentPreflightsAreQuietinclassify_test.goreplays the run's busiest recorded sample (14:59:13Z on 2026-09-23: load 41.91 on 16 cores, eight preflights, twelve.testbinaries) as a process table of eightmake, eightgo, twelve test binaries and their compilers, all younger than 15 minutes at full share: statusok, no trigger, andTestImplementLoadIsQuietUnderOwnParallelWorkasserts the rendered output holds noLOAD WARNING.TestEightConcurrentShortLivedProcessesDoNotWarnininternal/core/machineload/live_test.gostarts eight re-executions of the test binary that each busy-loop for three seconds, as one owned process group (the first child leads it and the rest join bySysProcAttr.Pgid), reads the real machine withRead, and classifies with the default stray limit: none of the eight pids appears among the strays. The extreme limit is set above the live load, because the test machine's real load is not the subject. It then classifies the same snapshot with the eight children's age and CPU time advanced past the limit and asserts all eight are named, so the test can fail. Cleanup kills the group and proves it gone with a freshRead, as the resolved iss-2609210828122412 requires of any test that spawns load.
- Extreme load gives the load and the core count, and carries on.
Sections 3 and 5.
TestExtremeTriggerIsStrictlyAboveinclassify_test.go(64.0 on 16 cores is quiet, 64.1 warns; a five-minute or fifteen-minute value above the limit alone does not warn);TestImplementLoadReportsLoadAndCoresinimplement_load_test.go(the line carries all three averages, the online core count and the limit's derivation; exit 0). - In an autonomous run, the same warning is written to the run log.
Section 6. In
internal/core/implement/load_test.go, under a temporaryHOME:TestLoadWarningIsLoggedInALiveRun(a joined first session; oneloadline withtriggers, attributed to that session),TestNoEventOutsideARun(no joined session: no directory and no file created),TestNoEventWhenQuiet,TestLoadIsVerbOwned(Run.Logrefusesload). Inimplement_load_test.go:TestLoggedEventRendersToThePrintedWarning(the event read back throughReadLogrenders byte-identical to the printed block, masked names included) andTestRunLogFailureIsLoudAndExitsZero. - Settings file limits; defaults from the online core count; malformed is
loud and falls back. Section 4. In
internal/core/machineload/limits_test.go:TestDefaultLimitsFromCores(16 cores gives 64, 10 gives 40, stray 30),TestLimitsFileSetsEitherOrBoth, andTestMalformedLimitsFallBackWhole(a table: unknown key, repeated key, non-numeric, zero, out of range, oversize, symlink, group-writable, another owner; each yields both defaults and a report naming the line and fault class, never the value).TestImplementLoadMalformedLimitsIsLoudinimplement_load_test.goasserts theLOAD CHECK SETTINGS UNUSABLEline appears even when the status isok. - Once at the start of preflight and the eval harness, never per package;
skipped on CI with a logged reason. Section 7. In
internal/core/lint/loadcheck_placement_test.go(beside the preflight gate pins):TestLoadCheckIsPreflightsFirstPrerequisite(and the marker line follows the prerequisite line),TestEvalHarnessRunsTheLoadCheckOnce(exactly one invocation inTestMain, beforem.Run()),TestNoOtherTestMainRunsTheLoadCheck(noTestMainin any other_test.gofile calls the verb orCheckLoad; the verb's own unit tests call it from ordinary test functions, which run on demand, not at a lane's start),TestCIRunsTheSkipStepBeforeEachHarness(both harness jobs carry the step beforemake smokeormake evals-cold-reading, and no workflow runsmake preflight), andTestHarnessJobsRunOnHostedRunners(the exemption's failable half). Ininternal/core/implement/load_test.go:TestCheckLoadSkipsOnCIWithReason(withGITHUB_ACTIONS=true, and separatelyCI=true, the injected reader fails the test if called; the result isskippedwith the variable named), andTestCIValueFalseIsNotCI. - macOS and Linux with no new dependency; elsewhere, "could not check".
Section 2 and section 8. In
internal/core/machineload:TestParseLoadavgSysctlBytes(the 23-byte form, including the restored NUL),TestParseDarwinPS(a fixture listing with names containing spaces, full paths reduced to base names,etimewith days,timewith minutes past 59),TestParseLinuxProc(anfstest.MapFSof/procwith acommholding) (, a zombie, a process that vanished between listing and read, astatusUid:line and anauxvAT_CLKTCK),TestParseOnlineRange,TestReadSeesThisProcess(the real reader on whichever platform runs it: CI's macOS and Linux legs cover both; the snapshot holdsos.Getpid()withos.Geteuid(), at least one core and non-negative loads),TestMachineLoadImportsOnlyTheStandardLibrary(no import path with a dot in its first element), andTestMachineLoadCompilesElsewhere(go vetof the package withGOOS=freebsd).TestImplementLoadCannotCheckinimplement_load_test.goinjectsErrUnsupportedand a failing process read: both print theLOAD CHECK UNAVAILABLEline and exit 0, and the second still reports the load average.go.modis unchanged in the diff.
Risks and not verified
- Lifetime share lags a late spinner. A program idle for days that starts spinning today reaches a 0.9 lifetime share late, or never. Both recorded incidents spun from birth, which is the shape the rule catches; a recent-share reading would need a second sample on Linux and a different definition per platform. Accepted; the extreme trigger still sees the load.
- A deliberate long-running program of the caller's warns on every run. A
virtual machine or an indexer at a full core is named each time. The only
knob is
stray-minutes; an allowlist is out of scope. - Cached and swallowed harness runs. A cached eval lane (
ok (cached), seen here on the second of two identical runs) starts no harness, so no harness check runs; the preflight's own check is unaffected. An agent with no terminal that runs the eval lanes on their own, outside a run, sees the harness's report only when the lane fails or runs with-v. Inside a run the event still lands. - Up to three events per preflight in a run. The preflight's check and
each harness start log separately;
within_preflightlets a reader fold them. The criterion asks for the warning to be logged whenever it is printed, so none is suppressed. - Attribution to the first session. The event names the run's first
session even when a second session's lane ran the preflight. Session records
of a run that crashed without leaving make later checks log into that run;
harmless, and visible in
abcd implement. - Linux
hidepid. With/procmountedhidepid=2, other accounts' processes are invisible and their count reads 0. Not detected. - The
LOADrule's wording. The remedy follows the loadtrust fix round's text (the one-job group, the re-checked handle, no kill by pattern), which is on its branch and not yet onmainat this tip. If that text changes before it merges, the remedy lines follow it in the same change. - Not verified: The Linux readers against a real
/proc(no Linux host in this session; the field positions are from proc(5), and the CI Linux leg runsTestReadSeesThisProcess); that/sys/devices/system/cpu/onlineis what glibc's_NPROCESSORS_ONLNreads on every distribution; theAT_CLKTCKauxv entry on every architecture (100 is the fallback); thathw.activecpuequals_NPROCESSORS_ONLNon a Mac with cores taken offline (equal here, all 16 online); the day form of macOSpstime; that/dev/ttyopens from a test binary started bygo testin a terminal session; and GitHub-hosted runner core counts. Probed on this machine and so verified: thevm.loadavgparse in Go,hw.activecpu, the macOSpskeywords and cost, the lifetime-share margin (highest 0.19 among programs over 30 minutes old at load 18 to 26), package-list mode hiding a passingTestMain's output, eval-lane caching, and GNU Make 3.81 passing a target-specific exported variable to prerequisites.
Frontmatter
| id | spc-2609231542463113 |
|---|---|
| type | spec |
| lifecycle | closed |
.abcd/development/specs/closed/spc-2609231542463113-abcd-s-own-test-lanes-check-the-machine.md
open on GitHub ↗ · commit history ↗
References1
- itd-2609231434459890abcd's own tests warn before running on an overloaded machineimplements
mentions3
- dcn-26092300000000212026-09-23 — The load check before abcd's own test lanes warns and never refuses (product thinker, planning interview for itd-2609231434459890; spc-2609231542463113, Decision 2). This changes the fourth condition of the run A interview ruling on iss-2609210828122412, which had the check refusing. The check names foreign sustained CPU (a near-full core for longer than 30 minutes) and an extreme load (four times the online cores), both limits overridable in the caller's `~/.abcd/load-limits`; it names the caller's own strays with a remedy and counts other accounts' processes without naming them; it writes a run-log event; it runs once at the start of `make preflight` and of the eval harness; CI skips it with a stated reason; it always exits 0, and its findings are minor.
- iss-2609210828122412abcd must never do this to a machine when running tests for a repo it manages. On 2026-09-21 at 08:16Z the development Mac kernel-panicked (userspace watchdog: no successful check-ins from WindowServer in 120 seconds, two induced crashes, then the panic), killing the pilot run's orchestrator session mid-gate and losing its scratchpad. The stackshot taken 25 seconds before the first WindowServer kill shows CPU starvation, not memory: eight orphaned /usr/bin/yes processes pegging eight cores for 2.3 days, two spinning zsh loops, and abcd's own reading.test and cli.test running under them from another session's go test; the OS's low-priority daemons (dasd at priority 4) got under a millisecond of CPU, 500-plus threads queued behind them through turnstiles, and WindowServer blocked in a synchronous XPC to cfprefsd that was itself blocked behind dasd. The burners came from a test-under-load experiment in a managed repository on 2026-09-19: an agent launched eight of them with nohup yes and appended each pid to a file whose directory did not exist yet, so the launches succeeded and the bookkeeping failed; the later kill loop read the pid file, pgrep -x yes still printed 8, a second kill pass over the same file followed, and the agent declared the machine clean. The pattern is one abcd can be asked to run and must refuse to run this way. Wanted, as the rule the record carries: (1) a test or experiment that spawns background load owns its children in a way that survives a failed bookkeeping step, a process group or setsid with the group killed, or a parent that dies with the session, never nohup with pids in a file as the only handle; (2) clean is proven by a process query, pgrep -x on the session's own burner name returning nothing, never by the pid file having been read; (3) a load experiment on a live development machine needs explicit consent and a hard ceiling below the core count, leaving cores for the OS's own daemons, and abcd's own test run is never started while such load is present, so the gate reads the load average before it begins and refuses above a declared ceiling, naming what is burning. Severity major: it took the product thinker's machine down and lost a session. Diagnostic report names (under the system's diagnostic-reports directory): panic-full-2026-09-21-091803.0002.panic, WindowServer-2026-09-21-091552.ips, WindowServer-2026-09-21-091632.ips, and two WindowServer userspace_watchdog_timeout spin stackshots.
- spc-2609232027132755stray-rule-silent-band