Skip to content

InverterNetwork/quay

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

392 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Quay

Bun + TypeScript implementation of the Quay task lifecycle service.

Documentation

Start with the user-facing docs:

The older specs and design notes are still useful implementation history, but they have not been maintained all the way. Treat them as internal references, not as the current user contract:

Install

Quay ships as a single, statically-compiled Bun binary. No runtime is required on the target host — bun build --compile bundles the TypeScript source, all migration SQL, and the shipped default ticket_schema.toml into one executable. Release binaries also embed the production Quay UI bundle, so quay serve can serve the Admin UI without a separate packages/admin-ui/dist directory on the host.

# Build locally from a checkout (requires Bun ≥ 1.1):
bun install
bun run build         # → dist/quay (~58 MB)
./dist/quay --version # → e.g. 0.1.0+abcdef1 (or 0.1.0+abcdef1+dirty on an unclean tree)

Download a release binary

Quay is delivered as prebuilt binaries on tagged GitHub Releases. Each v* tag triggers .github/workflows/release.yml, which builds four artifacts and a SHA256SUMS manifest, then attaches them to the release. The release workflow builds the in-repo packages/admin-ui workspace first and embeds that dist/ output into each binary. No private cross-repo checkout or UI read token is required.

Artifacts published per release:

File Target
quay-linux-amd64 Linux x86_64
quay-linux-arm64 Linux aarch64
quay-darwin-amd64 macOS Intel
quay-darwin-arm64 macOS Apple Silicon
SHA256SUMS sha256 of every binary above

Stable URL pattern (used by the hermes-agent installer):

https://github.com/lafawnduh1966/quay/releases/download/<tag>/quay-<os>-<arch>
https://github.com/lafawnduh1966/quay/releases/download/<tag>/SHA256SUMS

Install on a Linux deployment box (linux-amd64 shown):

set -euo pipefail

TAG=v0.1.0
BASE=https://github.com/lafawnduh1966/quay/releases/download/${TAG}

curl -fsSL -o quay         "${BASE}/quay-linux-amd64"
curl -fsSL -o SHA256SUMS   "${BASE}/SHA256SUMS"

# Verify the download against the published manifest before installing.
# `set -e` above guarantees that a failing sha256sum check aborts the
# script before `install` runs, so an unverified binary never lands on
# the host.
grep " quay-linux-amd64$" SHA256SUMS | sed 's/quay-linux-amd64/quay/' \
  | sha256sum -c -

install -m 0755 quay /usr/local/bin/quay
quay --version    # → 0.1.0+<short-sha>

The binary is single-file and statically compiled; nothing else needs to be installed on the host (no Bun, no Node).

Verification policy

Releases ship checksums (SHA256SUMS) but are not GPG- or cosign-signed. Trust is anchored on (a) HTTPS to github.com and (b) the GitHub Actions workflow being the only producer of release artifacts (the release job has contents: write and uploads happen via gh release create on v* tag pushes — no manual uploads). Consumers, the hermes-agent installer in particular, MUST fetch and check SHA256SUMS against the binary they downloaded; a release tag without a matching SHA256SUMS entry is invalid and must be rejected.

First invocation creates ~/.quay/ (or $QUAY_DATA_DIR) and applies the embedded migrations. Migrations are idempotent; re-running quay is safe.

Upgrading

quay tick holds a supervisor lock at ${data_dir}/tick.lock for the duration of each pass. To roll a new binary forward:

  1. Stop the tick driver (cron entry, systemd timer, or whatever schedules quay tick).
  2. Replace the binary at its install path.
  3. Re-enable the driver. The next tick picks up the new version; the supervisor lock guarantees no overlap with an in-flight pass.

The binary embeds its build version (quay --version returns <package version>+<git short SHA>, or +<sha>+dirty if built from an unclean tree) so an operator on a remote box can confirm what's deployed.

Building from source for development

bun install            # also runs packages/cli/scripts/embed.ts (prepare hook)
bun test
bun run typecheck
bun run admin-ui:build # optional when iterating on UI assets
bun run quay -- task list # invoke the CLI from TS source

packages/cli/scripts/embed.ts regenerates packages/cli/src/build/embedded.generated.ts (the in-binary copy of migrations + shipped schema + version stamp + optional UI assets). The generated file is gitignored; bun install and bun run build both regenerate it. bun run build builds packages/admin-ui and embeds that local UI bundle into dist/quay. Set QUAY_UI_DIST_DIR=/path/to/dist to embed a specific UI build. Without a UI build, local CLI-only builds remain API-only unless quay serve --ui-dir <path> is used.

The old InverterNetwork/quay-ui repository has been migrated into packages/admin-ui. Keep that repository read-only with a pointer back here, or archive it once any remaining consumers have moved to this monorepo.

Bootstrapping a repo

Quay is a consumer of bare clones, not a manager. Before enqueuing tasks against a repo, two things have to happen:

  1. Register repo metadata with quay repo add:

    quay repo add \
      --id myrepo \
      --url git@github.com:owner/myrepo.git \
      --base-branch main \
      --package-manager bun \
      --install-cmd "bun install"
  2. Materialize the bare clone at the path quay expects. By default that's <data_dir>/repos/<repo_id>.git (i.e. ~/.quay/repos/myrepo.git). Override with repos_root in ~/.quay/config.toml to put the cache anywhere — e.g. a shared agent-wide directory:

    # ~/.quay/config.toml
    repos_root = "/Users/me/.acc/repos"

    Then clone:

    git clone --bare git@github.com:owner/myrepo.git <repos_root>/myrepo.git

    That's the entire contract — vanilla --bare, no extra git config step. If the clone is missing when you quay enqueue, the error prints the exact expected path and command.

CLI surface

quay tick                                  # one supervisor pass over the queue
quay enqueue --repo <id> --brief-file <p> [--request-pr-screenshots|--require-pr-screenshots]
                                           # legacy enqueue (operator-composed brief)
quay enqueue --linear-issue <ENG-1234>     # Linear-adapter enqueue (target repo
                                           # comes from the ticket's `repo:` field;
                                           # `--repo <id>` is an optional override;
                                           # accepts screenshot request/require flags)
quay review-pr --pr <owner/repo>:<num>     # enroll/poke synthetic/Quay PR review
quay validate-ticket [--ticket-json <p|->] [--schema-file <p>] [--quiet]
                                           # standalone validator: JSON in, JSON out

quay repo set-tags <repo_id> --namespace <n> --value <v>   # per-repo tag vocab CRUD
quay repo unset-tags <repo_id> --namespace <n> [--value <v>]
quay repo get-tags <repo_id>
quay repo apply-tags <repo_id> --from <path|->             # declarative replace
quay tags set-deployment --namespace <n> --value <v>       # deployment-wide vocab
quay tags unset-deployment --namespace <n> [--value <v>]
quay tags get-deployment
quay tags apply-deployment --from <path|->
quay tags import --from <path> [--force]                   # bootstrap from TOML
quay tags list --repo <repo_id>                            # merged vocab + enforced flag

quay handoff list [--status <s>] [--task <id>] [--include-ineligible]
                                               # durable awaiting-next-brief handoffs
                                               # (JSON; default status is eligible pending)
quay outbox list [--status <s>] [--handler-class <class>] [--task <id>]
quay outbox claim <outbox_item_id> [--claim-id <id>]
quay outbox complete <outbox_item_id> --claim-id <id>
quay outbox fail <outbox_item_id> --claim-id <id> --error <message>
                                               # shared side-effect outbox
quay task get <task_id> | task list        # read commands (deterministic JSON)
quay submit-brief | escalate-human | record-human-reply | cancel
quay artifact get <task_id> <kind>         # raw bytes to stdout

outbox_items is the shared durable outbox for Quay-originated side effects that Hermes delivers. Workflow/intervention rows are backed by the existing handoff flow and may claim/block/resume a task; delivery rows are notification-only work that can be claimed, completed, failed, and retried without changing task state. quay outbox list defaults to delivery rows, and generic outbox mutation commands reject workflow/intervention rows so delivery workers cannot consume task-resume handoffs. Quay enforces idempotency with idempotency_key, so Slack delivery does not need to dedupe duplicate Quay emissions.

The first concrete delivery kind is pr_ready_approved. Quay emits it after a Quay-owned task reaches done with the current PR head reviewed and approved, including both orders: reviewer approval before CI pass and CI pass before reviewer approval. Its payload contains task_id, external_ref, repo_id, pr_number, pr_url, pr_title when available, head_sha, review_id, review_attempt_id, branch_name, and approval_status (approved for the first notification, reapproved when a prior ready-approved delivery exists for an earlier head SHA). The route hint contains slack_thread_ref plus fallback deployment_default_slack_channel; Hermes should post to the recorded thread when present and otherwise use the deployment default Slack channel.

quay handoff list is the compatibility pull surface for workflow handoffs. It defaults to --status pending and hides pending rows whose next_eligible_at is still in the future; pass --include-ineligible to include cooled-down rows for inspection. Accepted statuses are pending, claimed, completed, and cancelled. Rows are JSON objects with handoff_id, task_id, reason, status, claim metadata, timestamps, next_eligible_at, state_event_id, idempotency_key, and payload_json.

quay validate-ticket skips the dispatcher's adapter wiring for fast spawns. It opens the Quay DB lazily — and only when a ticket payload's repo is registered with per-repo tag vocab — to enforce the layered (deployment + per-repo) tag namespaces. A missing data dir, an unconfigured repo, or a repo with no per-repo vocab degrades cleanly to "no enforcement" so validation keeps working before Quay has been initialized.

Configuration

Deployment config — ~/.quay/config.toml

Resolution order (first match wins): $QUAY_CONFIG_FILE, $QUAY_CONFIG_DIR/config.toml, $QUAY_DATA_DIR/config.toml, ~/.quay/config.toml. A missing file is OK (defaults apply); an unparseable or schema-invalid file is a hard error.

# Tick / supervisor knobs (see docs/user/configuration.md for the full set)
data_dir = "/var/lib/quay"
repos_root = "/Users/me/.acc/repos"     # bare-clone cache; defaults to ${data_dir}/repos
max_concurrent = 4
max_concurrent_reviewers = 2
retry_budget = 5
agent_invocation = "claude < {prompt_file}"

[reviewer]
enabled = false
gate_quay_owned_done = false
# login = "quay-bot"  # gh login tick matches posted reviews against; set when the worker authenticates as a different gh identity than tick. Defaults to `gh api user`.
# Preferred reviewer auth: export QUAY_REVIEWER_GH_TOKEN in the tick environment.
# gh_token_file = "/run/hermes/reviewer-gh-token"  # fallback mode 0600 file the reviewer pane reads as GH_TOKEN during migration.

# Linear/Slack adapters (see docs/user/linear-and-slack.md)
[adapters.linear]
enabled = true
api_key_env = "LINEAR_API_KEY"          # name of the env var holding the bot token
# auth_mode = "bearer"                  # OAuth/app-actor tokens use Authorization: Bearer <token>
# bearer_token_env = "QUAY_LINEAR_APP_TOKEN"
# token_command = "hermes-agent linear-token --actor app"

[adapters.slack]
enabled = true
bot_token_env = "SLACK_TOKEN"
# max_thread_messages = 200             # fetchThreadContext cap; default 200

[adapters.linear].enabled gates quay enqueue --linear-issue. Without it the dispatcher returns adapter_not_enabled. A configured-but-missing token (e.g. LINEAR_API_KEY unset) surfaces as adapter_not_configured. The default Linear auth mode keeps the legacy personal API-key behavior. Set auth_mode = "bearer" to send OAuth/app-actor tokens as Authorization: Bearer <token>; bearer_token_env reads a current token from the environment and token_command runs a non-interactive helper for each request.

Validator schema — ticket_schema.toml

Resolution order (first match wins): --schema-file <path>, $QUAY_CONFIG_DIR/ticket_schema.toml, $HOME/.quay/ticket_schema.toml, shipped default at packages/cli/config/ticket_schema.toml. The default mirrors the quay-config block used by the Linear adapter, so tag/author shape stays consistent across validation and enqueue.

Environment variables

Var Purpose
LINEAR_API_KEY Linear bot token (rename via [adapters.linear].api_key_env)
SLACK_TOKEN Slack bot token (rename via [adapters.slack].bot_token_env)
QUAY_DATA_DIR Override ~/.quay data root (DB, worktrees, artifacts, lock)
QUAY_CONFIG_DIR Override config + schema lookup directory
QUAY_CONFIG_FILE Direct path to a config TOML; bypasses dir resolution
QUAY_INTEGRATION_TESTS=1 Opt in to network-backed adapter tests

Quay review workflow secrets

.github/workflows/quay-review.yml calls Hermes POST /quay/review-pr after successful pull request CI runs so Quay can enroll or refresh synthetic PR reviews. Configure these repository secrets in GitHub Actions:

Secret Purpose
QUAY_REVIEW_URL Base Hermes URL, or the full /quay/review-pr endpoint
QUAY_REVIEW_PR_TOKEN Bearer token accepted by Hermes for Quay review requests

Layout

packages/
  cli/
    migrations/   SQL migration files (read at runtime, applied in lex order)
    config/       shipped defaults (ticket_schema.toml)
    src/
      cli/        entry point + dispatch; production wiring lives in cli/index.ts
      core/       service API: enqueue, tick_once, claim_task, ticket_context, ...
      adapters/   LinearAdapter, SlackAdapter, GitHub CLI, tmux, local git
      validator/  pure validateTicket library + TOML schema loader
      db/         sqlite connection + migration runner
      artifacts/  artifact-store helper
      ports/      interfaces: TmuxPort, GitPort, GitHubPort, SlackPort, LinearPort, Clock, ...
    tests/
      support/    harness, fakes, fixtures (test-only)
      schema/ enqueue/ tick/ claim/ cancel/ slack/ pr/ cli/ adapters/ validator/ ticket_context/ quay_config_block/
  admin-ui/       React/Vite Admin UI for the versioned /v1 Admin API
scripts/          repo-level driver scripts used by the historical slice gates

Tests are organized by domain. The full suite runs without network access; the two adapter integration tests (linear_adapter_integration.test.ts, slack_adapter_fetch_thread_context.test.ts) skip unless QUAY_INTEGRATION_TESTS=1 is set.

About

Quay v1: TDD task lifecycle service driven by gated slice loops

Resources

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages

  • TypeScript 99.1%
  • Other 0.9%