GAUNTLET AI // FIELD MANUAL
// CLASSIFIED — INTERNAL ONLY FILE : PRIMER-001 EST. READ 25 MIN OPERATOR LEVEL : ROOKIE → READY

The GitLab
/ Gauntlet
Primer.

You have used GitHub for years. Your fingers know git push like a heartbeat. Welcome to labs.gauntletai.com — same Git underneath, slightly different chrome on top. This manual gets you operational in about twenty-five minutes.

Ten numbered drills. Six interactive trainers. One final challenge with a personal debrief. Complete them in order, or jump in where you are weakest.

CLEARANCE  internal
ISSUED TO  new operators
SUPERSEDES  none
CHANNEL  #gitlab-help
DRILL 01
——

Get inside the
perimeter.

The instance lives at labs.gauntletai.com. Sign in with the username and password issued to you when your account was provisioned. Once you are in, the Dashboard is your home base: think of it as the GitHub homepage, rearranged.

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
DRILL 02
——

The translation
layer.

GitLab and GitHub are 99% identical on the command line. The differences are almost all vocabulary. Tap each card to reveal the GitLab equivalent and a one-line explanation. Flip them all to clear the drill.

DRILL // click any card to translate ↹ TAP TO FLIP
FLIPPED
0 / 0
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
DRILL 03
——

Spin up a
new project.

A “project” in GitLab is what GitHub calls a “repo.” Same artifact, friendlier name. Here is the click trail — four steps from the dashboard to a working remote that you can git clone.

  1. From the left sidebar, open Projects and select New project.
  2. Choose Create blank project. (Templates and Imports live alongside it — useful later.)
  3. Name it. Set visibility to Internal for most Gauntlet AI work — team members can read, the wider internet cannot.
  4. Click Create project. You now have a Git remote at labs.gauntletai.com/<group>/<name>.git.

Tutorial → docs.gitlab.com / Create a project

01 sidebar Projects
02 menu New project
03 options Create blank
04 visibility Internal
05 commit Create project →
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
DRILL 04
——

Everyday git,
identical.

The whole point of Git is that the remote is interchangeable. Every command you have ever typed against GitHub works against labs.gauntletai.com with no edits. Run the simulator below to walk a feature branch from clone to push, command by command.

// operator@gauntlet  ·  ~/work/feature-cool-thing  ·  zsh
// Tap a command below to run it. Watch what happens. // Hit RESET any time to start over. operator@gauntlet ~/work $

The push response is the one place GitLab does something distinctive — it hands you back a URL to open a Merge Request immediately. Hold onto that URL. We’ll use it next.

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
DRILL 05
——

Pull Request,
renamed: Merge Request.

The single biggest naming difference in your daily life. Same review flow, same comment threads, same approvals, same merge button. Side by side, the two workflows look like this:

// GitHub   (what you know)

  • Push branch with git push -u origin <branch>
  • GitHub returns: “Compare & pull request”
  • Open Pull Request — title, description, reviewers
  • Reviewers comment, approve, request changes
  • CI runs via GitHub Actions on each push
  • Click Merge pull request when green
→

// GitLab   (what you’re learning)

  • Push branch with git push -u origin <branch>
  • GitLab returns: “Create merge request for <branch>” + URL
  • Open Merge Request — title, description, reviewers
  • Reviewers comment, approve, request changes
  • CI runs via GitLab CI/CD (.gitlab-ci.yml) on each push
  • Click Merge when green

Tutorial → docs.gitlab.com / Create a merge request

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
DRILL 06
——

Log in once,
stay logged in.

Two minutes of setup, zero password prompts for the rest of your tour. Mint a Personal Access Token, let git’s credential helper cache it on disk, then clone over HTTPS like nothing ever changed. One-time pain. Forever-time payoff.

  1. Mint a Personal Access Token. Click your avatar (top-right of GitLab) → Edit profile → Access Tokens. Give it a name (e.g. laptop-2026), pick an expiry, and tick the read_repository and write_repository scopes. Click Create — then copy the token immediately. GitLab will never show it to you again.
  2. Tell git who you are and turn on the credential helper (right column, top block). credential.helper store is the magic line — it’s what turns this into a one-time login.
  3. Clone any project over HTTPS (right column, bottom block). Grab the URL from the project’s blue Code button → Clone with HTTPS.
  4. At the prompt, type your GitLab username, then paste the PAT as the password. Not your Gauntlet AI account password — the token you just minted.
  5. That’s the whole drill. Every clone, push, and pull from this machine is now passwordless — until the token expires. Treat the PAT like a password; if a laptop walks off, revoke it from the same Access Tokens page.
bash  ·  one-time git config
# identity — shows up on every commit you make
git config --global user.name  "Your Name"
git config --global user.email "you@gauntletai.com"

# the magic line — cache credentials on disk after first login
git config --global credential.helper store
bash  ·  first clone (one-time prompt)
# clone any project over HTTPS
git clone https://labs.gauntletai.com/<group>/<project>.git

# git prompts you exactly once:
#   Username for 'https://labs.gauntletai.com': your-gitlab-username
#   Password for 'https://...':                 <paste your PAT here>

# from now on: no prompts, ever. credentials live in ~/.git-credentials
git pull
git push

Full guides → docs.gitlab.com / Personal Access Tokens  ·  git-scm.com / gitcredentials

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
DRILL 07
——

The big GitLab
win: built-in CI.

GitLab’s pipeline runner is built into the same surface as your code — no separate tab, no separate billing, no separate vocabulary. Drop a .gitlab-ci.yml at the root of your project and it runs on every push. Toggle stages on the left, watch the YAML rebuild on the right, then run a fake pipeline.

// PIPELINE STAGES

.gitlab-ci.yml

      

Full docs → docs.gitlab.com / Get started with GitLab CI/CD

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
DRILL 08
——

Pipelines that fit
in four gigs.

Our runners are small Docker containers: 1 to 4 GB of RAM and a couple of CPUs. That’s plenty, if your tools know it. Most red pipelines on small runners aren’t bugs. They’re tools sized for a 32 GB laptop getting OOM-killed. Six rules, one simulator, and one way to split heavy work. The full copy-paste template is in Drill 10.

// 08.A  The six rules
RULE 01

Know the kill signal.

exit code 137, a bare Killed, or heap out of memory means the job ran out of RAM. Fix memory first, then debug your code.

RULE 02

Cap the workers.

Containers often report the host’s CPU count, so tools spawn 8+ workers on a 2-CPU box. Pin it: jest --maxWorkers=2, pytest -n 2, go test -p 2, make -j2.

RULE 03

Cap the heap.

Tell the runtime where the ceiling is. Aim for ~75% of runner RAM: NODE_OPTIONS=--max-old-space-size=1536, -XX:MaxRAMPercentage=75, GOMEMLIMIT=1500MiB.

RULE 04

One heavy thing per job.

Don’t build, run a database, and run e2e tests in one job. Split them up and pass build output forward with artifacts + needs.

RULE 05

Shard anything over ~10 min.

parallel: 4 runs four copies of a job. Each one takes a slice using $CI_NODE_INDEX / $CI_NODE_TOTAL. You get the same coverage, a quarter of the wall time, and a smaller footprint per job.

RULE 06

Don’t run what didn’t change.

rules: changes: skips untouched areas. interruptible: true kills stale pipelines when you push again. timeout: 15m stops runaway jobs early.

// 08.B  Trainer: RAM budget

A Jest test job, out of the box, on a small runner. It starts out OOM-killed. Change the dials until it fits and finishes fast. The YAML underneath updates as you go.

// RUNNER & JOB DIALS

Runner RAM2 vCPU
Set by whoever runs the runner. You can’t raise it from YAML, so design for it.
Test workers7 · auto
“Auto” sizes to the host’s 8 CPUs, but this container only gets 2.
shards · parallel: N1 job
Splits the suite across N small jobs that run side by side.
job  ·  test  ·  peak memory—
LIMIT 2 GB
runtime + runner one block per worker
Peak RAM
—
Wall time
—
Status
—

    // Numbers are illustrative, not measured, but the shape is real: RAM scales with workers, speed scales with CPUs.

    yaml  ·  your test job, as dialled
    
            
    // 08.C  Split it, don’t scale it

    When a job is long or memory-hungry, the fix is almost never “get a bigger runner.” Break it into small jobs that each fit comfortably, and let GitLab run them side by side.

    Before
    ci-everythinginstall → build → unit → e2e · 22 min · peak 3.9 GB · exit 137
    After
    lint1 min · 0.4 GB
    →
    test 1/44m · 0.8 GB
    test 2/44m · 0.8 GB
    test 3/44m · 0.8 GB
    test 4/44m · 0.8 GB
    →
    build3 min · 1.2 GB
    → artifact
    →
    e2enightly / manual
    MoveUse it when…YAML
    Shard One big test suite is slow or bloats memory over time. parallel: 4 + --shard=$CI_NODE_INDEX/$CI_NODE_TOTAL
    Matrix Monorepo packages, or several versions or targets. One small job each. parallel: matrix: - PKG: [api, web, cli]
    Build once, pass it on Compiling is the expensive bit and several later jobs need the output. artifacts: paths: + needs: [build]
    Move to a schedule E2E, performance, or full security scans. Important, but not needed on every push. if: $CI_PIPELINE_SOURCE == "schedule"
    Make it manual Heavy and rarely needed, like a full rebuild or a data migration dry-run. when: manual
    Cache dependencies Every job re-downloads the world. That’s slow, and it spikes disk and memory. cache: key: files: [package-lock.json]

    Full docs → docs.gitlab.com / Pipeline efficiency  ·  docs.gitlab.com / parallel

    ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    DRILL 09
    ——

    Pay the setup tax
    once.

    Before a job runs a single test, it pays three taxes: pull an image, install tools, install dependencies. On a small instance with a couple of runners, paying all three in every job is where your minutes go. Bake the tools into a prebuilt image, install dependencies once per pipeline, and keep the instance tidy.

    // 09.A  Three layers, three speeds
    IMAGE
    Tools. OS packages, compilers, CLIs, browsers, uv, pnpm.
    changes monthly
    rebuild when Dockerfile.ci changes
    INSTALL
    Dependencies. node_modules, .venv, the Go module cache.
    changes weekly
    reinstall when the lockfile changes
    JOB
    Your code. Lint it, test it, build it. This is the only part that should change on every push.
    changes every push
    the only thing you should pay for each time
    // 09.B  Trainer: the job tax

    Four jobs from one pipeline. Grey is image + tool setup, amber is installing dependencies, and green is the work you actually wanted done. Flip the two switches and watch the grey and amber shrink.

    CI minutes / push
    —
    Spent on overhead
    —
    // 09.C  Prebuilt CI image
    1. Put a Dockerfile.ci at the project root. Start from a slim base and add only the tools your jobs need.
    2. Add a ci-image job in the built-in .pre stage. It runs only when Dockerfile.ci changes on the default branch.
    3. It pushes to your project’s own registry as $CI_REGISTRY_IMAGE/ci:latest. Every other job uses that as its image:.
    4. First time only: start the pipeline by hand (Build → Pipelines → Run pipeline) so the image exists before anything needs it.
    5. Keep dependencies out of the image. They change too often, and that’s the install layer’s job.
    docker  ·  Dockerfile.ci
    # tools only. no app code, no node_modules
    FROM node:20-slim
    RUN apt-get update \
     && apt-get install -y --no-install-recommends git ca-certificates curl jq \
     && rm -rf /var/lib/apt/lists/*          # keep the layer small
    yaml  ·  build + push the image
    ci-image:
      stage: .pre                       # built-in, always first
      image: docker:27
      services: [docker:27-dind]       # needs a privileged runner
      variables:
        DOCKER_TLS_CERTDIR: "/certs"
      script:
        - echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin "$CI_REGISTRY"
        - docker build -f Dockerfile.ci -t "$CI_REGISTRY_IMAGE/ci:latest" .
        - docker push "$CI_REGISTRY_IMAGE/ci:latest"
      rules:
        - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
          changes: [Dockerfile.ci]
    
    default:
      image: $CI_REGISTRY_IMAGE/ci:latest
    // 09.D  Install once, sync everywhere
    1. Install from the lockfile, strictly. Use the command that refuses to drift, so every job gets byte-for-byte the same dependencies.
    2. One install job does it. Its download cache is keyed on the lockfile, so it’s fast unless dependencies actually changed.
    3. It hands the installed folder forward as a short-lived artifact. Later jobs list needs: [install] and get it automatically. No reinstall.
    npmnpm ci
    pnpmpnpm install --frozen-lockfile
    yarnyarn install --immutable
    uvuv sync --frozen
    poetry 2poetry sync
    gogo mod download
    yaml  ·  install once, sync everywhere
    install:
      stage: install
      script:
        - npm ci --cache .npm --prefer-offline
      cache:                          # speeds up THIS job
        key:
          files: [package-lock.json]
        paths: [.npm/]
      artifacts:                      # hands deps to later jobs
        paths: [node_modules/]
        expire_in: 1 hour              # don't fill the server disk
    
    test:
      stage: test
      needs: [install]                 # downloads node_modules/
      script:
        - npx jest --maxWorkers=2
     CacheArtifacts
    PurposeSpeed things up if they’re thereHand exact files to later jobs
    Guaranteed?No. A runner without a shared cache starts cold.Yes, within the pipeline.
    Lives onThe runner (or its shared cache bucket)The GitLab server’s disk, so always set expire_in
    Use forDownload caches: .npm/, .uv-cache/, Go modulesInstalled deps, build output (dist/), reports
    // 09.E  Small-instance hygiene
    HYGIENE 01

    One pipeline per push.

    A branch with an open MR can trigger two identical pipelines. Use workflow: rules: to run the MR pipeline and skip the branch one. That halves your load on its own.

    HYGIENE 02

    Expire your artifacts.

    Artifacts live on the server’s disk. Use expire_in: 1 hour for installed deps and 1 day for build output. Only releases deserve longer.

    HYGIENE 03

    Clean up old images.

    Settings → Packages and registries → Cleanup policies. Keep the last 5 tags and drop anything older than 14 days. Otherwise registry disk only grows.

    HYGIENE 04

    Cancel stale runs.

    Mark jobs interruptible: true. When you push again, GitLab cancels the old pipeline and frees the runner for the one that matters.

    HYGIENE 05

    Shards ≤ runner slots.

    Small instances often have only 2–4 concurrent job slots. Running parallel: 8 on 2 slots just queues, so match shards to slots.

    HYGIENE 06

    One deploy at a time.

    resource_group: production queues deploys instead of racing them. Two deploys landing at once is a bad afternoon on any size instance.

    Full docs → Caching  ·  workflow:rules  ·  Registry cleanup

    ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    DRILL 10
    ——

    The pipeline
    forge.

    Everything from Drills 07–09 in one place. Every dial rewrites the YAML on the right, and the lines that changed light up. The scoreboard tells you how lean the result is. Start from Out of the box and work your way to 100, or jump to the lean preset and copy it straight into your project.

    Presets
    Lean score
    —
    Test job peak RAM
    —
    Pipeline wall time
    —
    CI minutes / push
    —

    // STACK

    // RUNNER

    Runner RAM2 vCPU each
    Concurrent slots
    How many jobs your runners can run at the same time. Ask your runner admin.

    // TESTS

    Test workers per job
    Shards · parallel: N

    // SPEED

    // HYGIENE

    Job timeout
    Build artifacts expire

    // SCORECARD

      .gitlab-ci.yml —
      
            

      // Times and memory are illustrative estimates for a typical mid-size project. The YAML is real: copy it, then swap in your own paths and commands.

      Full docs → CI/CD YAML reference  ·  needs (DAG pipelines)

      ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
      FINAL
      ——

      The final
      challenge.

      Fourteen questions across six skill areas. One attempt per question, so pick deliberately. A wrong answer doesn’t end the run. It gets logged, and at the end you get a personal debrief: where you’re strong, where you’re shaky, and which drill to revisit. Score 10 or more to clear the gauntlet. Your name and last score stay put for this browser session.

      // IDENTIFY YOURSELF
      Before we start: who’s running the gauntlet?

      14 questions · 6 skill areas · ~5 minutes · debrief at the end

      // INITIATION COMPLETE

      Welcome to the fleet.

      SCORE · 0 / 14