Skip to content

Repository files navigation

CherryPick

CherryPick reviews, selects, and approves task workflow outputs.

Its repository automation is a GPT-5.6-first harness. Eight thin skills live under .agents/skills, deterministic behavior lives in the shared FastMCP engine under src/cherrypick/module/harness, profile configuration lives under .codex/agents, and ephemeral work state lives under .agents/work/<work-id>.

Developer bootstrap

The supported development baseline is Git, uv, a just release that supports recipe groups and attributes, Docker with the Compose plugin, kubectl for the retained manifest-render replay, and a host capable of running CPython 3.14. Verify those tools before changing repository state:

git --version
uv --version
just --version
docker version
docker compose version
kubectl version --client

The full gate uses kubectl kustomize locally; it does not require a cluster or API-server access. Install a platform-compatible official binary and verify its published checksum. A user-local ~/.local/bin installation is sufficient.

Install Python and synchronize the committed lock as the workspace user:

uv python install 3.14
uv sync --python 3.14+gil --extra dev --frozen
uv run --frozen --offline --no-sync prek install

The last command installs both the declared pre-commit and pre-push hooks. After the initial synchronization, repository gates intentionally use --frozen --offline --no-sync.

Do not run uv, Git, or repository recipes through sudo. CherryPick does not edit /etc/apt, choose an Ubuntu mirror, install sudo, or manage the Docker daemon. The Kakao Ubuntu mirror is a valid host-local optimization, but any functioning package source is supported.

Recover an unusable .venv

If .venv/bin/python cannot be executed or the environment belongs to another user, first confirm that the path is the exact .venv inside this checkout. Move that directory aside, then rerun the synchronization as the workspace user. Do not delete a broad parent directory or use sudo uv.

Bash:

# Run this check first.
.venv/bin/python --version
# Only if that check fails, preserve this checkout's exact environment
# outside the repository:
venv_backup="${TMPDIR:-/tmp}/cherrypick-venv-unusable-$(date +%Y%m%d%H%M%S)"
mv .venv "$venv_backup"
uv sync --python 3.14+gil --extra dev --frozen

PowerShell:

# Run this check first.
& .\.venv\Scripts\python.exe --version
# Only if that check fails, preserve this checkout's exact environment
# outside the repository:
$venvBackup = Join-Path ([System.IO.Path]::GetTempPath()) `
  ("cherrypick-venv-unusable-" + (Get-Date -Format "yyyyMMddHHmmss"))
Move-Item -LiteralPath .venv -Destination $venvBackup
uv sync --python 3.14+gil --extra dev --frozen

The moved directory is a local backup outside the checkout and can be removed later after the replacement is verified.

Local configuration and database

Compose renders without a .env file. Create one only for local overrides or feature-specific secrets:

Bash:

cp .env.example .env

PowerShell:

Copy-Item .env.example .env

The complete key inventory, activation conditions, defaults, and secret classification are in docs/configuration.md. Values in .env.example are development defaults, not production credentials.

Build the exact PostgreSQL image used by local tests, render the Compose model, start the database, and apply Alembic migrations:

docker build --file infra/docker/Dockerfile.db --tag ghcr.io/choiec/cherrypick-db:local .
docker compose config --quiet
docker compose up --detach --build db migrate
docker compose wait migrate

PostgreSQL init scripts configure the database instance; Alembic owns the application schema. Ordinary startup never deletes or recreates the persistent volume.

Start the two application workloads with:

docker compose up --detach --build --wait --wait-timeout 120 api mcp

Both services run the same unified ASGI application containing REST, /mcp/chat, and /mcp/work. Ports 8000 (api) and 8001 (mcp) are workload aliases, not route-isolated servers. All three published ports bind only to 127.0.0.1 by default.

To run the application process on the host instead, leave db running, wait for the preceding one-shot migrate service to exit successfully, ensure DATABASE_URL addresses the database's published port, and run:

uv run --frozen --offline --no-sync fastapi dev src/cherrypick/main.py --host 127.0.0.1 --port 8000

Smoke-test liveness, database readiness, the OpenAPI schema, and a protected route with commands for your shell.

Bash:

curl --fail http://127.0.0.1:8000/health
curl --fail http://127.0.0.1:8000/:checkHealth
curl --fail http://127.0.0.1:8000/:checkReadiness
curl --fail http://127.0.0.1:8000/openapi.json
curl --include http://127.0.0.1:8000/tasks

PowerShell:

curl.exe --fail http://127.0.0.1:8000/health
curl.exe --fail http://127.0.0.1:8000/:checkHealth
curl.exe --fail http://127.0.0.1:8000/:checkReadiness
curl.exe --fail http://127.0.0.1:8000/openapi.json
curl.exe --include http://127.0.0.1:8000/tasks

The final request must fail closed with 401 Unauthorized when no Bearer token is supplied. AUTH_PLATFORM_ENABLED=false selects direct Bearer JWT verification; it does not disable authorization. The example issuer and JWKS URLs are deliberately non-runnable placeholders. A successful manual protected request requires an operator-supplied token from an approved external issuer; never print or commit that token. Automated positive authentication tests use static JWKS fixtures and do not require a local identity provider.

Codex Docker-out-of-Docker

When this checkout runs inside a Codex container, the outer host may already use ports 5432, 8000, or 8001. Override the published ports without editing Compose:

Bash:

export POSTGRES_HOST_PORT=15432
export API_HOST_PORT=18000
export MCP_HOST_PORT=18001
docker compose up --detach --build db migrate
docker compose wait migrate
docker compose up --detach --build --wait --wait-timeout 120 api mcp

PowerShell:

$env:POSTGRES_HOST_PORT = "15432"
$env:API_HOST_PORT = "18000"
$env:MCP_HOST_PORT = "18001"
docker compose up --detach --build db migrate
docker compose wait migrate
docker compose up --detach --build --wait --wait-timeout 120 api mcp

The outer Docker host's loopback is not the development container's 127.0.0.1. Verify the containerized path without guessing a host-gateway address by running these shell-neutral commands from either Bash or PowerShell:

docker compose ps api mcp
docker compose exec -T api python -c 'import http.client; c = http.client.HTTPConnection("127.0.0.1", 8000, timeout=5); c.request("GET", "/:checkReadiness"); r = c.getresponse(); assert r.status == 200, r.status'
docker compose exec -T api python -c 'import http.client; c = http.client.HTTPConnection("127.0.0.1", 8000, timeout=5); c.request("GET", "/openapi.json"); r = c.getresponse(); assert r.status == 200, r.status'
docker compose exec -T api python -c 'import http.client; c = http.client.HTTPConnection("127.0.0.1", 8000, timeout=5); c.request("GET", "/tasks"); r = c.getresponse(); assert r.status == 401, r.status'

The host-process alternative from the preceding section requires an explicit daemon-reachable database host in DATABASE_URL and forwarding for the daemon's loopback-published port. If the controlled container runner does not provide that forwarding, use the Compose application path above.

The development container needs access to a reachable Docker daemon, normally through /var/run/docker.sock. Testcontainers also needs a daemon-reachable host address and permission to start its Ryuk cleanup container. Set TESTCONTAINERS_HOST_OVERRIDE only when the daemon cannot route back to the container automatically. If a controlled runner forbids Ryuk, that runner owns explicit cleanup; do not disable it as a normal workstation default. Use unique port overrides and a disposable Compose project name when testing concurrently with another checkout.

Verification

just ci is the complete local and CI gate:

just ci

It runs direct pytest commands with a bounded worker topology: at most eight workers for unit/contract tests, four for PostgreSQL tests, and four for migration tests. Focused recipes are visible through just --list.

GitHub packages and dev delivery

GitHub delivery is bound to one full commit SHA. Pull requests run the repository gate and build validation without publishing images or receiving the dev environment. A main push, or a manual dispatch naming a full commit that is reachable from origin/main, follows the ordered graph verify -> publish-images -> deploy-dev. GitHub's setup-python action requests standard CPython 3.14; the repository continues to use uv's explicit 3.14+gil selector locally and in gate commands.

The private GHCR package names remain:

  • ghcr.io/choiec/cherrypick-api
  • ghcr.io/choiec/cherrypick-mcp
  • ghcr.io/choiec/cherrypick-db
  • ghcr.io/choiec/cherrypick-tunnel-client

API and MCP are two compatibility package names for one canonical application build and therefore publish the same manifest digest. DB and tunnel-client are built once each. Full Git SHA tags are traceability aliases; Kubernetes always consumes the exported sha256 digests, and the deploy job does not rebuild images. Publication uses the ephemeral GITHUB_TOKEN; the cluster pull token is read-only.

The deploy job is the only job that receives the dev environment and cluster credentials. It fails closed before mutation when required split Chat/Work configuration is absent or not distinct. The protected environment also pins the Proxmox SSH host public key and the K3s certificate-authority digest, so an unknown SSH endpoint or cluster is rejected before the first Kubernetes API request. The deployer then prepares backing services, waits for them, runs uniquely named Alembic and policy-seed jobs, applies application workloads, waits for every affected rollout, and runs existing-cluster observation. That smoke path does not install Helm or reconcile the steady-state overlay.

Repository implementation does not create GitHub environment values, change package visibility, publish live packages, or mutate a cluster. Complete the external prerequisites in docs/configuration.md before authorizing a live delivery.

If preflight, a backing service, migration, or policy seed fails, application workload digests remain unchanged. After a migration succeeds, recovery is an explicit operator decision using the recorded previous and new digest sets. The workflow never automatically downgrades the schema, deletes PVCs, or blindly rolls an image back.

MCP surfaces

CherryPick exposes three separately owned MCP surfaces:

  • chat is the short, interactive ChatGPT surface at /mcp/chat.
  • work is the durable, approval-aware ChatGPT Work surface at /mcp/work.
  • dev is the trusted local Codex stdio surface for repository work.

The production ASGI process mounts only chat and work. The dev server is registered by the project-local Codex configuration and is never mounted by the production process. Public deployments require the configured OAuth provider; production startup fails closed when it is absent.

Development MCP commands

just --list shows the thin repository recipes. The CLI and FastMCP adapters call the same application engine and return the same stable result envelope. The project-local Codex configuration registers that stdio server as dev. Start it manually or list its 15 coarse operations with:

just dev-tools
just dev-mcp
just dev core_get_work request.json

MCP request fields are top-level tool arguments; there is no nested arguments object. CLI invoke reads the identical argument object from the JSON file passed to --request-file. Both transports apply the same strict Pydantic model, reject unknown fields, and return cherrypick.harness.result.v1. Requests contain a UUIDv7 work_id, repository root, and operation-specific fields. Target-bound operations also carry the exact target digest and revision.

CLI process exit codes are part of that envelope: 0 means succeeded or replayed, 2 means blocked or invalid input, 3 means execution failed, and 4 means recovery is required. CLI parsing and validation failures write one cherrypick.harness.cli-error.v1 JSON object to stderr; they never print a traceback or a partial receipt.

Capabilities are user-issued outside MCP. Prepare a strict issue request with work_id, target, revision, action, scope, and an aware expires_at, then run:

PowerShell:

uv run python -m cherrypick.module.harness.adapter.cli capability-schema
uv run python -m cherrypick.module.harness.adapter.cli issue-capability `
  --request-file capability-request.json

Bash:

uv run python -m cherrypick.module.harness.adapter.cli capability-schema
uv run python -m cherrypick.module.harness.adapter.cli issue-capability \
  --request-file capability-request.json

The schema command is the authoritative, reference-free input contract. A minimal request has this shape (replace every bound value with current Core state):

{
  "root": ".",
  "work_id": "00000000-0000-7000-8000-000000000000",
  "expected_target_digest": "sha256:<64 lowercase hex characters>",
  "expected_revision": 0,
  "action": "change.authorize",
  "scope": ["src/**"],
  "expires_at": "2099-12-31T23:59:59Z"
}

The command requires an interactive TTY and exact challenge re-entry. Core persists the issuance and returns a target-, scope-, action-, revision-, and expiry-bound core.capability-receipt.v1. Pass that complete receipt as the capability field of the authorized CLI or MCP request. Review tools similarly hand off complete core.reviewer-attestation.v1 objects; each one binds a reviewer to the target, revision, review event, phase, and findings digest. A completion request selects only a tier; Core runs every registered check in that tier, so callers cannot submit a verification subset. An uncertain Git mutation must be reconciled and must not be blindly repeated.

Git recovery uses causally linked LIFO intent frames. Resolving a recovery command closes only that child frame and re-exposes its parent mutation; the parent requires its own capability and postcondition reconciliation. Abort is accepted only for the matching active Git operation, so a local recovery can never erase an unrelated or uncertain remote publish result.

The TTY challenge is a local operator-interaction boundary, not cryptographic proof of Codex UI or reviewer identity; a trusted issuer can strengthen that identity boundary later without changing the receipt shapes.

This structural proof and the bounded host command runner are development facilities. With ENVIRONMENT=prod, the shared composition installs deny-all providers: capability/reviewer operations require an externally signed TrustProvider, and verification or Git execution requires an injected isolated CommandRunner. There is no production fallback to TTY identity or host execution. Focused verification requests accept a registered verification_id, never raw argv; Core forces uv frozen, offline, no-sync execution and redirects tool caches and HOME to an OS-temporary directory.

Capability action and scope are exact, not prefix grants. Use change.authorize with the authorized path patterns and harness.improve with the changed paths. Branch scopes start with refs/heads/<expected-branch>; merge adds both git-ref/<source-branch> and git-oid/<expected-source-oid>, and publish adds remote/<remote>. Merge evidence must describe a clean source at that exact commit and tree; the command merges the immutable OID after checking that the named source still resolves to it. Every Git recovery adds command/<identity-without-sha256-prefix> and the current LIFO intent/<event-digest-without-sha256-prefix>; abort, rebase, and revert also add git-target/<target>. Their action names are branch.finish.<action> and git.recover.<action>, respectively. Keep and reconcile also require a capability even though they may only preserve or resolve state.

Repository contracts

  • Durable design contracts live under docs/specs.
  • The active skill roster is derived from the central harness registry and the eight skill directories; there is no separately maintained static catalog.
  • Skills contain instructions and UI metadata only; Python is owned by the shared FastMCP/CLI engine.
  • Work events and evidence use a work-local SQLModel/SQLite store.
  • CI enters through just ci and uses uv-managed Python with the bounded 8/4/4 unit/PostgreSQL/migration worker topology.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages