Repository navigation
[do-not-mirror][do-not-merge] chore(fork): put the fork-process guards in forkctl and split the docs - #9
[do-not-mirror][do-not-merge] chore(fork): put the fork-process guards in forkctl and split the docs#9jdsika wants to merge 3 commits into
Conversation
ASCS-eV/PufferDrive is consumed as a git submodule internally while feeding
features back to Emerge-Lab/PufferDrive. This commit carries everything that
exists only in the fork, and is the single place fork-only content lives.
Branch model:
3.0 verbatim, fast-forward-only mirror of upstream/3.0
integration/3.0 default branch; this commit plus a linear stack of
single-commit features; what internal consumers pin
feature/<slug> one feature, exactly one commit
mirror/<slug> cherry-picked onto pure upstream/3.0 for upstream PRs
integration/3.0 is the default branch because GitHub reads the PR template,
the contributing guidelines, the agent instructions and scheduled workflows
only from the default branch, and none of those files may exist on 3.0
without destroying the mirror. A clone checks out the default branch, so the
copy of the instructions here is the process as far as every agent and every
submodule consumer is concerned; a rule that lives only in an open PR has no
effect.
Nothing reaches integration/3.0 until three gates pass:
1. CI green on the final commit.
2. Adversarial review by someone other than the author, with the findings
written down -- including the probes that found nothing.
3. Upstream-readiness proven rather than asserted: the commit cherry-picks
onto a branch cut from pure upstream/3.0 and produces an upstream-clean
diff. We stack only commits we could contribute today.
chore(fork): commits are exempt from gate 3, since fork-only infrastructure
can never be upstream-ready. They carry two orthogonal markers: do-not-mirror
(must never reach upstream) and do-not-merge (the merge button is never
pressed -- merging would create a second chore(fork): commit and break the
single-commit invariant that every sync asserts). Such a PR is folded into
this commit by fixup and closed unmerged.
Contents:
- .github/copilot-instructions.md: normative process for coding agents
- CONTRIBUTING.md: human orientation, same rules
- .github/pull_request_template.md: testing and stack-hygiene gates
- .github/workflows/sync-mirror.yml: fast-forward-only mirror sync that
refuses any non-fast-forward instead of rewriting the upstream base
- AGENTS.md / CLAUDE.md: fenced pointer so agents find the fork process
without colliding with upstream-owned content
- ci.yml / install.yml: run CI for PRs targeting integration/3.0, which
upstream's branch filters would otherwise skip
A standing draft PR from integration/3.0 into 3.0 gives a continuous CI
verdict on the combined stack; it must never be merged.
Everything here is fork-only and must never be cherry-picked upstream.
The Linux CI jobs fail while installing the package:
[Errno 122] Disk quota exceeded
Errno 122 is EDQUOT, hit while unpacking the CUDA torch stack that
`pip install -e .` resolves by default. torch alone is 554.6 MB and the
accompanying nvidia-* wheels add another ~2.4 GB, which no longer fits
the runner's quota. The failure occurs during "Installing build
dependencies", i.e. the isolated PEP 517 build environment is already
enough to exceed it on its own.
The trigger is a runner image change, not a code change:
* upstream 11_bernhard_dev, 2026-10-20, Image: ubuntu-24.04 -> success
* same workflows, 2026-10-23, Image: ubuntu-26.04 -> failure
Both resolve an identical wheel set (torch 2.14.1, 554.6 MB). The
`ubuntu-latest` label began migrating to Ubuntu 26 on 2026-10-19
(actions/runner-images#14748), between those two runs. Both workflows
already carry "Free up disk space" steps, so these jobs were running
close to the limit before the migration.
Fix: install CPU-only torch from download.pytorch.org, then build with
--no-build-isolation so the build reuses it instead of resolving torch
a second time. --no-build-isolation needs the build dependencies present
up front, hence the explicit setuptools/wheel/numpy install; the
`setuptools<81` bound matches the pin already in install_requires.
This loses no coverage: BUILD_CUDA_EXT keys off CUDA_HOME/ROCM_HOME,
which is unset on these runners, so the CUDA sources were never compiled
here. tests/smoke_tests/Dockerfile already installs torch this way.
macOS keeps the plain `pip install -e .` unchanged: its PyPI wheels are
already CPU-only, and leaving it alone keeps the default PEP 517
build-isolation path under test somewhere in the matrix.
Gate 1 — dispatched workflow runsThis commit changes
The negative run is the one that matters; a detector that only ever passes is not a detector. It Both runs also executed the Dispatching from |
Gate 2 round 1: FAILED -- 13 findings, all now addressedAn adversarial review (fresh context, no authoring history) was run against commit Every finding is fixed in Critical -- guards that passed contaminated input
High / medium
What finding 12 changed about the argument for splitting the docThe PR originally justified the split as: moving a procedure out of the always-loaded core is safe So the criterion is now narrower and stated explicitly: a procedure may leave the core only if a Finding 1, honestly: only half solvedThe reviewer's second point was that a feature commit which invents a brand-new fork-only file The new fork-base registration invariant catches this once the file reaches the base commit -- I would rather leave that written down than close it with a check that looks authoritative and Re-verificationAll on
Gate 2 is being re-requested on this commit. |
Gate 1 re-run on
|
| check | result | time |
|---|---|---|
| pre-commit | pass | 32s |
| Unit Tests | pass | 4m32s |
| C Drive Tests | pass | 3m16s |
| Notebook Execution | pass | 5m8s |
| Smoke Train Test | pass | 14m34s |
| Smoke Performance Test | pass | 3m0s |
| 3.11 - macos-latest | pass | 2m36s |
| 3.11 - ubuntu-latest | pass | 3m14s |
| 3.12 - macos-latest | pass | 2m36s |
| 3.12 - ubuntu-latest | pass | 2m48s |
| 3.13 - ubuntu-latest | pass | 2m34s |
| 3.14 - ubuntu-latest | pass | 2m31s |
Because this commit changes a workflow, Gate 1 additionally requires that the workflow was run,
in both directions:
- run 40300687871 -- production
refs (origin/integration/3.0,origin/3.0):verifysuccess,self-testsuccess - run 40300911350 -- pointed at
a ref carrying twochore(fork):commits:verifyfailure with the annotation
2 chore(fork): commits on the stack, expected 1 Fix: fold fork-only changes into the base commit (section 3.3), while the advisoryself-testjob stayed green
The second run is the one that carries the proof: the detector rejects a malformed stack, emits a
correct ::error annotation with the fix hint, and the two jobs fail independently of each other.
Gate 2, round 2 — responseThank you; this round was as useful as the first. Eight findings, all accepted, all fixed in The pattern across both rounds is worth naming: every critical finding was a guard that printed 1. (critical) The workflow reader mis-parsed YAML and reported PASS
The reader now matches the Covered by 2. (critical)
|
Gate 1 evidence on
|
| check | result |
|---|---|
| pre-commit | pass (29 s) |
| Unit Tests | pass (4 m 17 s) |
| C Drive Tests | pass (3 m 38 s) |
| Notebook Execution | pass (4 m 38 s) |
| Smoke Performance Test | pass (3 m 41 s) |
| Smoke Train Test | pass (14 m 36 s) |
| 3.11 / 3.12 / 3.13 / 3.14 - ubuntu-latest | pass |
| 3.11 / 3.12 - macos-latest | pass |
The workflow this commit changes was actually run
A green required-check board says nothing about fork-only-view.yml itself, so it was dispatched
in both directions on the amended commit:
| run | input | job name as rendered | result |
|---|---|---|---|
| 40308651090 | production refs | ADVISORY -- candidate detector, not authoritative |
success |
| 40308673608 | stack=origin/chore/forkctl |
ADVISORY -- candidate detector, not authoritative |
failure, as required |
The negative run failed for exactly the intended reason:
2 chore(fork): commits on the stack, expected 1
Fix: fold fork-only changes into the base commit (section 3.3)
The rendered job name is also the Gate 2 round-2 finding 4 fix, visible in the Actions list rather
than only in the logs: a dispatched run can no longer present itself as the authoritative check.
The advisory forkctl self-test (candidate ref) job passed in both runs, so the 88-test suite is
proven green on ubuntu-latest and is not Windows-dependent.
Note on scope: until this commit is folded into the fork base, integration/3.0 carries neither
the workflow nor forkctl, so push and schedule runs cannot exist yet. workflow_dispatch
is the only available evidence, which is why it is given in both directions.
Gate 2 — round 3 responseSeven findings, all fixed. Suite: 67 → 88 → 105 tests. 1 (critical) — the workflow reader certified a branch the filter excludes
Fix. 2 (critical) — Gate 3 certified an unrelated commit
Fix. 3 (critical) — registered fork-only files did not have to existDeleting Fix. New 4 (high) — a feature commit could repair a malformed fork marker undetected
Fix. New 5 (high) — Ctrl-C mid-cherry-pick left a conflicted detached HEAD, reported as restoredBoth restore attempts failed (a conflicted index blocks Fix. 6 (medium) — the "byte-identical" claim was not true
Fix. Both use 7 (low) — dangling section referencesThe core doc's command table cited §6.2/§6.3, which no longer exist after the split. Fix. The column is now "Where it is written up", citing §7 and the italicised Re-verified after the fixes: 105/105 tests pass; |
The fork process was described entirely in prose plus inline shell. That had two costs. Token cost. `.github/copilot-instructions.md` is loaded automatically in every session in this repository, including the overwhelming majority that touch the simulator and never touch the fork process. It was 56 KB, roughly 14k tokens of standing overhead per session. Correctness cost. The same guard was retyped at several call sites and the copies drifted. The fork-only file list existed in six places. More seriously, the `AGENTS.md` fork-block guard never fired at all: it matched changed lines *containing* `fork:begin`/`fork:end`, but an edit between the markers produces changed lines carrying no marker, so fork-only prose could be edited inside a feature commit and pass every check. This commit moves the mechanics into code and the rationale out of the always-loaded file. `.github/fork/forkctl.py` is the single implementation of every mechanical check: doctor, check-commit, check-stack, check-view, mirror, status, delete-mirror, fork-only. It is stdlib-only and spawns no shell, so the ways a shell guard dies quietly have no analogue — `grep -c` exiting 1 on no match, `git log | grep -q` returning 141 under pipefail, a pipeline's status being swallowed. Those footguns previously needed paragraphs of warning in the docs. `.github/fork/test_forkctl.py` covers each guard in both directions against throwaway git repositories, including the fork-block hole above, which is now caught by comparing the block's content at the merge-base and at HEAD. `.github/fork/PROCESS.md` takes the deep procedures — the standing draft PRs, changing fork-only content, the adversarial-review checklist, the mirror PR workflow, syncing and releasing. They are rare, they must not be improvised, and they do not belong in a file that is parsed on every unrelated task. `.github/copilot-instructions.md` keeps the rules an agent needs *before* acting and signposts the rest; it drops from 56 KB to 23 KB. Splitting is only safe because the mechanics are now enforced: an agent that skips the reference still cannot push a contaminated commit, because forkctl fails closed. `.github/workflows/fork-only-view.yml` loses its copy of the stack invariants and the standing-PR contract and calls `forkctl check-view` instead. Its checkout now follows the triggering ref rather than a hard-coded branch, so the workflow is exercisable from the branch that changes it, and the refs it analyses are passed explicitly. It also runs the forkctl test suite. `CONTRIBUTING.md` and the PR template stop restating the fork-only list and the guard commands and point at forkctl.
Gate 2 — round 4 (delta review) responseAll three findings are fixed in A (critical) — duplicate top-level
|
b3fcee3 to
eddee28
Compare
|
Applied by fixup into the fork base per the fork process (section 3.3), not merged.
Deviation to record: the section 3.3 recipe calls for |
What
Move every mechanical fork-process guard out of
.github/copilot-instructions.mdinto a testedstdlib-only CLI at
.github/fork/forkctl.py, and split the remaining prose into a compactalways-loaded core plus an on-demand reference at
.github/fork/PROCESS.md.Why
.github/copilot-instructions.mdis loaded into the context of every agent session in thisrepository, whether or not the task has anything to do with the fork process. At 55 935 B it cost
roughly 14k tokens per session before any work began, and most of that budget was spent on
hand-typed bash guards that an agent had to re-derive, re-type and re-reason about each time.
That was expensive, and it was also fragile. The guards were duplicated in six places
(§3.3, §4.5, §4.8 Gate 3, §6, §7,
fork-only-view.yml), so a change to the fork-only file listmeant six edits, and the file itself had to spend paragraphs warning about shell footguns it had
hit in practice —
grep -cexiting 1 on no match,git log | grep -qreturning 141 underpipefailand silently disabling a guard. Those failure modes do not exist outside a shell.Two measurements drove the shape of this PR. Extracting the bash alone shrank the core file by only
~4%, because the bulk was prose, not commands — hence the split. And while porting the guards to
Python, the test suite found a real hole in the existing §4.5 check (see Risk / notes).
The core file is now 35 902 B / 652 lines, down from 55 935 B / 1021 lines — 35.8% smaller, on
every session. The detail is not lost, it is one
viewaway inPROCESS.md, and §0.2 is a tablethat says exactly when to open it.
Splitting a safety-critical document is only safe if skipping the half you did not load cannot
hurt you. The criterion, made explicit after Gate 2 round 1: a procedure may leave the core only
if a fails-closed command can refuse the wrong outcome. Every destructive procedure in
PROCESS.mdis gated by aforkctlcommand, so an agent that never opens the reference stillcannot push a contaminated commit, fold a malformed stack, or delete a mirror branch out from under
an open upstream PR. Rules whose failure mode is outside git — the
update-smoke-golden.ymldispatch hazard, the merge-button rules, upstream-facing judgement — stay in the core, because no
guard can exist for them.
forkctldeliberately depends on neithermakenorjust, so it is unaffected by #8.doctorcheck-commit [--chore]check-stack [--print-base] [--github-output]check-viewfork-only-view.ymlbranch-currency + standing-PR contractmirror <slug> --sha|--subjectstatusgit cherryaudit + §7 divergence reportdelete-mirror <slug>fork-only [-v]How tested
The full required suite ran in CI on the final commit — all 12 checks green. This is a
fork-only change to documentation, one standalone Python tool and one fork-only workflow, so no
.c/.h, no notebooks, no sim logic and nothing underpufferlib/is touched; the suites beloware therefore regression evidence rather than coverage of the change itself.
pre-commit run --all-files— pass in CI (31 s). This coversruff-format,trailing-whitespace,end-of-file-fixerandclang-formaton the whole tree.make rebuild— covered by C Drive Tests (pass, 3 m 36 s); no.c/.hchangedmake test-unit/make test-eval— Unit Tests pass (3 m 40 s)make test-c— C Drive Tests pass (3 m 36 s)make test-notebooks— Notebook Execution pass (5 m 33 s)make test-docker-smoke— Smoke Train Test pass (7 m 26 s)make -C tests/drive perf— Smoke Performance Test pass (3 m 15 s)make test— covered by the four suites above, plus the install matrix(3.11/3.12 macOS, 3.11–3.14 Ubuntu) all pass
The local environment here is Windows and
.venvin this checkout is a Linux venv, somakeandpre-commitcould not be invoked natively. Before pushing, the Python hooks were run directlyagainst the repo's own
ruff.tomland the whitespace hooks checked programmatically; CI has sinceconfirmed that approximation was correct.
The tool's own suite, which is the test that matters for this diff:
It also runs as an advisory self-test job inside
fork-only-view.yml, so it is additionally provengreen on
ubuntu-latestand is not Windows-dependent.Validated against this real repository, not only against fixtures:
The rewritten workflow was dispatched in both directions on the final commit:
refs, both jobs success
ref carrying two
chore(fork):commits,verifyfailed with::error 2 chore(fork): commits on the stack, expected 1, while the advisoryself-testjobstayed green
The negative run is the one that matters: it proves the detector detects, and that the two jobs are
independent.
New behaviour is covered by a new test / the fix has a test that fails without it:
105 tests in
.github/fork/test_forkctl.py. Every guard is asserted in both directions — itpasses a well-formed input and fails a malformed one — in
TestCheckStack,TestCheckCommit,TestMirror,TestMirrorDrift,TestReviewRegressions,TestRoundTwoRegressions,TestFailClosed,TestWorkflowParserandTestPureHelpers. The two bugs described under Risk / notes each have a dedicated regressiontest:
test_fails_when_content_inside_the_fork_block_is_edited(the hole) andtest_substring_lookalikes_are_not_fork_only(the over-fire). Every Gate 2 finding below has onetoo.
Stack hygiene
forkctl check-commitpasses (--choreon a fork-only branch)integration/3.0Merge gate
FAILED, 8 findings (4 critical); round 3 FAILED, 7 findings (3 critical, 2 high,
2 medium, 1 low). All 28 addressed, each with a regression test, in
6957b2b82and308033ed7(see the three Gate 2 comments below). Suite grew 67 → 88 → 105 tests.Round 4, scoped to the round-3 deltas, pending.
chore(fork)Gate 3 result:
Fork-only infrastructure cannot be upstream-ready by definition. The §4.5 fork-only file and hunk
checks are likewise exempt and are expected to "trip" — touching those files is the entire point of
this branch. Every other rule still holds, in particular "exactly one commit", asserted above.
Upstream disposition
Emerge-Lab/PufferDriverequested nowchore(fork):change, Gate 3 exemptRisk / notes
This PR must not be merged. Merging would put a second
chore(fork):commit on the stack, andforkctl check-stackasserts there is exactly one, so every future sync would abort until someoneunpicked it by hand. It is applied by fixup into the base commit instead
(
.github/fork/PROCESS.md-> Changing fork-only content) and ends closed, not merged. Thatfold force-pushes the default branch, so it is a human decision and is deliberately left undone
here.
A real guard hole, found by porting the checks to Python. The old §4.5 check was:
It only fires when a changed line itself contains a marker. Editing the text between
<!-- fork:begin -->and<!-- fork:end -->produces+/-lines with no marker on them, so theguard never fired and fork-only content could reach a mirror diff.
forkctlnow extracts the wholeblock at the merge-base and at
HEADand compares them, so any edit inside the block is caughtregardless of which lines moved.
The same checks also over-fired. The shell guards matched substrings, so a legitimate file
named e.g.
docs/sync-mirror-notes.mdortools/pull_request_template.pywas reported asfork-only contamination. Path matching is now exact paths plus explicit prefixes.
Event 1 drift detection changed meaning, deliberately. §6.2 previously tested
git merge-base --is-ancestor mirror/<slug>^ upstream/3.0, which cannot fire when upstreamfast-forwards — i.e. essentially never, despite the prose promising to catch "the PR starts showing
conflicts".
forkctl statusnow usesgit merge-tree --write-treeto test whether the mirror stillmerges onto the upstream tip, which is the condition the prose actually describes. An upstream
advance that still merges cleanly needs no action and is now correctly reported as nothing.
merge-tree --write-treeneeds git >= 2.38.Behaviour preserved on purpose: §4.5's hunk check stays scoped to the four shared files while
Gate 3's marker check scans the whole diff. That asymmetry is intentional — nothing in a mirror
diff should mention the fork at all — and it is asserted by tests rather than left to a reader.
fork-only-view.ymldrops ~70 lines of bash andjqfor two jobs.verifyis theauthoritative detector: on
pushandscheduleit checks outintegration/3.0and hardcodesorigin/integration/3.0/origin/3.0, so the ref under analysis can never supply the code thatpolices it, nor redirect the verdict at something else. Only a human-driven
workflow_dispatchmaypoint it at a candidate ref, and the step summary then says so in a table.
self-testis a second,advisory job that runs the unit suite from the triggering ref — it proves the tool is sound,
never that the repository state is. Still
contents: readonly.Two adversarial review rounds found 21 real guard holes, 13 of them critical. All are fixed and each has a regression test;
the full itemisation is in the Gate 2 comment below. The headline ones:
git diff --name-onlyapplies rename detection, so renaming a fork-only file away passed every check (
--no-renamesnow,plus a new "every file in the fork base is registered as fork-only" invariant); moving the branch
filter from
pull_request:topush:left the marker text untouched (there is now a structuralreader of the
on:block);git()stripped whitespace, so a leading-space subject defeated thechore(fork):andfixup!prefix checks;doctor/status/check-viewexited 0 whenghwasmissing or erroring; and
delete-mirrorcould read another fork's PR with the same branch name.Every one of those was a false PASS, which is the only failure mode that actually matters for a
tool whose whole purpose is to let the prose shrink.
One finding is only half solved, deliberately. A feature commit that invents a brand-new
fork-only file still passes
check-commit, because the fork-only list is hardcoded. The newregistration invariant catches it once the file reaches the base commit —
check-stackthen failsuntil the path is added to
FORK_ONLY_FILES— but not at feature-commit time. "This new file isfork-only" is a claim about intent that no diff inspection can derive; Gate 2 review is the control
for it. I would rather state that than close it with a check that looks authoritative and is not.
Docs updated in step:
CONTRIBUTING.mdand.github/pull_request_template.mdno longer restatethe fork-only file list or the guard commands — they point at
forkctl, so the list exists inexactly one place.