Skip to content

fix: bound ShellCheck concurrency across fm-lint runs - #71

Merged
MrGTV-love merged 7 commits into
mainfrom
fm/fm-lint-concurrency-bound
Oct 9, 2026
Merged

MrGTV-love merged 7 commits into
mainfrom
fm/fm-lint-concurrency-bound

Conversation

@MrGTV-love

Copy link
Copy Markdown
Owner

Intent

I would like all bugs to be fixed so tomorrow can be focused entirely to Vernant and not problems preventing Vernant from getting built. A recommended architecture should come with proof.

Context: the backlog item "Bound fm-lint shellcheck fan-out" asks to limit how many shellcheck processes fm-lint.sh runs at once by hw.ncpu and current load, queueing instead of failing (up to 42 ran at once, some about 4.8 GB each). It is not a cap on agents. Source: a host analysis of a 21-minute sample at load 102 on 18 cores: 0.1% idle, 56% kernel time, about 61% of CPU in sub-second processes, about 2,850 new processes per second. The target for the whole set of fixes over one fleet day is 1-minute load <= 36 for >= 90% of the day, process creation < 500/s, kernel time < 25%, and no new timeout-class failures. Pattern: measure, fix the root, test, measure again.

Decisions made in this change: the root cause is that fm-lint.sh bounds concurrency per run only (FM_LINT_JOBS), so N concurrent runs start 2N ShellCheck processes; a single-run test hides it. The fix is a host-wide counting semaphore of flock slot files taken per root by a gate mode in bin/fm-lint-cache.pl (Perl flock works on macOS, which has no flock command, and the kernel frees a slot when its holder dies, so no stale-lock code). Slot count is FM_LINT_HOST_SLOTS else max(2, ncpu/2); the allowance shrinks as 1-minute load rises past 2 x ncpu (matches the load target), with a floor of 2 so a lone run is never throttled and queued roots always progress. Waiting runs queue rather than fail. The gate sits outside the per-root timeout watchdog and queue time is excluded from the root deadline and recorded duration, so no new timeout-class failures. An unusable slot directory runs ungated with a warning; FM_LINT_SLOT_DIR=off disables the gate. A bad FM_LINT_HOST_SLOTS exits 2. Proof: five designs were compared on counted factors (per-run only, FM_LINT_JOBS=1, bash mkdir slots, central daemon, flock pool), and a fake-ShellCheck reproduction shows peak live processes of 2 x runs before and the slot cap after. The new behavioral tests in tests/fm-lint.test.sh fail on the pre-fix code and pass after. Not in scope: fleet-day measurement, FIFO ordering of waiters, queue-wait telemetry.

What Changed

  • Add a shared Perl flock slot pool that bounds ShellCheck concurrency across lint runs using CPU count and current load, with inherited locks protecting running descendants.
  • Queue roots outside their timeout watchdogs and exclude queue waits from retry budgets and high-resolution recorded durations. Support configurable slot caps, disabling the pool, and warning-only ungated fallback when slots are unusable.
  • Document pool controls and add behavioral regressions for concurrent admission, load thresholds, configuration failures, inherited-slot lifetime, and queued-root timing.

Risk Assessment

✅ Low: The change is narrowly scoped to host-wide lint admission and its behavioral proof, with no substantiated correctness, privacy, or intent-conformance defects found in the current source.

Testing

Targeted CLI and gate scenarios passed with real ShellCheck, including counted contention, load admission, queue timing, diagnostics, and process cleanup. The base-commit concurrency regression reproduced the original failure. Scheduler-sensitive test-controller waits were fixed and exercised afterward. CLI transcripts, process snapshots, telemetry, regression results, and native capability limits were retained; live memory-exhaustion retry could not be exercised. No repository-wide suite or separate lint/static-analysis phase was run.

  • Live validation: ✅ go - 11 of 12 scenarios driven live against the product
Scenario Result Live Evidence
A lone lint run retains its two analysis workers ✅ pass live Live-product evidence, lone-gated: peak two actual ShellCheck processes; all six roots completed successfully.
Concurrent lint runs share a three-slot bound instead of multiplying worker counts ✅ pass live Live-product evidence, four-disabled and four-gated: identical four-run workloads peaked at eight processes ungated and three gated; all 24 gated root results were successful.
Load preserves the default cap through the threshold and reduces admission above it ✅ pass live Live-product evidence: the 18-core fixture admitted nine actual analyzers at load 36, eight at load 37, and two under extreme load.
A reduced load allowance blocks admission while existing occupancy remains too high ✅ pass live Live-product evidence, occupancy-scan and occupancy-wake: neither path admitted with eight occupied slots and allowance two; both admitted after occupancy reached one.
A waiting gate probes native host load at a bounded cadence ✅ pass live Live-product evidence, load-probe-cadence: the instrumentation executed native sysctl; observed probe spacing was approximately 6.74 seconds.
A queued root excludes waiting time from its recorded analysis duration ✅ pass live Live-product evidence, queue-local: five-second slot hold, 9,736 ms root wall time, 3,698 ms recorded analysis duration, reason ok, exit zero.
A queue longer than the watchdog deadline does not consume the analysis deadline ✅ pass live Live-product evidence, queue-watchdog: six-second queue before the actual three-second watchdog; real ShellCheck completed with exit zero.
Gate death cannot release capacity while a protected descendant survives ✅ pass live Live-product evidence, slot-lifetime and actual watchdog cleanup: inherited capacity remained locked while the real descendant survived, and watchdog cleanup permitted admission without overlap.
An invalid host-slot setting is refused before creating the pool ✅ pass live CLI guard evidence, invalid-host-cap: FM_LINT_HOST_SLOTS=zero exited two, named the setting, and created no slot directory.
Unusable slot storage warns and runs analysis ungated ✅ pass live CLI guard evidence: unusable directory and slot-file cases warned and completed clean analysis; an unusable pool preserved a real finding and exit one.
Enabling or disabling the pool preserves finding diagnostics and exit status ✅ pass live CLI diagnostic evidence: gated and disabled runs produced byte-identical SC2086 output and exit one.
A genuinely memory-exhausted queued analysis retries with correct queue accounting ⏸️ untested no Native Bash rejected the probed address-space limits, including the public CLI's required-bound attempt. GHCRTS=-M16m could not impose a real heap ceiling because the installed ShellCheck disables RTS…
Evidence: Real-product concurrency, load, queue, and lifetime evidence

Source: Real-product concurrency, load, queue, and lifetime evidence

REAL PRODUCT LIVE EVIDENCE
The concurrency wrapper launches the installed ShellCheck 0.11.0, suspends the real child after exec to establish contention, then resumes it and preserves its actual analysis output/status. No analyzer or product stub is used in these live scenarios. Synthetic load values use the existing FM_TEST_SEAM interface.

=== lone-gated ===
{
  "name": "lone-gated",
  "runs": 1,
  "roots_per_run": 6,
  "jobs": 2,
  "slot_cap": 3,
  "load": 0,
  "peak_live_shellcheck": 2,
  "expected_peak": 2,
  "held_observations": [
    2
  ],
  "real_analysis_processes": 6,
  "real_analysis_exits": [
    0
  ],
  "lint_exits": [
    0
  ],
  "actual_processes": [
    "67644 66914 T    ~/.local/bin/shellcheck --norc --external-sources -- ~/.no-mistakes/worktrees/32d18ed9638d/01M4FB4GY4BGX79TB64806ZHF7/.fm-live-validation/root-1.sh",
    "68946 66677 T    ~/.local/bin/shellcheck --norc --external-sources -- ~/.no-mistakes/worktrees/32d18ed9638d/01M4FB4GY4BGX79TB64806ZHF7/.fm-live-validation/root-0.sh"
  ],
  "pass": true
}

CLI run 0:
fm-lint.sh: ShellCheck 0.11.0 (pinned 0.11.0)
fm-lint.sh: full ShellCheck extended analysis enabled

Root lifecycle run 0:
format	fm-lint-roots-v1
meta	shellcheck_version	0.11.0
meta	platform	Darwin arm64
meta	image_os	unknown
meta	image_version	unknown
meta	mode	full
meta	partition	all
meta	jobs	2
meta	bounds_enforced	0
meta	root_deadline_seconds	unbounded
meta	root_kill_grace_seconds	unbounded
meta	root_memory_limit_kib	unbounded
meta	timing_mechanism	none
begin	2	~/.no-mistakes/worktrees/32d18ed9638d/01M4FB4GY4BGX79TB64806ZHF7/.fm-live-validation/root-1.sh	1	full	1791525106320
begin	1	~/.no-mistakes/worktrees/32d18ed9638d/01M4FB4GY4BGX79TB64806ZHF7/.fm-live-validation/root-0.sh	0	full	1791525106499
end	1	~/.no-mistakes/worktrees/32d18ed9638d/01M4FB4GY4BGX79TB64806ZHF7/.fm-live-validation/root-0.sh	0	full	1791525106499	1791525127080	20581	0	ok	26112	1
end	2	~/.no-mistakes/worktrees/32d18ed9638d/01M4FB4GY4BGX79TB64806ZHF7/.fm-live-validation/root-1.sh	1	full	1791525106320	1791525127401	21077	0	ok	26144	1
begin	3	~/.no-mistakes/worktrees/32d18ed9638d/01M4FB4GY4BGX79TB64806ZHF7/.fm-live-validation/root-2.sh	0	full	1791525128033
begin	4	~/.no-mistakes/worktrees/32d18ed9638d/01M4FB4GY4BGX79TB64806ZHF7/.fm-live-validation/root-3.sh	1	full	1791525128347
end	4	~/.no-mistakes/worktrees/32d18ed9638d/01M4FB4GY4BGX79TB64806ZHF7/.fm-live-validation/root-3.sh	1	full	1791525128347	1791525133372	5025	0	ok	26128	1
begin	6	~/.no-mistakes/worktrees/32d18ed9638d/01M4FB4GY4BGX79TB64806ZHF7/.fm-live-validation/root-5.sh	1	full	1791525133499
end	3	~/.no-mistakes/worktrees/32d18ed9638d/01M4FB4GY4BGX79TB64806ZHF7/.fm-live-validation/root-2.sh	0	full	1791525128033	1791525133609	5576	0	ok	26144	1
begin	5	~/.no-mistakes/worktrees/32d18ed9638d/01M4FB4GY4BGX79TB64806ZHF7/.fm-live-validation/root-4.sh	0	full	1791525133634
end	6	~/.no-mistakes/worktrees/32d18ed9638d/01M4FB4GY4BGX79TB64806ZHF7/.fm-live-validation/root-5.sh	1	full	1791525133499	1791525150839	17334	0	ok	26144	1
end	5	~/.no-mistakes/worktrees/32d18ed9638d/01M4FB4GY4BGX79TB64806ZHF7/.fm-live-validation/root-4.sh	0	full	1791525133634	1791525151037	17403	0	ok	24352	1
meta	roots_begun	6
meta	roots_completed	6
meta	roots_unfinished	0
meta	result_exit	0

=== four-disabled ===
{
  "name": "four-disabled",
  "runs": 4,
  "roots_per_run": 6,
  "jobs": 2,
  "slot_cap": "off",
  "load": null,
  "peak_live_shellcheck": 8,
  "expected_peak": 8,
  "held_observations": [
    8
  ],
  "real_analysis_processes": 24,
  "real_analysis_exits": [
    0
  ],
  "lint_exits": [
    0,
    0,
    0,
    0
  ],
  "actual_processes": [
    "75954 74606 T    ~/.local/bin/shellcheck --norc --external-sources -- ~/.no-mistakes/worktrees/32d18ed9638d/01M4FB4GY4BGX79TB64806ZHF7/.fm-live-validation/root-0.sh",
    "76727 74904 T    ~/.local/bin/shellcheck --norc --external-sources -- ~/.no-mistakes/worktrees/32d18ed9638d/01M4FB4GY4BGX79TB64806ZHF7/.fm-live-validation/root-1.sh",
    "77636 76770 T    ~/.local/bin/shellcheck --norc --external-sources -- ~/.no-mistakes/worktrees/32d18ed9638d/01M4FB4GY4BGX79TB64806ZHF7/.fm-live-validation/root-1.sh",
    "77638 76494 T    ~/.local/bin/shellcheck --norc --external-sources -- ~/.no-mistakes/worktrees/32d18ed9638d/01M4FB4GY4BGX79TB64806ZHF7/.fm-live-validation/root-1.sh",
    "77654 76874 T    ~/.local/bin/shellcheck --norc --external-sources -- ~/.no-mistakes/worktrees/32d18ed9638d/01M4FB4GY4BGX79TB64806ZHF7/.fm-live-validation/root-0.sh",
    "77660 76695 T    ~/.local/bin/shellcheck --norc --external-sources -- ~/.no-mistakes/worktrees/32d18ed9638d/01M4FB4GY4BGX79TB64806ZHF7/.fm-live-validation/root-0.sh",
    "78366 77902 T    ~/.local/bin/shellcheck --norc --external-sources -- ~/.no-mistakes/worktrees/32d18ed9638d/01M4FB4GY4BGX79TB64806ZHF7/.fm-live-validation/root-0.sh",
    "78728 77906 T    ~/.local/bin/shellcheck --norc --external-sources -- ~/.no-mistakes/worktrees/32d18ed9638d/01M4FB4GY4BGX79TB64806ZHF7/.fm-live-validation/root-1.sh"
  ],
  "pass": true
}

CLI run 0:
fm-lint.sh: ShellCheck 0.11.0 (pinned 0.11.0)
fm-lint.sh: full ShellCheck extended analysis enabled

Root lifecycle run 0:
format	fm-lint-roots-v1
meta	shellcheck_version	0.11.0
meta	platform	Darwin arm64
meta	image_os	unknown
meta	image_version	unknown
meta	mode	full
meta	partition	all
meta	jobs	2
meta	bounds_enforced	0
meta	root_deadline_seconds	unbounded
meta	root_kill_grace_seconds	unbounded
meta	root_memory_limit_kib	unbounded
meta	timing_mechanism	none
begin	2	~/.no-mistakes/worktrees/32d18ed9638d/01M4FB4GY4BGX79TB64806ZHF7/.fm-live-validation/root-1.sh	1	full	1791525196613
begin	1	~/.no-mistakes/worktrees/32d18ed9638d/01M4FB4GY4BGX79TB64806ZHF7/.fm-live-validation/root-0.sh	0	full	1791525196670
end	1	~/.no-mistakes/worktrees/32d18ed9638d/01M4FB4GY4BGX79TB64806ZHF7/.fm-live-validation/root-0.sh	0	full	1791525196670	1791525216718	20048	0	ok	26160	1
begin	3	~/.no-mistakes/worktrees/32d18ed9638d/01M4FB4GY4BGX79TB64806ZHF7/.fm-live-validation/root-2.sh	0	full	1791525216738
end	2	~/.no-mistakes/worktrees/32d18ed9638d/01M4FB4GY4BGX79TB64806ZHF7/.fm-live-validation/root-1.sh	1	full	1791525196613	1791525216980	20367	0	ok	26128	1
begin	4	~/.no-mistakes/worktrees/32d18ed9638d/01M4FB4GY4BGX79TB64806ZHF7/.fm-live-validation/root-3.sh	1	full	1791525217027
end	3	~/.no-mistakes/worktrees/32d18ed9638d/01M4FB4GY4BGX79TB64806ZHF7/.fm-live-validation/root-2.sh	0	full	1791525216738	1791525218015	1277	0	ok	26128	1
begin	5	~/.no-mistakes/worktrees/32d18ed9638d/01M4FB4GY4BGX79TB64806ZHF7/.fm-live-validation/root-4.sh	0	full	1791525218103
end	4	~/.no-mistakes/worktrees/32d18ed9638d/01M4FB4GY4BGX79TB64806ZHF7/.fm-live-validation/root-3.sh	1	full	1791525217027	1791525219787	2760	0	ok	26128	1
begin	6	~/.no-mistakes/worktrees/32d18ed9638d/01M4FB4GY4BGX79TB64806ZHF7/.fm-live-validation/root-5.sh	1	full	1791525219941
end	5	~/.no-mistakes/worktrees/32d18ed9638d/01M4FB4GY4BGX79TB64806ZHF7/.fm-live-validation/root-4.sh	0	full	1791525218103	1791525222488	4385	0	ok	26128	1
end	6	~/.no-mistakes/worktrees/32d18ed9638d/01M4FB4GY4BGX79TB64806ZHF7/.fm-live-validation/root-5.sh	1	full	1791525219941	1791525223671	3730	0	ok	26128	1
meta	roots_begun	6
meta	roots_completed	6
meta	roots_unfinished	0
meta	result_exit	0

... [14555 bytes truncated] ...

4806ZHF7/.fm-live-validation/root-5.sh"
  ],
  "pass": true
}

=== load-heavy-floor ===
{
  "name": "load-heavy-floor",
  "ncpu": 18,
  "configured_cap": 6,
  "load": 100000,
  "expected": 2,
  "held_live_shellcheck": 2,
  "gate_exits": [
    0,
    0,
    0,
    0,
    0,
    0,
    0
  ],
  "actual_processes": [
    "11573  7596 T    ~/.local/bin/shellcheck --norc --external-sources -- ~/.no-mistakes/worktrees/32d18ed9638d/01M4FB4GY4BGX79TB64806ZHF7/.fm-live-validation/root-2.sh",
    "14366  9354 T    ~/.local/bin/shellcheck --norc --external-sources -- ~/.no-mistakes/worktrees/32d18ed9638d/01M4FB4GY4BGX79TB64806ZHF7/.fm-live-validation/root-5.sh"
  ],
  "pass": true
}

=== occupancy-scan ===
{
  "path": "scan",
  "ncpu": 18,
  "cap": 9,
  "load": 43,
  "allowance": 2,
  "held_occupants": 8,
  "no_admission_above_allowance": true,
  "admitted_after_occupancy_reached": 1,
  "exit": 0,
  "actual_process": [
    "73980 72468 T    ~/.local/bin/shellcheck --norc --external-sources -- ~/.no-mistakes/worktrees/32d18ed9638d/01M4FB4GY4BGX79TB64806ZHF7/.fm-live-validation/root-0.sh"
  ]
}

=== occupancy-wake ===
{
  "path": "wake",
  "ncpu": 18,
  "cap": 9,
  "load": 43,
  "allowance": 2,
  "held_occupants": 8,
  "no_admission_above_allowance": true,
  "admitted_after_occupancy_reached": 1,
  "exit": 0,
  "actual_process": [
    "27824 25545 T    ~/.local/bin/shellcheck --norc --external-sources -- ~/.no-mistakes/worktrees/32d18ed9638d/01M4FB4GY4BGX79TB64806ZHF7/.fm-live-validation/root-0.sh"
  ]
}

=== load-probe-cadence ===
{
  "command": [
    "/usr/bin/perl",
    "~/.no-mistakes/worktrees/32d18ed9638d/01M4FB4GY4BGX79TB64806ZHF7/bin/fm-lint-cache.pl",
    "gate",
    "~/.no-mistakes/worktrees/32d18ed9638d/01M4FB4GY4BGX79TB64806ZHF7/.fm-live-validation/load-probe-cadence/slots",
    "18",
    "~/.no-mistakes/worktrees/32d18ed9638d/01M4FB4GY4BGX79TB64806ZHF7/.fm-live-validation/load-probe-cadence/queue-ms",
    "--",
    "~/.local/bin/shellcheck",
    "--norc",
    "--external-sources",
    "--",
    "~/.no-mistakes/worktrees/32d18ed9638d/01M4FB4GY4BGX79TB64806ZHF7/.fm-live-validation/root-0.sh"
  ],
  "blocked_observation_seconds": 7,
  "real_sysctl_probes": [
    {
      "pid": 83537,
      "time": 1791526266.667687,
      "args": [
        "-n",
        "vm.loadavg"
      ]
    },
    {
      "pid": 90658,
      "time": 1791526273.403472,
      "args": [
        "-n",
        "vm.loadavg"
      ]
    }
  ],
  "probe_intervals_seconds": [
    6.735785007476807
  ],
  "exit": 0
}

=== queue-local ===
{
  "command": [
    "~/.no-mistakes/worktrees/32d18ed9638d/01M4FB4GY4BGX79TB64806ZHF7/bin/fm-lint.sh",
    "--jobs",
    "1",
    "--telemetry",
    "~/.no-mistakes/worktrees/32d18ed9638d/01M4FB4GY4BGX79TB64806ZHF7/.fm-live-validation/queue-local/telemetry.tsv",
    "~/.no-mistakes/worktrees/32d18ed9638d/01M4FB4GY4BGX79TB64806ZHF7/.fm-live-validation/root-0.sh"
  ],
  "exit": 0,
  "held_seconds": 5,
  "root_wall_ms": 9736,
  "analysis_ms": 3698,
  "reason": "ok",
  "cli": "fm-lint.sh: ShellCheck 0.11.0 (pinned 0.11.0)\nfm-lint.sh: full ShellCheck extended analysis enabled\n",
  "roots": "format\tfm-lint-roots-v1\nmeta\tshellcheck_version\t0.11.0\nmeta\tplatform\tDarwin arm64\nmeta\timage_os\tunknown\nmeta\timage_version\tunknown\nmeta\tmode\tfull\nmeta\tpartition\tall\nmeta\tjobs\t1\nmeta\tbounds_enforced\t0\nmeta\troot_deadline_seconds\tunbounded\nmeta\troot_kill_grace_seconds\tunbounded\nmeta\troot_memory_limit_kib\tunbounded\nmeta\ttiming_mechanism\tnone\nbegin\t1\t~/.no-mistakes/worktrees/32d18ed9638d/01M4FB4GY4BGX79TB64806ZHF7/.fm-live-validation/root-0.sh\t0\tfull\t1791525862492\nend\t1\t~/.no-mistakes/worktrees/32d18ed9638d/01M4FB4GY4BGX79TB64806ZHF7/.fm-live-validation/root-0.sh\t0\tfull\t1791525862492\t1791525872228\t3698\t0\tok\t26144\t1\nmeta\troots_begun\t1\nmeta\troots_completed\t1\nmeta\troots_unfinished\t0\nmeta\tresult_exit\t0\n",
  "telemetry": "format\tfm-lint-telemetry-v1\ngit_head\tf1600a06139e0dd10bf8a79974abfc849d0050ba\ncontent_cksum\t3287029876-132\nshellcheck_version\t0.11.0\nanalysis_mode\tfull\npartition\tall\njobs\t1\nroot_bounds_enforced\t0\nroot_deadline_seconds\tunbounded\nroot_kill_grace_seconds\tunbounded\nroot_memory_limit_kib\tunbounded\nroot_timing_mechanism\tnone\nroot_count\t1\ndirect_lines\t103\ndirect_bytes\t2455\nsource_directives\t0\nsource_boundary_directives\t0\nsource_followed_directives\t0\nsource_target_count\t0\nshard_1_weight_bytes\t2455\nshard_2_weight_bytes\t0\nwall_seconds\t16\nworker_wall_sum_seconds\t10.63\nmax_worker_wall_seconds\t10.43\nuser_seconds\t0.10\nsystem_seconds\t0.16\nmax_worker_rss_kib\t26144\nworker_rss_sum_kib\t29088\nshellcheck_processes_start\t8\nshellcheck_processes_end\t10\nload_average_start\t179.16/164.68/146.00\nload_average_end\t151.83/159.35/144.52\naggregate_cpu_percent_start\t898.80\naggregate_cpu_percent_end\t667.20\nresult_exit\t0\n"
}

=== queue-watchdog ===
{
  "command": [
    "/usr/bin/perl",
    "~/.no-mistakes/worktrees/32d18ed9638d/01M4FB4GY4BGX79TB64806ZHF7/bin/fm-lint-cache.pl",
    "gate",
    "~/.no-mistakes/worktrees/32d18ed9638d/01M4FB4GY4BGX79TB64806ZHF7/.fm-live-validation/queue-watchdog/slots",
    "18",
    "~/.no-mistakes/worktrees/32d18ed9638d/01M4FB4GY4BGX79TB64806ZHF7/.fm-live-validation/queue-watchdog/queue-ms",
    "--",
    "/bin/bash",
    "~/.no-mistakes/worktrees/32d18ed9638d/01M4FB4GY4BGX79TB64806ZHF7/bin/fm-lint.sh",
    "--internal-timed",
    "3",
    "1",
    "~/.local/bin/shellcheck",
    "--norc",
    "--external-sources",
    "--",
    "~/.no-mistakes/worktrees/32d18ed9638d/01M4FB4GY4BGX79TB64806ZHF7/.fm-live-validation/root-0.sh"
  ],
  "held_seconds": 6,
  "watchdog_deadline_seconds": 3,
  "queue_ms": 6474,
  "exit": 0,
  "output": ""
}

=== slot-lifetime ===
{
  "first_gate_killed": true,
  "wrapper_killed": true,
  "slot_locked_while_real_descendant_alive": true,
  "contender_held_before_descendant_exit": true,
  "contender_admitted_after_exit": true,
  "contender_exit": 0,
  "protected_snapshot": [
    "67395     1 T    ~/.local/bin/shellcheck --norc --external-sources -- ~/.no-mistakes/worktrees/32d18ed9638d/01M4FB4GY4BGX79TB64806ZHF7/.fm-live-validation/root-0.sh",
    "67528  6001 Ss   /usr/bin/perl ~/.no-mistakes/worktrees/32d18ed9638d/01M4FB4GY4BGX79TB64806ZHF7/bin/fm-lint-cache.pl gate ~/.no-mistakes/worktrees/32d18ed9638d/01M4FB4GY4BGX79TB64806ZHF7/.fm-live-validation/slot-lifetime/slots 18 ~/.no-mistakes/worktrees/32d18ed9638d/01M4FB4GY4BGX79TB64806ZHF7/.fm-live-validation/slot-lifetime/second/wait-ms -- ~/.no-mistakes/worktrees/32d18ed9638d/01M4FB4GY4BGX79TB64806ZHF7/.fm-live-validation/bin/shellcheck --norc --external-sources -- ~/.no-mistakes/worktrees/32d18ed9638d/01M4FB4GY4BGX79TB64806ZHF7/.fm-live-validation/root-0.sh"
  ],
  "second_events": "{\"event\": \"started\", \"pid\": 90489, \"wrapper\": 88817, \"time\": 1791526013.258795, \"args\": [\"--norc\", \"--external-sources\", \"--\", \"~/.no-mistakes/worktrees/32d18ed9638d/01M4FB4GY4BGX79TB64806ZHF7/.fm-live-validation/root-0.sh\"]}\n{\"event\": \"finished\", \"pid\": 90489, \"time\": 1791526014.906805, \"rc\": 0}\n"
}

=== actual watchdog cleanup ===
{
  "gate_killed": true,
  "watchdog": "actual fm-lint.sh --internal-timed",
  "protected_actual_shellcheck_pid": 27105,
  "admitted_after_cleanup": true,
  "overlap": false,
  "contender_exit": 0,
  "observations": [
    {
      "protected_live": [],
      "contender_admitted": true,
      "time": 1791526595.330496
    }
  ],
  "watchdog_output": ""
}
Evidence: CLI configuration guards and diagnostic parity

Source: CLI configuration guards and diagnostic parity


=== invalid-host-cap ===
{
  "name": "invalid-host-cap",
  "command": [
    "~/.no-mistakes/worktrees/32d18ed9638d/01M4FB4GY4BGX79TB64806ZHF7/bin/fm-lint.sh",
    "--jobs",
    "2",
    "~/.no-mistakes/worktrees/32d18ed9638d/01M4FB4GY4BGX79TB64806ZHF7/.fm-live-validation/root-0.sh"
  ],
  "environment": {
    "FM_LINT_HOST_SLOTS": "zero",
    "FM_LINT_SLOT_DIR": "~/.no-mistakes/worktrees/32d18ed9638d/01M4FB4GY4BGX79TB64806ZHF7/.fm-live-validation/invalid-slots"
  },
  "exit": 2,
  "expected_exit": 2,
  "output": "fm-lint.sh: ShellCheck 0.11.0 (pinned 0.11.0)\nfm-lint.sh: full ShellCheck extended analysis enabled\nfm-lint.sh: FM_LINT_HOST_SLOTS must be a positive integer, got zero.\n"
}

=== slot-dir-unavailable ===
{
  "name": "slot-dir-unavailable",
  "command": [
    "~/.no-mistakes/worktrees/32d18ed9638d/01M4FB4GY4BGX79TB64806ZHF7/bin/fm-lint.sh",
    "--jobs",
    "2",
    "~/.no-mistakes/worktrees/32d18ed9638d/01M4FB4GY4BGX79TB64806ZHF7/.fm-live-validation/root-0.sh"
  ],
  "environment": {
    "FM_LINT_SLOT_DIR": "~/.no-mistakes/worktrees/32d18ed9638d/01M4FB4GY4BGX79TB64806ZHF7/.fm-live-validation/root-0.sh/blocked"
  },
  "exit": 0,
  "expected_exit": 0,
  "output": "fm-lint.sh: ShellCheck 0.11.0 (pinned 0.11.0)\nfm-lint.sh: full ShellCheck extended analysis enabled\nfm-lint: host slot directory unavailable; running without the host-wide ShellCheck bound\n"
}

=== slot-file-unavailable ===
{
  "name": "slot-file-unavailable",
  "command": [
    "~/.no-mistakes/worktrees/32d18ed9638d/01M4FB4GY4BGX79TB64806ZHF7/bin/fm-lint.sh",
    "--jobs",
    "2",
    "~/.no-mistakes/worktrees/32d18ed9638d/01M4FB4GY4BGX79TB64806ZHF7/.fm-live-validation/root-0.sh"
  ],
  "environment": {
    "FM_LINT_SLOT_DIR": "~/.no-mistakes/worktrees/32d18ed9638d/01M4FB4GY4BGX79TB64806ZHF7/.fm-live-validation/blocked-slots",
    "FM_LINT_HOST_SLOTS": "3"
  },
  "exit": 0,
  "expected_exit": 0,
  "output": "fm-lint.sh: ShellCheck 0.11.0 (pinned 0.11.0)\nfm-lint.sh: full ShellCheck extended analysis enabled\nfm-lint: host slot directory unavailable; running without the host-wide ShellCheck bound\n"
}

=== findings-disabled ===
{
  "name": "findings-disabled",
  "command": [
    "~/.no-mistakes/worktrees/32d18ed9638d/01M4FB4GY4BGX79TB64806ZHF7/bin/fm-lint.sh",
    "--jobs",
    "2",
    "~/.no-mistakes/worktrees/32d18ed9638d/01M4FB4GY4BGX79TB64806ZHF7/.fm-live-validation/finding.sh"
  ],
  "environment": {
    "FM_LINT_SLOT_DIR": "off"
  },
  "exit": 1,
  "expected_exit": 1,
  "output": "fm-lint.sh: ShellCheck 0.11.0 (pinned 0.11.0)\nfm-lint.sh: full ShellCheck extended analysis enabled\n\nIn ~/.no-mistakes/worktrees/32d18ed9638d/01M4FB4GY4BGX79TB64806ZHF7/.fm-live-validation/finding.sh line 2:\nprintf \"%s\\n\" $1\n              ^-- SC2086 (info): Double quote to prevent globbing and word splitting.\n\nDid you mean:\nprintf \"%s\\n\" \"$1\"\n\nFor more information:\n  https://www.shellcheck.net/wiki/SC2086 -- Double quote to prevent globbing ...\n"
}

=== findings-gated ===
{
  "name": "findings-gated",
  "command": [
    "~/.no-mistakes/worktrees/32d18ed9638d/01M4FB4GY4BGX79TB64806ZHF7/bin/fm-lint.sh",
    "--jobs",
    "2",
    "~/.no-mistakes/worktrees/32d18ed9638d/01M4FB4GY4BGX79TB64806ZHF7/.fm-live-validation/finding.sh"
  ],
  "environment": {
    "FM_LINT_SLOT_DIR": "~/.no-mistakes/worktrees/32d18ed9638d/01M4FB4GY4BGX79TB64806ZHF7/.fm-live-validation/finding-slots",
    "FM_LINT_HOST_SLOTS": "3"
  },
  "exit": 1,
  "expected_exit": 1,
  "output": "fm-lint.sh: ShellCheck 0.11.0 (pinned 0.11.0)\nfm-lint.sh: full ShellCheck extended analysis enabled\n\nIn ~/.no-mistakes/worktrees/32d18ed9638d/01M4FB4GY4BGX79TB64806ZHF7/.fm-live-validation/finding.sh line 2:\nprintf \"%s\\n\" $1\n              ^-- SC2086 (info): Double quote to prevent globbing and word splitting.\n\nDid you mean:\nprintf \"%s\\n\" \"$1\"\n\nFor more information:\n  https://www.shellcheck.net/wiki/SC2086 -- Double quote to prevent globbing ...\n"
}

=== findings-unavailable ===
{
  "name": "findings-unavailable",
  "command": [
    "~/.no-mistakes/worktrees/32d18ed9638d/01M4FB4GY4BGX79TB64806ZHF7/bin/fm-lint.sh",
    "--jobs",
    "2",
    "~/.no-mistakes/worktrees/32d18ed9638d/01M4FB4GY4BGX79TB64806ZHF7/.fm-live-validation/finding.sh"
  ],
  "environment": {
    "FM_LINT_SLOT_DIR": "~/.no-mistakes/worktrees/32d18ed9638d/01M4FB4GY4BGX79TB64806ZHF7/.fm-live-validation/finding.sh/blocked"
  },
  "exit": 1,
  "expected_exit": 1,
  "output": "fm-lint.sh: ShellCheck 0.11.0 (pinned 0.11.0)\nfm-lint.sh: full ShellCheck extended analysis enabled\n\nIn ~/.no-mistakes/worktrees/32d18ed9638d/01M4FB4GY4BGX79TB64806ZHF7/.fm-live-validation/finding.sh line 2:\nprintf \"%s\\n\" $1\n              ^-- SC2086 (info): Double quote to prevent globbing and word splitting.\n\nDid you mean:\nprintf \"%s\\n\" \"$1\"\n\nFor more information:\n  https://www.shellcheck.net/wiki/SC2086 -- Double quote to prevent globbing ...\nfm-lint: host slot directory unavailable; running without the host-wide ShellCheck bound\n"
}
Evidence: Targeted regressions and failing-before concurrency proof

Source: Targeted regressions and failing-before concurrency proof

TARGETED REGRESSION PROOF (not marked live; analyzer/locking fault fixtures are controlled dependencies)

Base commit concurrency reproduction:
not ok - six concurrent runs reached 12 live ShellCheck processes, expected at most 3 host-wide

Current implementation concurrency/load/guard regressions:
ok - six concurrent runs peak at 12 ShellCheck processes ungated and 3 with a three-slot pool
ok - host load shrinks the shared ShellCheck slots to a two-slot floor and idle hosts use all of them
ok - host caps follow the load threshold and both acquisition paths respect total occupancy
ok - the slot pool can be disabled, rejects bad settings, and never fails lint when unusable

I/O fallback regression after increasing its controller wait:
analysis ran
analysis ran
analysis ran
ok - slot open and non-contention scan/wait lock failures warn and run ungated

Initial slot lifetime regression:
ok - six concurrent runs peak at 12 ShellCheck processes ungated and 3 with a three-slot pool
ok - host load shrinks the shared ShellCheck slots to a two-slot floor and idle hosts use all of them
ok - host caps follow the load threshold and both acquisition paths respect total occupancy
ok - the slot pool can be disabled, rejects bad settings, and never fails lint when unusable
analysis ran
analysis ran
analysis ran
ok - slot open and non-contention scan/wait lock failures warn and run ungated
ok - inherited slots prevent overlap after gate death and allow admission after watchdog cleanup

Queued timing/retry regression after correcting controller waits:
CAPABILITY bounded=none
OBSERVED retry=0 rc=0 reason=ok duration=10400
OBSERVED retry=1 rc=0 reason=memory-fallback duration=16437
ok - queued roots and memory retries record precise analysis durations without EPOCHREALTIME

Test-only changes: reset each contention-controller stage to its own 30-second wait, keep fake analyzers held for 90 seconds, and allow 30 seconds for I/O/timing fixture startup and bookkeeping. Product admission rules, deadlines, and contention assertions are unchanged.
Evidence: Native memory-bound and heap-limit capability blockers

Source: Native memory-bound and heap-limit capability blockers

Full CLI attempt: FM_LINT_REQUIRE_BOUNDS=1 FM_LINT_ROOT_SECONDS=3 FM_LINT_HOST_SLOTS=1 FM_LINT_ROOT_MEMORY_KIB=12582912 bin/fm-lint.sh --jobs 1 --telemetry <disposable path> <clean fixture>
fm-lint.sh: ShellCheck 0.11.0 (pinned 0.11.0)
fm-lint.sh: full ShellCheck extended analysis enabled
fm-lint.sh: bounds required but per-root memory limit FM_LINT_ROOT_MEMORY_KIB=12582912 KiB is not enforceable on this host (ulimit -v).
fm-lint.sh: refusing to lint uncapped under FM_LINT_REQUIRE_BOUNDS=1.

Alternate route: docker info --format {{.OSType}} did not return within its 30-second command deadline. No container/VM was started and no system state changed.

Attempt to impose a real GHC heap ceiling with GHCRTS=-M16m on a disposable source-following workload:
shellcheck: Most RTS options are disabled. Link with -rtsopts to enable them.
fm-lint.sh: ShellCheck  (pinned 0.11.0)
fm-lint.sh: ShellCheck 0.11.0 required for CI parity, found . Install 0.11.0 with bin/fm-install-shellcheck.sh <destination-directory>.

The installed ShellCheck binary has RTS options disabled; no credentials, system packages, global configuration, or real user data were modified.
Evidence: Actual ShellCheck contention instrumentation

Source: Actual ShellCheck contention instrumentation

#!~/.pyenv/versions/3.13.6/bin/python3
import os,sys,subprocess,signal,time,json,fcntl,pathlib
real='~/.local/bin/shellcheck'
if sys.argv[1:] == ['--version']:
    os.execv(real,[real,*sys.argv[1:]])
case=pathlib.Path(os.environ['FM_LIVE_CASE'])
child=subprocess.Popen([real,*sys.argv[1:]],close_fds=False)
os.kill(child.pid, signal.SIGSTOP)
with open(case/'events.lock','a') as lock:
    fcntl.flock(lock,fcntl.LOCK_EX)
    (case/f'active.{child.pid}').write_text(str(os.getpid()))
    with open(case/'events.jsonl','a') as log:
        log.write(json.dumps(dict(event='started',pid=child.pid,wrapper=os.getpid(),time=time.time(),args=sys.argv[1:]))+'\n')
try:
    until=time.monotonic()+90
    while not (case/'release').exists():
        if time.monotonic()>until: raise RuntimeError('controller did not release actual ShellCheck')
        time.sleep(.02)
    os.kill(child.pid,signal.SIGCONT)
    rc=child.wait(timeout=30)
finally:
    if child.poll() is None:
        os.kill(child.pid,signal.SIGCONT)
        child.kill(); child.wait()
    (case/f'active.{child.pid}').unlink(missing_ok=True)
    with open(case/'events.jsonl','a') as log:
        log.write(json.dumps(dict(event='finished',pid=child.pid,time=time.time(),rc=child.returncode))+'\n')
sys.exit(rc)

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

🔧 **Review** - 4 issues found → auto-fixed (4) → no changes applied ✅
  • 🚨 bin/fm-lint-cache.pl:60 - The allowance contradicts the required criterion: "the allowance shrinks as 1-minute load rises past 2 x ncpu." The added calculation is int(2 * ncpu - load + 0.5), clamped to the cap, rather than subtracting only excess load from the cap. With 18 cores and the default cap of 9, load 30 permits only 6 slots and load 36 permits only 2; the stated behavior requires all 9 until load exceeds 36. The same contract is documented at bin/fm-lint-cache.pl:21 and bin/fm-lint.sh:68. Tests at tests/fm-lint.test.sh:1165 and :1169 exercise only extreme and zero load, missing this boundary. Reconcile the implementation with the authorized threshold by reducing the cap only for load above twice the core count.
  • 🚨 bin/fm-lint-cache.pl:64 - Slot-file failures are treated as contention and can queue a root forever. For example, a writable slot directory whose existing allowed slot files are non-writable passes the directory check at :56, but every open at :64 and :71 fails; the loop sleeps and retries indefinitely without warning or starting analysis. Exhausted filesystem metadata produces the same path. Non-contention flock failures at :65 and :75 can instead produce a tight retry loop. Because the gate is outside the watchdog at bin/fm-lint.sh:297, no root deadline terminates either case. Distinguish busy locks from slot-pool I/O/locking failures at the shared gate boundary and use the already-authorized warning-and-ungated fallback at :85.
  • 🚨 bin/fm-lint-cache.pl:93 - The slot can be released while its bounded ShellCheck tree is still running. Perl normally closes the opened slot descriptor on exec, so after the fork/exec at :93-96 only the gate retains the lock acquired at :65 or :80. On ordinary lint cancellation, bin/fm-lint.sh:1088 immediately TERM/KILLs the worker group, including this new gate. The watchdog launched through bin/fm-lint.sh:297 is in a separate group, and its analysis tree remains alive until its owner-death polling and TERM/KILL cleanup complete (bin/fm-timeout-lib.sh:318-336). An already-queued gate can therefore acquire the released slot and launch another analysis before the previous one exits, exceeding the advertised host-wide bound. Preserve the same slot lock through the protected command/watchdog lifetime, including gate death; ensure the launch at bin/fm-lint.sh:306 follows the same ownership invariant.
  • ⚠️ bin/fm-lint.sh:394 - Subtracting millisecond queue time from second-resolution timestamps can record negative root durations on Bash versions without EPOCHREALTIME, including macOS's system Bash. fm_lint_now_ms at :170 then truncates timestamps to whole seconds, while bin/fm-lint-cache.pl:89 records precise wait milliseconds. A root starting at T+0.1s, queueing for 1.5s, and finishing analysis at T+1.7s produces start=T000, end=(T+1)000, and duration_ms=-500 despite successful execution. The same mixed-precision inputs affect the retry budget at bin/fm-lint.sh:353; wait values enter through :313-315 and accumulate at :346 and :373. Use a consistent high-resolution clock for root timestamps and queue accounting rather than merely clamping the resulting duration.

🔧 Fix applied.
3 issues (2 errors, 1 warning) still open:

  • 🚨 bin/fm-lint-cache.pl:64 - Slot-file failures are treated as contention and can queue a root forever. For example, a writable slot directory whose existing allowed slot files are non-writable passes the directory check at :56, but every open at :64 and :71 fails; the loop sleeps and retries indefinitely without warning or starting analysis. Exhausted filesystem metadata produces the same path. Non-contention flock failures at :65 and :75 can instead produce a tight retry loop. Because the gate is outside the watchdog at bin/fm-lint.sh:297, no root deadline terminates either case. Distinguish busy locks from slot-pool I/O/locking failures at the shared gate boundary and use the already-authorized warning-and-ungated fallback at :85.
  • 🚨 bin/fm-lint-cache.pl:93 - The slot can be released while its bounded ShellCheck tree is still running. Perl normally closes the opened slot descriptor on exec, so after the fork/exec at :93-96 only the gate retains the lock acquired at :65 or :80. On ordinary lint cancellation, bin/fm-lint.sh:1088 immediately TERM/KILLs the worker group, including this new gate. The watchdog launched through bin/fm-lint.sh:297 is in a separate group, and its analysis tree remains alive until its owner-death polling and TERM/KILL cleanup complete (bin/fm-timeout-lib.sh:318-336). An already-queued gate can therefore acquire the released slot and launch another analysis before the previous one exits, exceeding the advertised host-wide bound. Preserve the same slot lock through the protected command/watchdog lifetime, including gate death; ensure the launch at bin/fm-lint.sh:306 follows the same ownership invariant.
  • ⚠️ bin/fm-lint.sh:394 - Subtracting millisecond queue time from second-resolution timestamps can record negative root durations on Bash versions without EPOCHREALTIME, including macOS's system Bash. fm_lint_now_ms at :170 then truncates timestamps to whole seconds, while bin/fm-lint-cache.pl:89 records precise wait milliseconds. A root starting at T+0.1s, queueing for 1.5s, and finishing analysis at T+1.7s produces start=T000, end=(T+1)000, and duration_ms=-500 despite successful execution. The same mixed-precision inputs affect the retry budget at bin/fm-lint.sh:353; wait values enter through :313-315 and accumulate at :346 and :373. Use a consistent high-resolution clock for root timestamps and queue accounting rather than merely clamping the resulting duration.

🔧 Fix applied.
6 issues (5 errors, 1 warning) still open:

  • 🚨 bin/fm-lint-cache.pl:64 - Slot-file failures are treated as contention and can queue a root forever. For example, a writable slot directory whose existing allowed slot files are non-writable passes the directory check at :56, but every open at :64 and :71 fails; the loop sleeps and retries indefinitely without warning or starting analysis. Exhausted filesystem metadata produces the same path. Non-contention flock failures at :65 and :75 can instead produce a tight retry loop. Because the gate is outside the watchdog at bin/fm-lint.sh:297, no root deadline terminates either case. Distinguish busy locks from slot-pool I/O/locking failures at the shared gate boundary and use the already-authorized warning-and-ungated fallback at :85.
  • 🚨 bin/fm-lint-cache.pl:93 - The slot can be released while its bounded ShellCheck tree is still running. Perl normally closes the opened slot descriptor on exec, so after the fork/exec at :93-96 only the gate retains the lock acquired at :65 or :80. On ordinary lint cancellation, bin/fm-lint.sh:1088 immediately TERM/KILLs the worker group, including this new gate. The watchdog launched through bin/fm-lint.sh:297 is in a separate group, and its analysis tree remains alive until its owner-death polling and TERM/KILL cleanup complete (bin/fm-timeout-lib.sh:318-336). An already-queued gate can therefore acquire the released slot and launch another analysis before the previous one exits, exceeding the advertised host-wide bound. Preserve the same slot lock through the protected command/watchdog lifetime, including gate death; ensure the launch at bin/fm-lint.sh:306 follows the same ownership invariant.
  • 🚨 bin/fm-lint-cache.pl:67 - Round 1 corrected the load calculation but left admission based on slot indices rather than total occupancy. Concrete sequence: on 18 cores, nine checks acquire slots at low load; load then rises to 43, making the allowance two. When slot.0 finishes, this branch immediately admits its replacement even while slots.1–8 remain occupied, restoring nine live checks despite the two-check allowance. The blocking acquisition at bin/fm-lint-cache.pl:75–87 also admits a slot selected before the allowance shrank without revalidating it. At the shared gate boundary, let existing checks finish but queue new admissions until total occupied capacity is below the current allowance; apply that invariant to both acquisition paths.
  • 🚨 tests/fm-lint.test.sh:1398 - Round 2 introduced a lifetime regression that rejects correct watchdog cleanup on bounded hosts. Killing the gate changes the watchdog's parent; bin/fm-timeout-lib.sh:332–334 consequently terminates the protected group. If its time/cache wrapper exits, :312–316 kills the remaining group, legitimately releasing the inherited slot. Nevertheless, this assertion forbids admission after a fixed 200 ms without establishing that the old tree remains alive. The same incorrect assumption appears at tests/fm-lint.test.sh:1401, and :1402 requires a descendant to survive cleanup that the watchdog intentionally performs. [INFERENCE from source] These assertions can fail with correct slot ownership. Keep the survivor checks in the unbounded fixture; in the bounded fixture, assert that admission does not overlap a live protected tree and permit admission after cleanup.
  • 🚨 docs/fm-test-portable-shards.md:139 - The required architecture proof is absent from the reviewed deliverable. Intent specifies: "five designs were compared on counted factors" and a reproduction showing "peak live processes of 2 x runs before and the slot cap after." The added documentation at :139–142 describes implementation and test invocation but supplies no counted comparison. The concurrency test at tests/fm-lint.test.sh:1148–1156 measures only gated runs; the disabled-gate test at :1243 executes one root without measuring concurrency, so neither supplies the paired baseline. Retain the five-design comparison and a counted ungated/gated reproduction, or obtain explicit approval to waive those proof requirements. This is missing source-verifiable evidence, not a request to execute the pipeline's later validation phase.
  • ⚠️ tests/fm-lint.test.sh:3188 - Simplification: Round 2 added the --host-slots test mode, duplicating the seven-test dispatch already present at tests/fm-lint.test.sh:3236–3242. The authorized findings require behavioral regressions, not another test-runner mode; the existing suite satisfies that requirement. Revert this component to the minimal fix by removing the additional dispatch branch and its documentation at docs/fm-test-portable-shards.md:142, while retaining the regression tests in the normal suite.

🔧 Fix applied.
8 issues (5 errors, 3 warnings) still open:

  • 🚨 bin/fm-lint-cache.pl:64 - Slot-file failures are treated as contention and can queue a root forever. For example, a writable slot directory whose existing allowed slot files are non-writable passes the directory check at :56, but every open at :64 and :71 fails; the loop sleeps and retries indefinitely without warning or starting analysis. Exhausted filesystem metadata produces the same path. Non-contention flock failures at :65 and :75 can instead produce a tight retry loop. Because the gate is outside the watchdog at bin/fm-lint.sh:297, no root deadline terminates either case. Distinguish busy locks from slot-pool I/O/locking failures at the shared gate boundary and use the already-authorized warning-and-ungated fallback at :85.
  • 🚨 bin/fm-lint-cache.pl:93 - The slot can be released while its bounded ShellCheck tree is still running. Perl normally closes the opened slot descriptor on exec, so after the fork/exec at :93-96 only the gate retains the lock acquired at :65 or :80. On ordinary lint cancellation, bin/fm-lint.sh:1088 immediately TERM/KILLs the worker group, including this new gate. The watchdog launched through bin/fm-lint.sh:297 is in a separate group, and its analysis tree remains alive until its owner-death polling and TERM/KILL cleanup complete (bin/fm-timeout-lib.sh:318-336). An already-queued gate can therefore acquire the released slot and launch another analysis before the previous one exits, exceeding the advertised host-wide bound. Preserve the same slot lock through the protected command/watchdog lifetime, including gate death; ensure the launch at bin/fm-lint.sh:306 follows the same ownership invariant.
  • 🚨 bin/fm-lint-cache.pl:67 - Round 1 corrected the load calculation but left admission based on slot indices rather than total occupancy. Concrete sequence: on 18 cores, nine checks acquire slots at low load; load then rises to 43, making the allowance two. When slot.0 finishes, this branch immediately admits its replacement even while slots.1–8 remain occupied, restoring nine live checks despite the two-check allowance. The blocking acquisition at bin/fm-lint-cache.pl:75–87 also admits a slot selected before the allowance shrank without revalidating it. At the shared gate boundary, let existing checks finish but queue new admissions until total occupied capacity is below the current allowance; apply that invariant to both acquisition paths.
  • 🚨 tests/fm-lint.test.sh:1398 - Round 2 introduced a lifetime regression that rejects correct watchdog cleanup on bounded hosts. Killing the gate changes the watchdog's parent; bin/fm-timeout-lib.sh:332–334 consequently terminates the protected group. If its time/cache wrapper exits, :312–316 kills the remaining group, legitimately releasing the inherited slot. Nevertheless, this assertion forbids admission after a fixed 200 ms without establishing that the old tree remains alive. The same incorrect assumption appears at tests/fm-lint.test.sh:1401, and :1402 requires a descendant to survive cleanup that the watchdog intentionally performs. [INFERENCE from source] These assertions can fail with correct slot ownership. Keep the survivor checks in the unbounded fixture; in the bounded fixture, assert that admission does not overlap a live protected tree and permit admission after cleanup.
  • ⚠️ tests/fm-lint.test.sh:3188 - Simplification: Round 2 added the --host-slots test mode, duplicating the seven-test dispatch already present at tests/fm-lint.test.sh:3236–3242. The authorized findings require behavioral regressions, not another test-runner mode; the existing suite satisfies that requirement. Revert this component to the minimal fix by removing the additional dispatch branch and its documentation at docs/fm-test-portable-shards.md:142, while retaining the regression tests in the normal suite.
  • 🚨 tests/fm-lint.test.sh:1152 - The authorized proof requirement remains incomplete after the fix following Round 3. The recorded decision requires: "Put the five-design counted comparison and a paired reproduction in the PR description and in the commit message." Every commit message from the base through HEAD was inspected; none contains the counted five-design comparison or paired reproduction results. Lines 1152–1160 now provide an executable baseline/gated fixture, but that is not the required retained comparison and measured proof in the commit message. Supply that evidence in the commit message without adding tracked docs, or obtain an explicit waiver. The later PR update remains pipeline-owned and is not the subject of this finding.
  • ⚠️ bin/fm-lint-cache.pl:79 - The occupancy fix following Round 3 introduces rapid process creation precisely when host load is excessive. On an 18-core Mac at load 43, the nine-slot pool permits two occupants but leaves seven files unlocked. Waiting gates therefore repeatedly take the has-free branch at lines 79–81 and invoke the external sysctl load reader at lines 48–50 on each 100 ms retry. With the intended 21 concurrent runs, up to 40 roots can be waiting; this path can generate hundreds of short-lived sysctl processes per second despite starting no analysis. Preserve occupancy revalidation, but use a bounded, slower load-recheck cadence across this branch and the blocking-wait path at lines 83–95 rather than multiplying 10 Hz load probes by the waiter count.
  • ⚠️ tests/fm-lint.test.sh:1123 - The concurrent-count fixture does not establish the concurrency its assertions require. It inherits FM_LINT_JOBS, so running the suite with the documented FM_LINT_JOBS=1 immediately makes the lone-run assertion at line 1150 reject correct behavior. Independently, each stub exits after only 500 ms at line 1108: if a worker or later run is delayed beyond that window on the overloaded host this change targets, correct code produces a lower peak. The exact twelve-process baseline added after Round 3 at line 1153 inherits this problem, as do the gated saturation assertion at line 1159 and idle-load assertion at line 1174. Set --jobs 2 explicitly in the shared launcher and use controller-coordinated hold/release with bounded waits so the peak comparison measures admission rather than scheduler speed.

🔧 No changes applied.
5 issues (2 errors, 3 warnings) still open:

  • 🚨 tests/fm-lint.test.sh:1398 - Round 2 introduced a lifetime regression that rejects correct watchdog cleanup on bounded hosts. Killing the gate changes the watchdog's parent; bin/fm-timeout-lib.sh:332–334 consequently terminates the protected group. If its time/cache wrapper exits, :312–316 kills the remaining group, legitimately releasing the inherited slot. Nevertheless, this assertion forbids admission after a fixed 200 ms without establishing that the old tree remains alive. The same incorrect assumption appears at tests/fm-lint.test.sh:1401, and :1402 requires a descendant to survive cleanup that the watchdog intentionally performs. [INFERENCE from source] These assertions can fail with correct slot ownership. Keep the survivor checks in the unbounded fixture; in the bounded fixture, assert that admission does not overlap a live protected tree and permit admission after cleanup.
  • ⚠️ tests/fm-lint.test.sh:3188 - Simplification: Round 2 added the --host-slots test mode, duplicating the seven-test dispatch already present at tests/fm-lint.test.sh:3236–3242. The authorized findings require behavioral regressions, not another test-runner mode; the existing suite satisfies that requirement. Revert this component to the minimal fix by removing the additional dispatch branch and its documentation at docs/fm-test-portable-shards.md:142, while retaining the regression tests in the normal suite.
  • 🚨 tests/fm-lint.test.sh:1152 - The authorized proof requirement remains incomplete after the fix following Round 3. The recorded decision requires: "Put the five-design counted comparison and a paired reproduction in the PR description and in the commit message." Every commit message from the base through HEAD was inspected; none contains the counted five-design comparison or paired reproduction results. Lines 1152–1160 now provide an executable baseline/gated fixture, but that is not the required retained comparison and measured proof in the commit message. Supply that evidence in the commit message without adding tracked docs, or obtain an explicit waiver. The later PR update remains pipeline-owned and is not the subject of this finding.
  • ⚠️ tests/fm-lint.test.sh:1123 - The concurrent-count fixture does not establish the concurrency its assertions require. It inherits FM_LINT_JOBS, so running the suite with the documented FM_LINT_JOBS=1 immediately makes the lone-run assertion at line 1150 reject correct behavior. Independently, each stub exits after only 500 ms at line 1108: if a worker or later run is delayed beyond that window on the overloaded host this change targets, correct code produces a lower peak. The exact twelve-process baseline added after Round 3 at line 1153 inherits this problem, as do the gated saturation assertion at line 1159 and idle-load assertion at line 1174. Set --jobs 2 explicitly in the shared launcher and use controller-coordinated hold/release with bounded waits so the peak comparison measures admission rather than scheduler speed.
  • ⚠️ tests/fm-lint.test.sh:1144 - Round 4's controller releases every held check as soon as the expected cap is reached, before establishing that the remaining workers have attempted admission. With staggered startup, even the original per-run-only implementation can pass: three checks start, the controller releases them at :1149, and delayed workers subsequently finish without raising the peak above three. The ungated baseline waits for twelve, so the paired runs do not establish equivalent contention. The same early-release path affects the three-slot assertion at tests/fm-lint.test.sh:1181–1184 and heavy-load floor assertion at tests/fm-lint.test.sh:1193–1195; the idle-cap comparison at tests/fm-lint.test.sh:1197–1199 also uses this helper. Coordinate contender readiness and retain the admitted checks through a bounded contention-observation phase before releasing them, so removing host-wide admission reliably fails the regression.

🔧 Fix applied.
✅ Re-checked - no issues remain.

✅ **Test** - passed

✅ No issues found.

  • Live validation: ✅ go - 11 of 12 scenarios driven live against the product
Scenario Result Live Evidence
A lone lint run retains its two analysis workers ✅ pass live Live-product evidence, lone-gated: peak two actual ShellCheck processes; all six roots completed successfully.
Concurrent lint runs share a three-slot bound instead of multiplying worker counts ✅ pass live Live-product evidence, four-disabled and four-gated: identical four-run workloads peaked at eight processes ungated and three gated; all 24 gated root results were successful.
Load preserves the default cap through the threshold and reduces admission above it ✅ pass live Live-product evidence: the 18-core fixture admitted nine actual analyzers at load 36, eight at load 37, and two under extreme load.
A reduced load allowance blocks admission while existing occupancy remains too high ✅ pass live Live-product evidence, occupancy-scan and occupancy-wake: neither path admitted with eight occupied slots and allowance two; both admitted after occupancy reached one.
A waiting gate probes native host load at a bounded cadence ✅ pass live Live-product evidence, load-probe-cadence: the instrumentation executed native sysctl; observed probe spacing was approximately 6.74 seconds.
A queued root excludes waiting time from its recorded analysis duration ✅ pass live Live-product evidence, queue-local: five-second slot hold, 9,736 ms root wall time, 3,698 ms recorded analysis duration, reason ok, exit zero.
A queue longer than the watchdog deadline does not consume the analysis deadline ✅ pass live Live-product evidence, queue-watchdog: six-second queue before the actual three-second watchdog; real ShellCheck completed with exit zero.
Gate death cannot release capacity while a protected descendant survives ✅ pass live Live-product evidence, slot-lifetime and actual watchdog cleanup: inherited capacity remained locked while the real descendant survived, and watchdog cleanup permitted admission without overlap.
An invalid host-slot setting is refused before creating the pool ✅ pass live CLI guard evidence, invalid-host-cap: FM_LINT_HOST_SLOTS=zero exited two, named the setting, and created no slot directory.
Unusable slot storage warns and runs analysis ungated ✅ pass live CLI guard evidence: unusable directory and slot-file cases warned and completed clean analysis; an unusable pool preserved a real finding and exit one.
Enabling or disabling the pool preserves finding diagnostics and exit status ✅ pass live CLI diagnostic evidence: gated and disabled runs produced byte-identical SC2086 output and exit one.
A genuinely memory-exhausted queued analysis retries with correct queue accounting ⏸️ untested no Native Bash rejected the probed address-space limits, including the public CLI's required-bound attempt. GHCRTS=-M16m could not impose a real heap ceiling because the installed ShellCheck disables RTS…
  • shellcheck --version: confirmed the available analyzer is ShellCheck 0.11.0.
  • Drove bin/fm-lint.sh --jobs 2 --telemetry &lt;isolated path&gt; &lt;six disposable roots&gt; with one and four concurrent runs. Instrumentation suspended actual ShellCheck processes after exec, established contention, then resumed their real analyses.
  • Compared four identical concurrent workloads with FM_LINT_SLOT_DIR=off versus FM_LINT_HOST_SLOTS=3: observed peaks of eight and three real ShellCheck processes.
  • Executed perl bin/fm-lint-cache.pl gate ... with real ShellCheck and synthetic load inputs: default cap admitted nine processes at load 36 and eight at load 37; extreme load reduced an explicit six-slot pool to two.
  • Exercised scan and lock-wakeup admission paths with eight occupied slot files and a load allowance of two; admission resumed only after occupancy fell below the allowance.
  • Measured native sysctl load probes while a gate was blocked; observed probe spacing exceeded two seconds.
  • Drove the public CLI with its only slot held for five seconds; inspected generated root lifecycle and telemetry output to verify queue exclusion from analysis duration.
  • Executed the actual gate and fm-lint.sh --internal-timed watchdog with a six-second queue preceding a three-second analysis deadline; real ShellCheck completed successfully.
  • Killed a gate and its immediate wrapper while a real ShellCheck descendant survived; verified capacity remained locked until that descendant exited.
  • Killed a gate protecting the actual watchdog; verified contender admission followed cleanup without overlapping a live protected process.
  • Drove invalid-cap, unusable-directory, unusable-slot-file, and real SC2086 finding scenarios through the public CLI; checked warnings, exit statuses, and gated/ungated diagnostic parity.
  • Ran seven selected host-slot regression functions from tests/fm-lint.test.sh, not the complete suite. Corrected controller waits and exercised the affected concurrency, I/O fallback, and timing checks afterward.
  • Ran test_host_slots_bound_concurrent_runs against disposable executable copies from the supplied base commit; it failed with twelve processes against the expected three-slot bound.
  • Attempted live memory-retry validation using native required bounds, GHCRTS=-M16m, and a read-only Docker availability probe; recorded the capability blockers.
  • Removed all disposable workspace fixtures and temporary test drivers, and verified no owned live fixture processes remained.
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

Each fm-lint.sh run limited itself to two workers, but nothing limited the
sum, so N concurrent runs started 2N ShellCheck processes (42 seen, some
near 4.8 GB). Every root now holds one host-wide flock slot: half the cores
(at least two), shrinking to a floor of two as 1-minute load passes twice the
cores. Runs queue instead of failing, and queue time stays out of the root
deadline and recorded duration.
Bound every waiting path to one fresh load probe per two seconds while preserving occupancy revalidation. Force two fixture workers and hold checks until the controller observes the required admission count. Replace scheduler-sensitive timestamp comparisons with observed lifecycle brackets.

Counted architecture comparison (binary factors):
Design | Host-wide bound | Preserves two workers | Resident daemon | Custom stale-lock recovery | Extra per-root gate process
Per-run only | 0 | 1 | 0 | 0 | 0
FM_LINT_JOBS=1 | 0 | 0 | 0 | 0 | 0
Bash mkdir slots | 1 | 1 | 0 | 1 | 0
Central daemon, disconnect-based release | 1 | 1 | 1 | 0 | 0
Flock pool | 1 | 1 | 0 | 0 | 1

The flock pool supplies the required host-wide bound and preserves two workers without a resident service or custom stale-lock recovery, at the cost of one gate process per root.

Paired reproduction: six simultaneous fm-lint runs, four identical roots per run, --jobs 2, inherited FM_LINT_JOBS=1, and controller-held fake ShellCheck processes. With FM_LINT_SLOT_DIR=off, the measured peak was 12 = 2 x 6 runs. With a three-slot pool and zero simulated load, the measured peak was 3 = the slot cap.

Focused verification: all seven host-slot regressions passed, covering concurrency, load thresholds and occupancy, disable/misconfiguration behavior, slot I/O and locking failures, gate-death ownership, and queue-excluded initial/retry timing. A real load-reader smoke observed two probes over 4.5 seconds on both the free-capacity and blocking-wakeup paths, with intervals of 2.622 and 2.653 seconds respectively; admission resumed below the occupancy allowance. No full repository test or lint suite was run.

The outer executor owns copying this counted comparison and paired reproduction into the PR description. No new tracked documentation was added.
@MrGTV-love
MrGTV-love merged commit b062a76 into main Oct 9, 2026
22 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant