Skip to content

[do-not-mirror][do-not-merge] chore(fork): detect fork-only view drift instead of publishing it - #6

Closed
jdsika wants to merge 3 commits into
integration/3.0from
chore/fork-only-view-drift
Closed

jdsika wants to merge 3 commits into
integration/3.0from
chore/fork-only-view-drift

Conversation

@jdsika

@jdsika jdsika commented Oct 23, 2026

Copy link
Copy Markdown

What

Turn fork-only-view.yml into a drift detector with no write access, and publish
fork-only/3.0 by hand as the last step of the fold (3.3) and rebase (7) procedures.

Why

The first real run of the workflow — triggered by folding #5 — failed:

! [remote rejected] b9b276a3d -> fork-only/3.0 (refusing to allow a GitHub App to
  create or update workflow `.github/workflows/ci.yml` without `workflows` permission)

GitHub blocks GITHUB_TOKEN from pushing any commit that creates or updates a file under
.github/workflows/
. It is a platform rule, and no permissions: block lifts it — confirmed
against GitHub's own docs on choosing GitHub App permissions. The fork base commit is made almost
entirely of workflow files, so the publish-by-workflow design could never have worked.

Automating it anyway would mean storing a PAT or GitHub App token with workflows write in a
public repository
. That is a far worse trade than one extra line in two procedures that are
already manual and already force-push with human credentials.

So the roles swap: humans publish, the workflow detects. When the view is missing or stale the
run fails and prints the exact git push that fixes it. Permissions drop from
contents: write + pull-requests: write to contents: read + pull-requests: read.

How tested

  • YAML validity -> YAML OK, 4 steps, permissions: {contents: read, pull-requests: read}
  • The failure itself is the evidence: run
    39986186116 resolved the
    fork base correctly (all 8 guards passed) and failed only on the push — so the guard
    logic is already proven in production and is carried over unchanged
  • Platform restriction confirmed against GitHub documentation, not assumed from the error text
  • git diff --check clean; LF-only, single trailing newline on all three files
  • One commit; Conventional Commits subject

Not run, and why: pre-commit and the make suites need a POSIX shell and .venv/bin/python
(4.3); authored on Windows, and the CI pre-commit job covers the former. No .c/.h, Python,
notebook, dynamics, reward, observation or training-loop code is touched.

New behaviour covered by a test: n/a — fork-only CI/doc infrastructure. The detector's own
guards are its test, and they are already proven green by the run linked above.

Stack hygiene

  • exactly one commit
  • Conventional Commits subject, rebased on current integration/3.0
  • no fork-only file in the diff — exempt: chore(fork)
  • no fork-only hunk in a shared file — exempt: chore(fork)

Merge gate

  • Gate 1 — CI green on the final commit
  • Gate 2 — adversarial review by a separate agent with no authoring context
  • Gate 3 — exempt: chore(fork)

Upstream disposition

  • Mirror PR requested now
  • Contributable, but held
  • Never upstream — fork-only chore(fork): change, Gate 3 exempt

Risk / notes

  • The view branch is now only as fresh as the last manual push. Mitigated by making the
    detector fail — not warn — when it is missing or stale, so the standing integration PR and the
    Actions tab both show red until someone republishes.
  • The fork base SHA changes on every sync rebase, so the republish line is placed inside both
    procedures that rewrite it, rather than left as advice somewhere else.
  • Process note: the publishing design cleared Gate 1 (12/12) and Gate 2 (an adversarial review
    that found 2 blockers and 6 should-fix issues, all handled) and was still wrong. Neither gate can
    see a server-side push restriction; only executing it could. Worth remembering before trusting a
    green gate set on anything that has never actually run.

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.
@jdsika jdsika added do-not-mirror Fork-only change; must never be sent to Emerge-Lab/PufferDrive do-not-merge Never use the merge button; applied by fixup (3.3) or never merged at all labels Oct 23, 2026
@jdsika
jdsika force-pushed the chore/fork-only-view-drift branch 3 times, most recently from f6f4fca to 625313e Compare October 23, 2026 16:54
@jdsika

jdsika commented Oct 23, 2026

Copy link
Copy Markdown
Author

Gate 2 — adversarial review record

Reviewed with fresh context, no authoring state. Verdict: FAIL on first pass — 3 blockers, 3
should-fix. All dispositioned below; the branch has been amended accordingly (625313e55).

Blockers

B1 — two pushes, one invariant. The fold published integration/3.0 and fork-only/3.0 as
separate pushes. A rejected lease on the first does not stop the second, so a losing race leaves the
view pointing at a base that is no longer on the stack — and the detector red for whoever looks
next. Fixed: single git push --atomic in both section 3.3 and section 7; either both refs
move or neither does.

B2 — the guard that fails open. git log … | grep -q '^fixup!' under set -o pipefail
returns 141, not 1, once the log is long enough that grep -q exits on its first match and
git takes SIGPIPE. The surrounding if then reads "no match" and the invariant is
skipped silently. Reproduced locally: large log → exit 141, small log → exit 0, i.e. it passes
today and degrades as the stack grows — the worst possible failure shape for a guard.
Fixed: every such check is now pipe-free (git log --grep='^fixup!' --max-count=1 --format=%H
and git rev-list --min-parents=2 --max-count=1, tested for non-empty output), in the workflow
and in the documented section 3.3 / section 7 procedures, which carried the same trap.
grep -c is left alone — it reads to EOF and cannot be SIGPIPEd.

B3 — still no execution path. The rewritten workflow was, once again, about to be folded on the
strength of a green required-check board that never runs it. That is precisely how #5 cleared Gate 1
and Gate 2 while being impossible. Fixed twice over: the workflow was executed in all five
states it claims to distinguish (below), and section 4.8 Gate 1 now requires that any commit
adding or changing a workflow links runs proving it, because the required checks validate the
repository, not your workflow.

Should-fix

  • S1 — tampering with the view itself was invisible. Added a push trigger on
    fork-only/3.0 (plus a daily schedule), so a hand-edit of the view is caught, not just drift of
    the stack.
  • S2 — the PR contract was unverified. The detector now checks that exactly one
    fork-only/3.0 PR is open and that it keeps base 3.0 + draft + do-not-merge, and fails
    loudly if one was ever merged (which would mean the mirror is contaminated).
  • S3 — stale prose. Section 5 still described the view as "refreshed automatically". Corrected;
    the section 2 bullet now states the GITHUB_TOKEN workflow-push restriction as the reason.

Execution evidence — all five states, dispatched on this branch

# State Expected Run Result
1 fork-only/3.0 missing fail + exact repair command 39989767189 ✅ does not exist. Publish it with: git push --force origin b9b276a3d…:refs/heads/fork-only/3.0
2 view stale (wrong SHA) fail + both SHAs + repair 39989921965 ✅ is stale (74b44c2f3…); the fork base is b9b276a3d…
3 view equal, no standing PR pass + warning 39990064592 ✅ ::warning::no standing fork-only/3.0 PR is open
4 view equal, PR intact pass 39990316870 ✅ Standing view PR is intact.
5 PR contract broken (label removed) fail, naming the breach 39990424613 ✅ #7 base=3.0 draft=true labels=

State 4 is also the first execution of the jq PR-contract filter, which was previously untested
(jq is unavailable on the authoring machine). State 5 proves it fails in the right direction —
the label was removed deliberately and restored immediately afterwards.

The standing view PR is now open as #7 — draft, base 3.0, do-not-merge alone. Verified
that the section 4.1 in-flight-process query (--label do-not-mirror --label do-not-merge)
returns this PR only and not #7, which is the whole point of the one-label/two-label
discriminator.

Probes that found nothing

  • Fork-only hunt patterns. All three guards (4.5, Gate 3, section 6) list fork-only-view;
    no fork-only filename is missing from any of them.
  • Lease form. Both procedures use the explicit --force-with-lease=<ref>:<sha> form against a
    SHA captured before the rewrite; neither relies on the bare form.
  • Editor-free. No interactive rebase -i/--continue without GIT_SEQUENCE_EDITOR/
    GIT_EDITOR anywhere in the changed procedures.
  • Permissions. Workflow is contents: read + pull-requests: read; it requests nothing it
    does not use, and cannot write even if it tried.
  • Determinism of the base derivation. git rev-list origin/3.0..integration/3.0 | tail -1
    plus the chore(fork) subject assertion — the same derivation the documented procedures use, so
    the detector and the fold cannot disagree about what the base is.
  • Markdown integrity. Code fences balanced in both edited documents; no CRLF, single trailing
    newline, git diff --check clean.

Accepted, not fixed

  • Gate 3 still has no machine attestation. It remains a procedure a human or agent runs. Making
    it a check would mean giving CI a cherry-pick onto upstream on every PR; the cost is not yet
    justified by the volume. Recorded as a known gap rather than silently tolerated.
  • sync-mirror.yml's non-fast-forward and issue paths, and update-smoke-golden.yml, have
    never executed.
    Out of scope here, but the same reasoning as B3 applies to them and they should
    get the same treatment before anyone relies on them.

The first real run of fork-only-view.yml proved the publishing design
cannot work: GitHub blocks GITHUB_TOKEN from pushing any commit that
creates or updates a file under .github/workflows/, and the fork base
commit is made almost entirely of such files. No permissions: block
lifts that restriction.

Automating it anyway would mean storing a PAT or GitHub App token with
workflows write in a public repository. Instead the branch is published
by hand as the last step of the two procedures that already force-push
with human credentials, and the workflow becomes a detector that fails
loudly when the view is missing or stale, printing the exact command to
fix it. It now needs no write access at all.
@jdsika
jdsika force-pushed the chore/fork-only-view-drift branch from 625313e to 670efb8 Compare October 23, 2026 17:02
@jdsika

jdsika commented Oct 23, 2026

Copy link
Copy Markdown
Author

Follow-up finding, caught by executing the procedure rather than reading it

While running the section 3.3 fold for this very PR, the fold aborted on a mismatch: section 3.3's
push block had not been converted to the atomic form
— only section 7's had. Section 3.3 is the
procedure that creates a new fork base, so it is the one place where the view is guaranteed to go
stale the instant integration/3.0 lands. Folding this PR with that text in it would have left
the repository in exactly the state the new detector is built to scream about, immediately, by
construction.

Fixed in 670efb8c3: both procedures now push integration/3.0 and fork-only/3.0 with a
single git push --atomic, under one explicit lease.

This is the second time in this PR that the difference between stating a procedure and running
it produced a defect that no review and no green check board caught — first the GITHUB_TOKEN
workflow-push restriction in #5, now this. The Gate 1 rule added here ("a commit that adds or
changes a workflow must link runs proving it") covers the first; this one argues the same for
procedures, and the fold is already that test, provided it is actually performed from the document
rather than from memory.

Gate 1 is re-running on 670efb8c3; the previous 12/12 was on 625313e55 and does not transfer.

@jdsika

jdsika commented Oct 23, 2026

Copy link
Copy Markdown
Author

Folded into the base commit — closing unmerged (section 3.3)

This PR is closed, not merged, exactly as a chore(fork): PR must be: merging it would put a
second chore(fork) commit on the stack and every future sync would abort on the section 7
invariant.

before after
fork base b9b276a3d aee11355f
integration/3.0 fad5d9586 b3fcee357
fork-only/3.0 b9b276a3d aee11355f

Gates: Gate 1 12/12 on 670efb8c3 (the final commit; the earlier 12/12 on 625313e55 was
discarded when the commit was amended). Gate 2 recorded above. Gate 3 exempt.

Invariants verified before the push

Check Result
exactly one chore(fork) commit PASS
no fixup! left on the stack PASS
oldest stack commit is the fork base PASS
stack is linear (no merge commits) PASS
stack depth = 2 PASS
tree identical to the CI-green tree 9ec7379fe PASS
ci-fix patch-id 1315dfd78… preserved PASS
fork base's parent is still origin/3.0 PASS

Tree-identity is what carries Gate 1 across the fold: the rewritten integration/3.0 has byte-for-byte the tree
that passed 12/12, so the verdict transfers rather than being re-asserted. The patch-id check keeps
the section 6.2 linkage to the open upstream PR Emerge-Lab#602 valid across the rewrite,
since the rebase changed that commit's SHA but not its content.

The atomic push, exercised for the first time

+ b9b276a3d...aee11355f aee11355f -> fork-only/3.0 (forced update)
+ fad5d9586...b3fcee357 integration/3.0 -> integration/3.0 (forced update)

One git push --atomic under one explicit lease, so the view can never be published against a
base that is not on the stack. Verified afterwards that the view equals the re-derived fork base.

The detector, proven on the real event

The push fired the new workflow on both branches, from the default branch, with no dispatch:

That second run is S1 working live: tampering with the view itself is now caught, not only drift of
the stack. Together with the five dispatched states in the Gate 2 comment, every branch of this
workflow has now actually executed.

The standing view PR is #7 (draft, base 3.0, do-not-merge alone).

What this PR changed in the process

  • Gate 1 now requires that a commit adding or changing a workflow links runs proving it ran.
  • Both git log | grep -q invariant guards were fixed; under pipefail they returned 141 and
    failed open once the log grew past a pipe buffer.
  • Section 3.3 and section 7 publish both refs atomically.
  • The fork-only view is a read-only detector, never a publisher — GITHUB_TOKEN cannot push
    commits touching .github/workflows/, and the fork base is almost entirely workflow files.

@jdsika jdsika closed this Oct 23, 2026
@jdsika
jdsika deleted the chore/fork-only-view-drift branch October 23, 2026 17:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

do-not-merge Never use the merge button; applied by fixup (3.3) or never merged at all do-not-mirror Fork-only change; must never be sent to Emerge-Lab/PufferDrive

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant