diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 4ca55495087b2781d111783a86aa58c9275d909c..55a6077ddedf83f8b5b7f0424fa03a57cbf6b867 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -2,7 +2,6 @@ stages:
   - dist
   - test
   - review
-  - docs
   - deploy
 
 # have to specify this so that all jobs execute for all commits
@@ -28,44 +27,42 @@ gwinc/base:
       cat <<EOF > Dockerfile
       FROM igwn/base:buster
       RUN apt-get update -qq
-      RUN apt-get -y install --no-install-recommends git gitlab-cli python3 python3-yaml python3-scipy python3-matplotlib python3-ipython lalsimulation-python3 python3-pypdf2 python3-h5py
+      RUN apt-get -y install --no-install-recommends git python3-gitlab python3 python3-yaml python3-scipy python3-matplotlib python3-ipython python3-lalsimulation python3-pypdf2 python3-h5py
       RUN git clone https://gitlab-ci-token:ci_token@git.ligo.org/gwinc/inspiral_range.git
       EOF
     - docker build -t $IMAGE_TAG .
     - docker push $IMAGE_TAG
 
-# validate that the noises haven't changed relative to the reference
-# (the reference itself could have been updated, though, see
-# check_approval job)
-validate_noise:
+# create plots for the canonical IFOs
+generate_budgets:
   stage: test
   image: $CI_REGISTRY_IMAGE/gwinc/base:$CI_COMMIT_REF_NAME
   script:
-    - rm -f gwinc_test_report.pdf
-    - export MPLBACKEND=agg
-    - python3 -m gwinc.test -r gwinc_test_report.pdf
+      - mkdir -p ifo
+      - export PYTHONPATH=/inspiral_range
+      - for ifo in $(python3 -c "import gwinc; print(' '.join(gwinc.IFOS))"); do
+      -     python3 -m gwinc $ifo -s ifo/$ifo.png
+      -     python3 -m gwinc $ifo -s ifo/$ifo.h5
+      - done
+      - python3 -m gwinc.ifo -s ifo/all_compare.png
   artifacts:
-    when: on_failure
+    when: always
     paths:
-      - gwinc_test_report.pdf
-    expose_as: 'noise validation failure report'
+      - ifo
 
-# this is a special job intended to run only for merge requests where
-# the test reference hash file has been updated, indicating that there
-# has been a noise change.  if the merge request has not yet been
-# approved, generate a report of noise changes relative to the target
-# branch and present that to the reviewers.  if the merge request is
-# approved, re-run this job, which will succeed if the MR is approved.
-check_approval:
+# this is a special job intended to run only for merge requests.
+# budgets are compared against those from the target branch.  if the
+# merge request has not yet been approved and noise changes are found,
+# the job will fail.  once the merge request is approved the job can
+# be re-run, at which point the pipeline should succeed allowing the
+# merge to be merged.
+noise_change_approval:
   stage: review
   rules:
     # - if: '$CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "master"'
     - if: $CI_MERGE_REQUEST_ID
-      changes:
-        - gwinc/test/ref_hash
   image: $CI_REGISTRY_IMAGE/gwinc/base:$CI_COMMIT_REF_NAME
   script:
-    - echo "NOISE REFERENCE CHANGE, checking approval..."
     - |
       cat <<EOF > check_approved.py
       import sys
@@ -85,42 +82,31 @@ check_approval:
     - if [[ $approved != True ]] ; then
     -     old_hash=$(git cat-file -p origin/$CI_MERGE_REQUEST_TARGET_BRANCH_NAME:gwinc/test/ref_hash)
     -     if ! python3 -m gwinc.test --git-ref $old_hash -r gwinc_test_report.pdf ; then
-    -         echo "APPROVAL REQUIRED TO MERGE THIS BRANCH."
+    -         echo "NOISE CHANGES RELATIVE TO $CI_MERGE_REQUEST_TARGET_BRANCH_NAME."
+    -         echo "Approval required to merge this branch."
     -         /bin/false
     -     else
-    -         echo "Reference update did not cause appreciable noise change."
+    -         echo "No noise changes detected."
     -     fi
     - else
-    -     echo "Merge request approved, reference change accepted."
+    -     echo "Merge request approved, noise change accepted."
     - fi
   artifacts:
     when: on_failure
     paths:
       - gwinc_test_report.pdf
-    expose_as: 'noise changes relative to target branch head APPROVAL REQUIRED TO MERGE'
+    expose_as: 'noise changes relative to target branch'
 
-# create plots for the canonical IFOs
-ifo:
-  stage: docs
-  image: $CI_REGISTRY_IMAGE/gwinc/base:$CI_COMMIT_REF_NAME
-  script:
-      - mkdir -p ifo
-      - export PYTHONPATH=/inspiral_range
-      - for ifo in $(python3 -c "import gwinc; print(' '.join(gwinc.IFOS))"); do
-      -     python3 -m gwinc $ifo -s ifo/$ifo.png
-      -     python3 -m gwinc $ifo -s ifo/$ifo.h5
-      - done
-      - python3 -m gwinc.ifo -s ifo/all_compare.png
-  artifacts:
-    when: always
-    paths:
-      - ifo
-
-# generate the html doc web pages
-html:
-  stage: docs
+# generate the html doc web pages.  the "pages" job has special
+# meaning, as it's "public" artifact becomes the directory served
+# through gitlab static pages
+pages:
+  stage: deploy
   only:
     - master
+  needs:
+    - job: generate_budgets
+      artifacts: true
   image: $CI_REGISTRY_IMAGE/gwinc/base:$CI_COMMIT_REF_NAME
   script:
     - rm -rf public
@@ -129,24 +115,6 @@ html:
     - make html
     - cd ..
     - mv ./build/sphinx/html public
-  artifacts:
-    when: always
-    paths:
-      - public
-
-# the "pages" job has special meaning, as it's "public" artifact
-# becomes the directory served through gitlab static pages
-pages:
-  stage: deploy
-  only:
-    - master
-  needs:
-    - job: ifo
-      artifacts: true
-    - job: html
-      artifacts: true
-  image: $CI_REGISTRY_IMAGE/gwinc/base:$CI_COMMIT_REF_NAME
-  script:
     - mv ifo public/
   artifacts:
     when: always
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index f7e67fb6d0fba7499fff7a17c080fb2e86cd6fac..815596463f99a0e6f10f766969cf7eb1a5086de3 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -16,11 +16,9 @@ as possible, and provide a complete, descriptive log of the changes
 legibility.
 
 `pygwinc` comes with a validation command that can compare budgets
-between different versions of the code and produce a graphical report.
-`pygwinc` stores a reference to the git commit that is considered the
-latest good reference for the budget calculation functions, and the
-validation is run against that reference by default.  The command can
-be run with:
+from the current code against those produced from different versions
+in git (by default it compares against the current HEAD).  The command
+can be run with:
 ```shell
 $ python3 -m gwinc.test
 ```
@@ -30,37 +28,22 @@ arbitrary commit using the '--git-ref' option.  Traces for referenced
 commits are cached, which speeds up subsequent comparison runs
 significantly.
 
-Commits for code changes that modify budget calculations should be
-followed up updates to test reference. This can be done with the
-'--update-ref' option to the test command:
-```shell
-$ python3 -m gwinc.test --update-ref
-``` 
-If no specific reference is provided, a pointer to the last commit
-will be made.  *Note: reference updates need to be made in a separate
-commit after the commits that change the noise calculations* (git
-commits can't have foreknowledge of their own commit hash).
-
-Once you submit your merge request, an automated pipeline will run the
-test command to validate your changes.  If there are budgets
-differences but the reference has not been updated, the pipeline will
-fail and indicate that a reference update is required.  Updates to the
-reference will also cause a validation pipeline failure, but these
-failures can be resolved through reviewer approval of the changes (see
-below).
+Once you submit your merge request a special CI job will determine if
+there are budgets differences between your code and the master branch.
+If there are, explicit approval from reviewers will be required before
+your changes can be merged (see "approving noise" below).
 
 
-## For admins: approving noise curve changes
+## For reviewers: approving noise curve changes
 
 As discussed above, merge requests that generate noise changes will
-cause pipeline failures.  If the MR properly includes a reference
-update, then the failure should only be in the approval check.  A
-report will be generated comparing all noise changes against the
-target branch (usually 'master').  See the 'View exposed artifacts'
-menu item in the pipeline report.  Once you have reviewed the report
-and the code, and understand and accept the noise changes, click the
-'Approve' button in the MR.  Once sufficient approval has been given,
-re-run the `review:check_approval` pipeline job, which should now pick
-up that approval has been given and allow the pipeline to succeed.
-Once the pipeline succeeds the merge can be enacted.  Click the
-'Merge' button to finally merge the code.
+cause a pipeline failure in the `review:noise_change_approval` CI job.
+The job will generate a report comparing the new noise traces against
+those from master, which can be found under the 'View exposed
+artifacts' menu item in the pipeline report.  Once you have reviewed
+the report and the code, and understand and accept the noise changes,
+click the 'Approve' button in the MR.  Once sufficient approval has
+been given, `review:noise_change_approval` job can be re-run, which
+should now pick up that approval has been given and allow the pipeline
+to succeed.  Once the pipeline succeeds the merge request can be
+merged.  Click the 'Merge' button to finally merge the code.
diff --git a/gwinc/test/__main__.py b/gwinc/test/__main__.py
index 532030e46517ee9300be9edd839406d3ca8a7f3c..9f2dfa01d5d904359166875f6cd3ed37c57658d4 100644
--- a/gwinc/test/__main__.py
+++ b/gwinc/test/__main__.py
@@ -42,28 +42,10 @@ def git_ref_resolve_hash(git_ref):
         return subprocess.run(
             ['git', 'show', '-s', '--format=format:%H', git_ref],
             capture_output=True, universal_newlines=True,
+            check=True,
         ).stdout
-    except subprocess.CalledProcessError:
-        return None
-
-
-def write_ref_hash(ref_hash):
-    """Write ref hash to reference file
-
-    """
-    with open(test_path('ref_hash'), 'w') as f:
-        f.write('{}\n'.format(ref_hash))
-
-
-def load_ref_hash():
-    """Load the current reference git hash.
-
-    """
-    try:
-        with open(test_path('ref_hash')) as f:
-            return f.read().strip()
-    except IOError:
-        return None
+    except subprocess.CalledProcessError as e:
+        logging.error(e.stderr.split('\n')[0])
 
 
 def prune_cache_dir():
@@ -79,7 +61,7 @@ def prune_cache_dir():
     )[CACHE_LIMIT:]
     if not expired_paths:
         return
-    logging.info("pruning {} old caches...".format(len(expired_paths)))
+    logging.info("pruning {} old cache...".format(len(expired_paths)))
     for path in expired_paths:
         logging.debug("pruning {}...".format(path))
         shutil.rmtree(path)
@@ -248,12 +230,9 @@ def main():
         '--skip', '-k', metavar='NOISE', action='append',
         help='traces to skip in comparison (multiple may be specified)')
     parser.add_argument(
-        '--git-ref', '-g', metavar='HASH',
+        '--git-ref', '-g', metavar='HASH', default='HEAD',
         help='specify git ref to compare against')
     rgroup = parser.add_mutually_exclusive_group()
-    rgroup.add_argument(
-        '--update-ref', '-u', metavar='HASH', nargs='?', const='HEAD',
-        help="update the stored reference git hash to HASH (or 'HEAD' if not specified) and exit")
     rgroup.add_argument(
         '--plot', '-p', action='store_true',
         help='plot differences')
@@ -265,46 +244,11 @@ def main():
         help='specific ifos to test (default all)')
     args = parser.parse_args()
 
-    # get the current hash of HEAD
-    head_hash = git_ref_resolve_hash('HEAD')
-    if not head_hash:
-        logging.warning("could not determine git HEAD hash.")
-
-    # update the reference if specified
-    if args.update_ref:
-        if args.update_ref == 'HEAD':
-            if not head_hash:
-                sys.exit("Could not update reference to head.")
-            logging.info("updating reference to HEAD...")
-            ref_hash = head_hash
-        else:
-            ref_hash = git_ref_resolve_hash(args.update_ref)
-        logging.info("updating reference git hash: {}".format(ref_hash))
-        write_ref_hash(ref_hash)
-        sys.exit()
-
     # get the reference hash
-    if args.git_ref:
-        ref_hash = git_ref_resolve_hash(args.git_ref)
-    else:
-        ref_hash = load_ref_hash()
-        if not ref_hash:
-            pass
-        try:
-            with open(test_path('ref_hash')) as f:
-                ref_hash = f.read().strip()
-        except IOError:
-            logging.warning("could not open reference")
-            sys.exit("Unspecified reference git hash, could not run test.")
-
-    logging.info("head hash: {}".format(head_hash))
-    logging.info("ref  hash: {}".format(ref_hash))
-
-    # don't bother test if hashes match
-    if ref_hash == head_hash:
-        logging.info("HEAD matches reference, not bothering to calculate.")
-        logging.info("Use --git-ref to compare against an arbitrary git commit.")
-        sys.exit()
+    ref_hash = git_ref_resolve_hash(args.git_ref)
+    if not ref_hash:
+        sys.exit("Could not resolve reference, could not run test.")
+    logging.info("ref hash: {}".format(ref_hash))
 
     # load the cache
     cache_path = test_path('cache', ref_hash)