Skip to content

Configuration

Setup-Runner.ps1 is the operator entry point. Each invocation converges one profile without changing other profiles on the same host.

Setup parameters

Parameter Required Description Default
-Token No Fine-grained PAT used only to register runners. When omitted for an existing profile, PitCrew reuses its stored token before trying gh auth token. Stored profile token, then authenticated gh token
-Profile No Built-in profile name. default
-ProfilePath No Path to an external profile manifest. Relative image-build paths resolve from the manifest directory. None
-Scope No GitHub runner scope: repo, org, or ent. repo
-Repos Repository scope Repository URLs, optionally followed by =workers. None
-AddRepos No Adds repositories to the selected profile's generated state. None
-RemoveRepos No Removes repositories from the selected profile's generated state. None
-OrgName Organization scope GitHub organization name. None
-EnterpriseName Enterprise scope GitHub enterprise name. None
-Replicas No Default workers per repository, or total workers for organization/enterprise scope. 0 auto-sizes to half the host processors with a minimum of two. Profile value
-Labels No Comma-separated custom labels. The mandatory profile label remains. Profile value
-NamePrefix No Prefix shown for runner registrations in GitHub. Host name plus profile
-Image No Overrides the profile's worker image. Profile value
-PullImage No Controls whether setup pulls a prebuilt image before verification. Profile value
-RunnerGroup No Organization or enterprise runner group. Profile value
-Autoscale No Enables GitHub Runner Scale Set demand-driven activation. Configured counts become maximum capacity. Off
-MinimumIdle No Warm idle runners retained per autoscaled target. 0
-ScaleDownDelaySeconds No Stable low-demand period before excess idle JIT runners are removed. 120
-MaximumActiveWorkers No Contract-11 aggregate active-worker ceiling across all targets in an autoscaled profile. None
-WorkerMemory No Contract-11 per-worker memory limit in bytes or a binary unit such as 512MiB or 2g. Unlimited
-WorkerMemorySwap No Contract-11 total memory-plus-swap limit. Requires -WorkerMemory and cannot be lower than it. Unlimited
-WorkerCpus No Contract-11 positive per-worker CPU limit with at most nine fractional digits. Unlimited
-WorkerPids No Contract-11 positive per-worker process limit. Unlimited
-Down No Stops only the selected profile and removes its managed workers. Off
-Refresh No Builds and hot-swaps only the selected manager while preserving compatible workers and active jobs. Off
-RecoverMissingManager No With -Refresh, explicitly starts an existing selected profile that has no running manager. Plain refresh continues to preserve intentionally stopped profiles. Off
-CapacityOnly No Requires an in-place capacity update and fails rather than replacing a manager when the current profile cannot reconcile capacity safely. Off
-Pause No Reuses the existing desired targets, sets their effective capacity to zero, and drains busy workers naturally without stopping the manager. Off

Repository worker counts

Repository scope supports a different count for every target:

.\Setup-Runner.ps1 -Repos `
    https://github.com/you/light-project=1,`
    https://github.com/you/heavy-project=6

These workers are dedicated to their repositories. Use organization or enterprise scope when several repositories should share one capacity pool.

Profile manifest

Named profiles conform to runner-profile.schema.json.

Field Required Description
schemaVersion Yes Manifest contract version. Version 1 is currently supported.
name Yes Lowercase profile identifier and mandatory routing label.
description Yes Human-readable purpose.
image Yes Worker image tag.
labels Yes Additional capability labels.
replicas Yes Default positive worker count.
pullImage No Pull a prebuilt image before verification.
disableDefaultLabels No Omit GitHub's broad default labels. Named profiles default to true.
runnerGroup No Organization or enterprise runner group.
autoscaling No Scale-set mode, minimum idle runners, scale-down stabilization delay, and optional aggregate admission ceiling.
hostAdmission No Opt-in host-local admission namespace, capacity, safety margin, per-worker cost, reservation, and borrowing policy.
readOnlyVolumes No Existing external Docker named volumes mounted at deterministic /mnt/pitcrew-data/<name> paths.
serviceNetwork No One existing local, non-internal Docker bridge network that provides stable DNS for operator-owned or explicitly PitCrew-shipped profile services.
runtime No Bounded container-runtime policy. Only typed KVM access and canonical shared-memory sizing are supported.
resources No Contract-11 per-worker memory, memory-plus-swap, CPU, and PID policy.
verificationCommands No Shell commands executed in the prepared image before profile replacement.
build No Local Docker build context, Dockerfile, and non-secret build arguments.

Autoscaling policy

Field Required Description Default
mode Yes GitHub demand integration. scale-set is supported. None
minimumIdle No Warm idle JIT runners retained per target. 0
scaleDownDelaySeconds No Stable low-demand period before idle removal. 120
maximumActiveWorkers No Aggregate active-worker ceiling across all targets. Must be positive. None

minimumIdle and prewarming can reduce cold-start exposure, but they are operational mitigations rather than proof of any host, network, or package-feed root cause.

Bounded worker runtime

The optional runtime object permits the narrow container capabilities that both manager modes implement:

{
  "runtime": {
    "devices": ["kvm"],
    "sharedMemory": "2g"
  }
}

devices currently accepts only kvm, which maps to the exact Docker device /dev/kvm:/dev/kvm:rwm. Arbitrary device paths, capabilities, blanket privilege, seccomp settings, host mounts, and Docker endpoints are rejected by the schema.

sharedMemory uses the same canonical binary-size syntax as memory limits and must be at least 64 MiB. Runtime policy contributes to worker revision and static diagnostics. Setup applies it to image-verification containers before manager handoff, so a missing KVM device fails without replacing the current profile.

Worker-runtime contract version 3 adds this normalized policy without changing the manager observed-state contract. Changes roll worker containers while preserving assigned jobs on their prior runtime.

Host-local admission policy

hostAdmission opts one profile into the workload-agnostic host-local admission contract:

{
  "hostAdmission": {
    "namespace": "shared-ci",
    "capacityUnits": 20,
    "safetyMarginUnits": 4,
    "workerCostUnits": 4,
    "reservationUnits": 8,
    "borrowable": false
  }
}

These values are synthetic examples, not measurements or recommendations. capacityUnits and safetyMarginUnits are host-wide values and must match every participating profile in the namespace. The effective admission budget is capacity minus safety margin.

workerCostUnits, reservationUnits, and borrowable are profile policy. Reservations are expressed in the same abstract units as worker cost and may provide partial progress toward a later worker admission. A non-borrowable reservation preserves headroom for that profile. A borrowable reservation may be used by another eligible profile but never preempts an active worker when the owner later has demand.

Units are abstract positive integers derived from controlled measurement. They are not CPU cores, memory bytes, worker counts, or inferred hardware capacity. Built-in profiles declare no host-admission policy, and independent-profile behavior remains the default.

Manager contract 18 resolves and fingerprints this policy, starts the dedicated coordinator through Setup-Runner.ps1, and enforces leases for both fixed and autoscaled managers. Disabled profiles (no hostAdmission manifest entry) remain behavior-compatible; they never touch the coordinator.

When policy is enabled for a profile that already has running workers, both manager modes adopt those workers as durable active leases without stopping or recreating them. Adoption reflects usage that already exists and is therefore not denied by the configured budget. If retained usage exceeds the effective budget, availableUnits remains zero and ordinary acquisition stays withheld until workers exit and release their leases naturally.

Setup establishes a durable coordinator fence before replacing a participating manager. The fence blocks ordinary acquisition for every profile in the namespace until that manager completes its recovered-running-worker adoption pass. Fences from multiple profile replacements compose and survive coordinator restart; completing one profile does not clear another profile's fence.

Protocol 2 adds adoption and recovery-fence operations. Protocol 2 clients and servers continue to negotiate protocol 1 for ordinary lease and status commands during coordinator-first rolling replacement. Adoption and fence operations require protocol 2 and fail closed against a protocol 1 service.

Labels, runner groups, and scale sets control GitHub queue eligibility. Host-local admission separately controls whether a participating manager may start a new worker. It does not constrain non-participating profiles or other host processes and does not preempt existing workers.

Use the Host-Local Admission Operations guide to calibrate service classes, enable and update policy, validate reservations and borrowing, diagnose failures, and roll back through supported setup paths. Configured reservation alone is not proof of protected headroom.

Observed-state telemetry

Once contract 18 is active, observed-state.json carries a root-level, credential-free hostAdmission object describing this profile's own admission state. Its status is one of:

Status Meaning
disabled This profile has no hostAdmission policy. Every other field is null.
available The coordinator responded with the expected namespace, complete policy identities, and current demand accounting for this profile.
degraded The coordinator responded, but its namespace or policy identity does not match, this profile is unknown, or demand has not yet been republished after coordinator restart or policy replacement.
unavailable The configured namespace is known, but the coordinator could not be reached or returned an unreadable response. Existing workers continue; new admission fails closed.

With unavailable, the configured namespace remains populated and measured values are null, never a fabricated zero, so a reader cannot mistake "could not measure" for "measured as empty."

Accounting is scoped to this profile only; the coordinator's full multi-profile ledger is never published. The published fields use these precise semantics:

  • epoch — applied host-policy epoch.
  • decisionSequence — durable sequence of granted lease operations. A denied decision does not advance it.
  • capacityUnits, safetyMarginUnits, and effectiveTotalUnits — configured host policy and its effective budget.
  • availableUnits — coordinator budget not currently held by leases.
  • hostPolicyFingerprint and profilePolicyFingerprint — bounded identities used to detect incompatible policy.
  • unitCost, reservedUnits, and borrowable — this profile's current policy.
  • activeUnits — units held by active leases. Ambiguous recovery may retain an active lease until exact worker absence and release are proven.
  • provisionalUnits — units tentatively held for a worker still starting.
  • heldUnitsactiveUnits + provisionalUnits.
  • borrowedUnitsmax(heldUnits - reservedUnits, 0): shared capacity this profile is using beyond its own configured reservation.
  • pendingUnits — outstanding worker demand multiplied by this profile's unit cost.
  • withheldUnits — the same outstanding units while host admission has not granted them. Positive demand must be refreshed within 30 seconds. Both values are null until demand is republished after coordinator restart, policy replacement, or demand expiry.

Per-target capacity evidence keeps this host result separate from the existing profile ceiling. host-admission-withheld identifies a target currently denied by host budget or fairness; host-admission-degraded identifies incompatible policy or lease state; and host-admission-unavailable identifies a target whose new launch is blocked because the coordinator cannot be reached.

lastDecision is a single bounded record of this profile's most recent admission decision (sequence, command, granted, failureCategory, decidedAtUnixNano) — never an exact slot identity, raw error message, host name, runner name, container ID, path, URL, or job output. The closed command vocabulary includes adopt for existing-worker recovery in addition to ordinary acquire, renew, activate, release, and reconcile decisions.

Shared-pool fairness rotates equal unit opportunity among profiles with registered pending demand after own reservations and unused non-borrowable reservations are accounted for. Abandoned positive demand expires after 30 seconds so a stopped manager cannot protect shared capacity indefinitely. It is not GitHub queue weighting or worker priority. Different worker costs can produce different worker counts and leave fragments smaller than one whole worker cost.

Read-only external volumes

Each entry contains:

Field Required Description
name Yes Unique logical lowercase name; the worker target becomes /mnt/pitcrew-data/<name>.
source Yes Existing external Docker named volume. PitCrew never creates or removes it.

Setup inspects every source before manager handoff and attaches the volumes to image-verification containers. The normalized contract contributes to worker revision, so source changes roll safely while busy workers retain their original mounts. Volume changes are rejected by -Refresh and -CapacityOnly.

Only read-only named volumes are supported. Bind mounts, arbitrary targets, devices, sockets, driver options, and credentials are outside the profile contract. See Read-Only External Data Volumes.

External service network

serviceNetwork.source names one existing user-defined Docker network. Setup requires the exact network to use the local bridge driver with Internal=false, rejects Docker's built-in bridge, then attaches image-verification containers and new workers with --network <source>.

The network contributes to worker revision, so source changes roll while busy workers retain their original network. Service-network changes are rejected by -Refresh and -CapacityOnly.

PitCrew never creates, configures, removes, or attaches the service itself. Aliases, ports, storage, credentials, and service health remain operator-owned. Manager Compose networks are reserved and cannot be selected. Use one service network per profile or trust boundary because containers on a shared bridge network can reach each other's exposed ports. See Pool-Local Services.

Contract-11 resource policy

The profile schema defines the following future contract:

{
  "schemaVersion": 1,
  "name": "bounded-build",
  "description": "Build workers with explicit resource admission.",
  "image": "example/runner:1.0.0",
  "labels": ["build"],
  "replicas": 8,
  "autoscaling": {
    "mode": "scale-set",
    "minimumIdle": 1,
    "scaleDownDelaySeconds": 120,
    "maximumActiveWorkers": 6
  },
  "resources": {
    "memory": "8GiB",
    "memorySwap": "10GiB",
    "cpus": "2.5",
    "pids": 1024
  }
}

Memory units use powers of 1024 and are stored as canonical byte counts. Memory must be at least 6 MiB. memorySwap is total memory plus swap, requires memory, and must be greater than or equal to it. Unlimited -1 values are not accepted. CPU values are stored as invariant decimal strings without insignificant zeroes. Empty generated environment values mean no configured limit; managers must not interpret them as zero.

Resource policy and maximumActiveWorkers were introduced in manager contract 11 and remain supported by the active contract 18 managers. A profile that still runs an older manager upgrades through the established manager hot-swap, and its existing workers are preserved and converge naturally. Activation occurs only after both manager modes implement the same contract, so a newer contract is still refused before Docker, image, or generated state mutation.

Worker image shutdown contract

The default worker image retains its GitHub credential only in the entry-point shell and explicitly does not export that credential to the runner process. PitCrew leaves the private shell value available so the image can deregister on SIGTERM. Manager stop and restart signal all workers concurrently, wait a bounded period, and force-remove only exact-label leftovers.

Custom worker images must provide the same contract: handle SIGTERM, deregister the current runner, keep registration credentials out of the runner process and workflow environment, and exit within the manager's shutdown window. Images that discard their deregistration credential after startup can leave offline runner registrations behind.

Generated state

The default profile writes .env; named profiles write .env.<profile>. These static environment files contain the runner-registration token plus image, immutable local image ID, labels, scope, runner group, name-prefix settings, and canonical manager policy. PitCrew generates them and Git ignores them. Do not edit or commit them.

Mutable capacity is stored separately under .pitcrew-state/<profile>/desired-capacity.json. The document contains no registration token or workload credential. Setup validates the complete next document, writes it through a temporary file and atomic rename, and waits for the running manager to acknowledge its generation.

Each manager also projects credential-free operational status to .pitcrew-state/<profile>/observed-state.json. The manager replaces this file atomically after slot lifecycle changes and on a low-frequency heartbeat. It contains the manager instance, accepted generation, desired-state health, and per-slot lifecycle state. Every 30 seconds, the same projection samples host capacity plus manager and worker CPU cores, memory working-set bytes, and PID counts. A CPU value of 1.0 represents one fully utilized logical processor and can exceed 1.0 for a multi-core workload.

Resource telemetry is marked available, partial, or unavailable; missing measurements remain null rather than appearing as zero usage. The manager collects these values through its existing Docker socket. Connectors and dashboard services continue to consume only the read-only state projection and do not receive Docker access.

Manager contract 7 introduces these additive fields. Older connectors continue to relay lifecycle state but discard fields they do not recognize, so update the optional connector and dashboard before expecting resource cards to appear. Manager contract 8 adds configured-maximum and autoscaling state while retaining the same credential-free connector boundary. Manager contract 9 adds worker revision and rolling-convergence state. Manager replacement preserves sibling workers; scale-set profiles safely replace stale idle JIT runners through GitHub's service-side removal fence. Current managers also identify the configured target image reference and its resolved immutable local image ID in that rollout projection. Legacy contract-9 through contract-11 observations that omit those additive fields remain valid; consumers must report the target identity as unavailable rather than infer it from one live worker. Manager contract 10 adds GitHub registration reconciliation. Each slot reports whether its runner is connected, disconnected, missing from GitHub, or unknown, and the profile reports eligible capacity separately from running containers. Fixed managers replace only exact workers that remain missing or offline and not busy across repeated server-side observations.

The contract-11 schema adds the configured resource policy, immutable image identity, cumulative network and block-I/O counters, exit diagnostics, an aggregate autoscaling ceiling, timestamped GitHub scale-set statistics, and separate local worker counts. A representative field excerpt is:

{
  "resourcePolicy": {
    "memoryBytes": 8589934592,
    "memorySwapBytes": 10737418240,
    "cpuCores": "2.5",
    "pids": 1024
  },
  "autoscaling": {
    "maximumActiveWorkers": 6,
    "targets": [
      {
        "key": "repo:example/project",
        "repository": "https://github.com/example/project",
        "maximumSlots": 8,
        "targetSlots": 2,
        "localActiveWorkers": 2,
        "localIdleWorkers": 0,
        "localBusyWorkers": 2,
        "localDrainingWorkers": 0,
        "statistics": {
          "observedAt": "2026-07-26T12:00:00Z",
          "availableJobs": 0,
          "acquiredJobs": 0,
          "assignedJobs": 2,
          "runningJobs": 2,
          "registeredRunners": 8,
          "busyRunners": 2,
          "idleRunners": 6
        }
      }
    ]
  },
  "update": {
    "status": "rolling",
    "targetImage": "ghcr.io/example/project-runner@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
    "targetImageId": "sha256:2222222222222222222222222222222222222222222222222222222222222222",
    "targetRevision": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
    "currentWorkers": 1,
    "staleWorkers": 1,
    "lastError": null
  },
  "slots": [
    {
      "resources": {
        "cpuCores": 1.25,
        "memoryWorkingSetBytes": 2147483648,
        "pids": 48,
        "networkRxBytes": 1048576,
        "networkTxBytes": 262144,
        "blockReadBytes": 536870912,
        "blockWriteBytes": 134217728
      },
      "imageId": "sha256:1111111111111111111111111111111111111111111111111111111111111111",
      "lastExit": {
        "observedAt": "2026-07-26T11:55:00Z",
        "classification": "oom-killed",
        "exitCode": 137,
        "signal": 9,
        "dockerOomKilled": true,
        "evidence": "docker-inspect"
      }
    }
  ]
}

The excerpt shows the exact contract-11 field shapes but omits unchanged top-level and slot fields. I/O counters are cumulative. null means a measurement is unavailable; zero means it was measured as zero. GitHub statistics are timestamped external evidence, while local*Workers describe local containers. Neither source substitutes for the other.

Exit classification uses this precedence: Docker-confirmed OOM, SIGKILL, another signal, clean zero exit, ordinary nonzero error, launch failure, then unknown. Exit code 137 alone does not prove an OOM kill.

The fixed manager already publishes these projections. It applies the configured memory, swap, CPU, and PID limits to every worker it launches, so a policy change converges as busy workers finish their current job and are replaced. Exit evidence comes from Docker: the container state when it is still readable, and otherwise the wait status plus a bounded out-of-memory event lookup for the exact container. When Docker offers no usable evidence the slot reports unknown rather than a clean exit, and an unconfirmed out-of-memory kill stays null.

Contract-12 operation evidence

The contract-12 schema adds three additive projections: a durable manager operation journal, current Docker and GitHub subsystem summaries, and explicit capacity-deficit evidence. Every field is nullable for contract-11 and older observations, so contract-10 and contract-11 readers are unaffected.

{
  "operationJournal": {
    "status": "current",
    "capacity": 64,
    "highestSequence": 45,
    "droppedEvents": 0,
    "events": [
      {
        "sequence": 45,
        "managerInstanceId": "manager-instance-b",
        "observedAt": "2026-07-26T12:00:00Z",
        "subsystem": "worker-launch",
        "operation": "worker-launch",
        "target": "repo-example-000001",
        "outcome": "retry-scheduled",
        "durationMilliseconds": null,
        "attempt": 2,
        "consecutiveFailures": 2,
        "retryAt": "2026-07-26T12:00:30Z",
        "reason": "retry-backoff",
        "evidence": "Worker launch is waiting for its backoff window"
      }
    ]
  },
  "subsystemHealth": {
    "docker": {
      "state": "healthy",
      "observedAt": "2026-07-26T12:00:00Z",
      "consecutiveFailures": 0,
      "retryAt": null,
      "lastSuccess": {
        "operation": "docker-ping",
        "observedAt": "2026-07-26T12:00:00Z",
        "durationMilliseconds": 0,
        "reason": "none",
        "evidence": null
      },
      "lastFailure": null
    },
    "github": {
      "state": "degraded",
      "observedAt": "2026-07-26T12:00:00Z",
      "consecutiveFailures": 1,
      "retryAt": "2026-07-26T12:00:30Z",
      "lastSuccess": null,
      "lastFailure": {
        "operation": "registration-token-request",
        "observedAt": "2026-07-26T11:59:30Z",
        "durationMilliseconds": 30000,
        "reason": "timeout",
        "evidence": "Registration token request exceeded its deadline"
      }
    }
  },
  "capacityEvidence": {
    "fixed": {
      "observedAt": "2026-07-26T12:00:00Z",
      "freshness": "current",
      "targetSlots": 2,
      "activeWorkers": 1,
      "startingWorkers": 0,
      "drainingWorkers": 0,
      "cleanupPendingWorkers": 0,
      "eligibleWorkers": 1,
      "localDeficit": 1,
      "eligibilityDeficit": 1,
      "reason": "retry-backoff",
      "evidence": "One worker is waiting for its launch backoff window"
    },
    "targets": []
  }
}

The journal is persisted atomically beneath the profile state directory, so it survives ordinary manager restart and hot-swap. sequence is durable and monotonic across restarts, so dashboards deduplicate on the profile plus the sequence and treat managerInstanceId as the observer rather than the identity of the event. The journal retains failures, state transitions, retries, and recovery instead of every reconciliation pass.

Journal limits are strict and enforced: at most 64 retained events, at most 160 characters of sanitized evidence per event, and at most 16384 serialized bytes. subsystem, operation, outcome, and reason are closed vocabularies; a manager that needs a new value needs a new contract version. evidence excludes :, /, @, ?, =, and &, so tokens, URLs, HTTP bodies, environment values, JIT payloads, job output, and raw Docker or GitHub stderr cannot be relayed. target is limited to a slot or autoscaling target key that already appears in non-secret state.

Journal status separates the intact window (current) from a window that discarded older, malformed, or oversized entries (truncated, which requires a nonzero droppedEvents) and from a journal the manager could not read or restore (unavailable, which reports no events). A discarded journal never discards otherwise valid observed state. An empty events array with status current means no notable event has occurred.

Subsystem summaries describe operations PitCrew itself performed. They are not a claim that the host, Docker daemon, network, or GitHub service is healthy. unknown means the manager has performed no such operation yet and therefore carries no evidence, healthy requires a last success and zero consecutive failures, and degraded or unavailable requires a last failure and at least one consecutive failure.

Capacity evidence separates the actual target from local and control-plane counts. targetSlots is desiredSlots for a fixed profile and the activation targetSlots for an autoscaling target; a configured autoscaling maximum is not a health target and never creates a deficit by itself. eligibleWorkers and eligibilityDeficit are null together when the manager has no current control-plane evidence, while 0 means it observed none. freshness distinguishes current and stale measurements from unavailable evidence, which reports the unknown reason because the manager observed nothing to blame. reason is observed manager state, never a diagnosis inferred by a dashboard.

Manager contract 12 introduced the external service-network launch contract while retaining its diagnostic projections.

Contract-13 host hardware inventory

Contract 13 adds a top-level host.hardware projection. It describes only sanitized capacity and runtime facts visible to the manager:

{
  "host": {
    "hardware": {
      "status": "current",
      "collectedAt": "2026-08-03T12:00:00Z",
      "attemptedAt": "2026-08-03T12:05:00Z",
      "inventoryHash": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
      "processorModel": "Example Processor",
      "architecture": "amd64",
      "physicalCoreCount": 10,
      "logicalProcessorCount": 20,
      "performanceCoreCount": null,
      "efficiencyCoreCount": null,
      "memoryBytes": 34359738368,
      "operatingSystem": "Docker Desktop",
      "kernelVersion": "6.12.34",
      "dockerServerVersion": "28.3.3",
      "dockerStorageDriver": "overlayfs",
      "dockerBackingFilesystem": "extfs"
    }
  }
}

Every potentially unsupported field is present and nullable. PitCrew does not infer performance- and efficiency-core counts from processor marketing names. current means the latest bounded Docker probe succeeded, stale retains the last valid inventory after a failed refresh, and unavailable contains no retained values.

The inventory hash covers only the ordered hardware values, not timestamps or freshness. An unchanged inventory preserves collectedAt across periodic samples and manager handoff. A changed processor, topology, memory allocation, OS/kernel, Docker version, storage driver, or backing filesystem produces a new hash and collection timestamp.

The projection excludes usernames, absolute paths, serial numbers, machine GUIDs, network addresses, MAC addresses, Docker root paths, credentials, registration material, and job output.

Manager contract 18 is active in this release. Both manager modes publish the same hardware contract while retaining contract-11 resource and contract-12 diagnostic semantics. Setup fails closed before Docker, image, or generated state mutation if a contract ahead of both implementations is selected.

Contract-14 runner correlation

Contract 14 adds runnerNameHash to every observed slot. For a live worker it is the lowercase SHA-256 digest of the exact UTF-8 runner name registered with GitHub. The field is null whenever the manager does not currently hold a usable exact runner identity, including non-running and launch-backoff slots.

The hash is a correlation key, not an authentication value or a fuzzy host identifier. A diagnostic client can hash the exact runner_name already returned by GitHub job metadata and compare by equality. PitCrew never publishes the raw runner name, configured prefix, container name, container ID, registration payload, JIT configuration, token, or job output.

The stable slot key remains unchanged and continues to own reconciliation. Dashboard retention of historical hash-to-node/profile assignments is a separate downstream responsibility; PitCrew observed state reports only the current bounded slot projection.

Contract-15 active job context

Contract 15 adds currentJob to every observed slot. Fixed workers, idle workers, recovered workers whose start event cannot be reconstructed, and other unattributed workers report null. An autoscaled worker reports a bounded object while the scale-set listener owns usable lifecycle metadata for its current job.

The object contains only the canonical GitHub repository URL, workflow-run and job identifiers, bounded display and event names, queue/assignment/start timestamps, and a bounded finish result while the draining worker still exists. Dashboard can derive an exact GitHub job link and retain the interval after the ephemeral worker exits.

PitCrew does not publish the workflow ref, requested labels, runner ID or name, job message payload, logs, step output, environment values, commit text, registration material, or credentials. Invalid or oversized metadata degrades only currentJob; the worker remains busy and protected. Resource activity is never used to invent missing job identity.

Contract-16 Docker-host pressure

Contract 16 adds resourceTelemetry.hostPressure. The manager reads aggregate CPU, load, memory, swap, and optional Linux Pressure Stall Information from a read-only /proc mount that belongs only to the manager container.

The source is named docker-host because it describes the Docker engine's execution domain. On native Linux that is the Docker host kernel. On Docker Desktop or WSL it is the Linux VM that runs the containers, not a claim about the complete physical Windows or macOS machine.

The first CPU sample is partial because utilization requires two monotonic counter observations. Load and memory remain available immediately. PSI some and full ten-second averages are optional; kernels that do not expose them report null without degrading otherwise complete core pressure. Counter reset, manager restart, malformed files, or unavailable host-proc access never become measured zero.

Only aggregate files are read. PitCrew does not enumerate or publish process IDs, command lines, environment values, mount paths, or per-process data. Pressure remains diagnostic evidence; it never cancels a job, stops a worker, or changes admission automatically.

The fixed shell manager keeps the journal, the Docker summary, and the GitHub summary under diagnostics/ inside the profile state directory and writes each file atomically, so an ordinary manager restart or handoff preserves the preceding causal sequence without replaying events. The retained window is bounded to 32 events and a bounded serialized size; older events are dropped into droppedEvents and the journal reports truncated. A corrupt or unreadable journal degrades only the journal, and a failed diagnostic write never stops a worker, changes cleanup selectors, or discards desired state. Healthy reconciliation is not journaled: the fixed manager records state transitions, failures, retry scheduling, recovery, and unusually slow operations rather than every loop.

The projection contains no registration token, environment values, job logs, container identity, or Docker socket details. Resource usage does not identify whether a runner is busy, so consumers must not infer job state from CPU or memory activity. Consumers must use observedAt and the resource sampledAt value to reject stale status after an ungraceful manager exit.

For repository scope, desired state records each repository URL and worker count. Organization and enterprise scope record one shared replica count. The manager derives stable ordinal slot keys, so changing a repository from five workers to six starts only ordinal six. Changing it back to five drains only ordinal six.

Contract-17 zero-capacity pause

An existing desired target may carry zero capacity. -Pause writes that state through the normal capacity-only generation and acknowledgement path while retaining repository routing, scale-set identity, manager state, and history. It is distinct from manifest replicas, which remains a positive default, and from -Replicas 0, which keeps its auto-size meaning. Resume by applying a positive capacity with -CapacityOnly. Pause reuses the already accepted targets and does not require a new GitHub runner-registration access probe; resume and every positive capacity change retain the normal token validation.

For autoscaled profiles, the same values are configured maximums. GitHub's assigned-job statistics determine current activation between the minimum idle floor and each maximum.

Contract-18 host-local admission

Contract 18 adds the root-level hostAdmission object described under Host-local admission policy and allows the coordinator, generated host state, and manager enforcement to activate for profiles that opt into hostAdmission. Profiles without that manifest entry remain fully behavior-compatible: hostAdmission.status is disabled and no other field is ever populated.

The fixed manager obtains coordinator status through the same CLI used for lease acquisition, activation, and release; the autoscaler samples the same coordinator directly through its status client. Both project an identical contract shape and distinguish coordinator outage (unavailable) from budget denial, profile-policy mismatch (degraded), and ordinary GitHub demand or Docker/JIT/listener failure, which remain reported through their existing, unrelated fields. Reading admission status is diagnostics-only. A coordinator that cannot be reached never stops an existing worker or changes desired or acknowledged state, but it does fail closed for every new worker admission.

Capacity reconciliation

When the static profile fingerprint is unchanged and the manager is running, setup skips image pull/build and verification, leaves the manager container untouched, and publishes only desired capacity. Reapplying identical capacity is a no-op.

Scale-down is graceful:

  • A runner already executing a job is never force-removed because capacity decreased.
  • Once the draining runner container exits, its slot stops instead of spawning a replacement.
  • An idle ephemeral runner can accept one final job before it exits. PitCrew does not query GitHub's runner busy state in this reconciliation path.

Changes to labels, default-label behavior, scope, organization or enterprise identity, runner group, name prefix, registration token, build or verification contract, or manager runtime contract continue to replace the selected profile. Worker image content and resource-policy changes advance the worker revision but remain rolling-compatible, so busy workers can finish before replacement. maximumActiveWorkers changes manager compatibility without changing the worker revision.

Use -Refresh after switching an installation checkout to a new PitCrew release when the manager implementation changed without changing its runtime worker configuration. Refresh builds the replacement first and hands off existing workers without requiring them to be idle. Apply rolling-compatible worker image or resource-policy changes with the complete setup command; stop explicitly before routing or registration-topology changes. -Refresh and -CapacityOnly reject a changed local image ID even when its mutable tag is unchanged.

Plain -Refresh refuses to start a profile with no running manager, preserving an intentionally stopped profile. After diagnosing an unexpected missing manager, replay the exact existing configuration with -Refresh -RecoverMissingManager. The explicit opt-in requires compatible stored static state, builds the manager before changing profile state, and requires the selected manager to be absent.

Setup records Docker's immutable local sha256:<64 hex> image ID after pull or build and includes it in worker revision and refresh compatibility. A legacy static profile without this identity requires one complete safe setup run; it cannot be migrated with -Refresh or -CapacityOnly.

For a manifest-backed profile, static-profile.json also retains local non-secret manifest provenance: built-in or external kind, source path, SHA-256 content hash, and the parsed manifest document. Operations tooling can replay the approved snapshot without accepting a later source change implicitly. These local paths and manifest documents are never copied into observed state.

Locally built profiles also fingerprint their complete build-context inventory. Generated PitCrew state and the selected secret environment are excluded. The fingerprint is intentionally conservative: a file Docker later excludes may trigger an unnecessary rebuild, but a changed copied input cannot be skipped.

Legacy and direct Compose bootstrap

When neither desired nor last-valid state exists, the manager can import REPO_URLS or REPO_URL for repository scope, or RUNNER_REPLICAS for organization and enterprise scope. This is a one-time adapter for pre-reconciliation .env files and direct docker compose up usage. Direct Compose requires a stable PITCREW_SESSION_OWNER and a 64-character PITCREW_WORKER_REVISION. Contract-11 direct Compose also requires the exact local PITCREW_WORKER_IMAGE_ID and canonical policy values documented in .env.example.

After the adapter creates generation one, environment changes do not alter capacity. Use Setup-Runner.ps1 for every subsequent update so generation, locking, atomic publication, and acknowledgement remain enforced.

Manager termination without a container-targeted shutdown request preserves workers for handoff. Use Setup-Runner.ps1 -Down rather than routine docker compose down when the intent is to remove the complete profile.

The mounted directory contains no credentials. If Docker creates a missing bind source as root, the manager makes that directory host-writable so a later setup command can replace state atomically. Pre-create the directory when stricter host ownership is required.

On the first setup run after upgrading, -AddRepos and -RemoveRepos import repository targets from the old profile environment when desired state has not been created yet.