Skip to main content

Provider-pool-aware multi-agent coding orchestration.

Project description

Turma

Provider-pool-aware multi-agent coding orchestration with spec-driven planning, Beads task tracking, and resumable swarm execution.

Status

Early implementation phase, but the core workflow is functional end to end. Five CLI commands ship:

  • turma init — local project scaffolding.
  • turma plan — the full author/critic loop with an explicit human approval gate and a resume CLI.
  • turma plan-to-beads — transcribes an approved plan into a feature-tagged Beads task set.
  • turma run — a single-feature sequential swarm orchestrator (preflight → reconcile → repair → main loop, one PR per Beads task).
  • turma status — a read-only readout of a feature's Beads + PR + worktree state.

The swarm drives each task through a pluggable worker backend — claude-code (default), codex, or opencode. A Gemini author backend exists, but its consumer gemini CLI path is being retired by Google, so a Gemini worker backend is deferred (see the Gemini CLI note in docs/architecture.md).

turma status, turma run, and turma plan each have a machine-readable --json mode — status/plan as snapshots (turma.status.v1 / turma.plan.v1), run as a turma.run.v1 NDJSON event stream — for scripts and surfaces. Baseline CI and public architecture documentation round it out.

What It Is

Turma is designed as a two-phase workflow:

  1. Planning: generate and refine OpenSpec artifacts through an author/critic loop with explicit human approval.
  2. Execution: translate approved tasks into a Beads DAG and route work across multiple agent runtimes while tracking task and integration state.

The main design goal is to treat provider rate-limit pools as a routing input without overstating that pool independence alone solves throughput.

Repository Layout

.
├── .github/workflows/          # minimal CI
├── .agents/                    # role guidance for author / critic / implementer / reviewer
├── .claude/commands/           # slash commands used in project context
├── openspec/                   # feature specs and changes
├── docs/
│   ├── architecture.md         # public system model
├── src/turma/                  # Python package and CLI
├── tests/                      # automated tests
├── CHANGELOG.md
├── LICENSE
├── README.md
├── turma.example.toml         # committed config template
└── pyproject.toml

CLI Commands

Default development workflow:

uv sync
uv run turma --help
uv run turma init
uv run turma plan --feature oauth-auth                    # author/critic loop; suspends at the human gate
uv run turma plan --feature oauth-auth --resume --approve  # approve: writes the APPROVED marker
uv run turma plan-to-beads --feature oauth-auth           # transcribe the approved plan into Beads tasks
uv run turma run --feature oauth-auth                     # orchestrate execution against the Beads DAG
uv run turma status --feature oauth-auth

turma run preflight refuses to start until the feature has an APPROVED marker (from the plan human gate) and a TRANSCRIBED.md (from turma plan-to-beads) — see the Resume CLI and Plan-to-Beads sections below.

Current command status:

  • turma init is implemented
  • turma plan runs the full author/critic loop with a human approval gate and a resume CLI
  • turma plan-to-beads transcribes an approved plan into a feature-tagged Beads task set (requires bd and Dolt; see Plan-to-Beads below)
  • turma run drives a single-feature sequential swarm against the transcribed Beads DAG (see Swarm Execution below)
  • turma status --feature <name> prints a read-only readout of a feature's current Beads + PR + worktree state (see Feature Status below)

turma init expects turma.example.toml to exist in the target directory. It creates turma.toml from that template and updates .gitignore with Turma-managed entries.

turma plan --feature <name> does the following per round:

  • reads planning.author_model and planning.critic_model from turma.toml
  • requires .agents/author.md and .agents/critic.md
  • on round 1, scaffolds an OpenSpec change with openspec and generates proposal.md, design.md, and tasks.md
  • on round ≥ 2, runs the two-call revision: author first writes response_{N-1}.md replying to each finding in critique_{N-1}.md, then regenerates the three artifacts using that response as context
  • runs the critic backend to produce a strict critique_N.md
  • routes on the critic's ## Status: token: blocking → revise, nits_only / approved → await human, malformed → needs_human_review
  • suspends at awaiting_human_approval with the exact resume commands printed

Loop budget: planning.max_rounds caps the iterations; repeated unresolved blocking finding IDs across two rounds also route to needs_human_review. Filesystem terminal markers (APPROVED, ABANDONED.md, NEEDS_HUMAN_REVIEW.md) are authoritative — re-running turma plan on an already-terminal plan is a read-only no-op.

Planning quality depends on the chosen backend/model. Claude-backed planning is currently the strongest validated path. OpenCode transport is validated, but provider/model quality varies. Gemini requires the gemini CLI (npm install -g @google/gemini-cli) — but Google is retiring that CLI: the consumer free / Pro / Ultra tiers stopped serving requests on 2026-06-18, and it continues only via paid Gemini / Gemini Enterprise Agent Platform API keys, Standard/Enterprise Gemini Code Assist licenses, or Gemini Code Assist for GitHub through Google Cloud. The replacement Antigravity CLI is not a drop-in, so Gemini authoring is supported only on those paid/enterprise paths pending that transition (announcement).

turma plan only produces and critiques spec artifacts; it does not commit changes or orchestrate execution. Transcription is handled by turma plan-to-beads and execution by turma run (see below).

Resume CLI

uv run turma plan --feature <name> --resume                           # read-only status
uv run turma plan --feature <name> --resume --approve                 # write APPROVED
uv run turma plan --feature <name> --resume --revise "<why>"          # advance into a new round
uv run turma plan --feature <name> --resume --abandon "<why>"         # write ABANDONED.md
uv run turma plan --feature <name> --resume --approve --override "<why>"  # override from needs_human_review

--approve, --revise, and --abandon are valid only when the graph is suspended at awaiting_human_approval. --approve --override is valid only when the graph has halted in needs_human_review.

Machine-readable output

turma plan --feature <name> --json (and any --resume variant with --json) emits a single turma.plan.v1 JSON snapshot of the planning outcome instead of text — for scripts and surfaces. plan runs to one terminal-or-suspended outcome per invocation, so this is a snapshot (like status --json), not an event stream:

{
  "schema": "turma.plan.v1",
  "feature": "oauth",
  "state": "awaiting_human_approval",
  "round": 2,
  "next_nodes": ["awaiting_human_approval"],
  "checkpoint": ".langgraph/oauth.sqlite",
  "artifacts_dir": "openspec/changes/oauth/",
  "action": null
}

state is one of awaiting_human_approval / approved / needs_revision / abandoned / needs_human_review; action is the resume action (approve / revise / abandon / override_approve / status) for --resume invocations, null for a fresh plan. stdout is exactly one JSON document — all per-round progress is suppressed in --json mode. On failure, a single {"schema": "turma.plan.v1", "error": "<message>"} object, exit nonzero. Absent the flag, text output is unchanged.

Together with status --json (snapshot) and run --json (NDJSON stream), this completes the machine-readable operator surface.

Plan-to-Beads

Once a plan has an APPROVED marker, turma plan-to-beads translates its tasks.md into a feature-tagged set of Beads tasks with typed entries, priorities, and dependency edges.

uv run turma plan-to-beads --feature <name>
uv run turma plan-to-beads --feature <name> --force

Prerequisites

bd (Beads) is a Go binary, not a PyPI package. Install it together with Dolt (Beads' storage backend):

brew install beads        # pulls Dolt and other required dependencies

turma plan-to-beads raises a clear error with the brew install beads hint when bd is not on PATH. See https://github.com/steveyegge/beads for non-macOS install paths.

Behavior

  • Gates on the APPROVED terminal marker; a plan that is not approved is rejected.
  • Parses tasks.md via the strict parser (see openspec/changes/beads-transcription/design.md for the grammar).
  • Translates parser task types to Beads-native types (impl/testtask, docschore, specdecision) and parser priority to Beads priority (min(section_number - 1, 4); 0 is highest).
  • Creates each section's Beads task with a feature:<name> label for downstream filtering, then adds bd dep add blocking edges from each section to its blocked-by predecessors.
  • On full success writes TRANSCRIBED.md in the change directory recording the created Beads task ids in section order.
  • Prints a compact summary of the created tasks on stdout.

--force semantics

  • With a prior TRANSCRIBED.md present: closes the recorded Beads task ids in reverse section order, removes the marker, and re-runs the pipeline.
  • With no marker but feature-tagged Beads orphans present (from a prior failed attempt): closes the orphans via bd list --label feature:<name> and re-runs the pipeline.
  • With neither a marker nor orphans: --force is a no-op and the pipeline runs normally.
  • A TRANSCRIBED.md that exists but parses to no - section N: <id> lines is hard-rejected under --force to avoid duplicate creation against a corrupt marker. Inspect or delete the file manually and retry.

Partial-failure recovery

Turma does not roll back partial Beads state on failure. If an adapter call fails mid-run, the already-created tasks remain on the Beads side with their feature:<name> label, and no TRANSCRIBED.md is written. A re-run without --force detects the feature-tagged orphans during preflight and surfaces their ids plus two recovery paths:

# Option A — manual close, then retry from scratch
bd close <id> <id> ...
uv run turma plan-to-beads --feature <name>

# Option B — let --force close the orphans for you
uv run turma plan-to-beads --feature <name> --force

Validation commands:

uv run turma --help
uv run python -m turma --help
uv run pytest

Swarm Execution

Once a feature has been transcribed to Beads, turma run drives a single-feature sequential execution loop that claims ready Beads tasks, runs a worker agent inside a per-task git worktree, opens one PR per completed task against the default base branch (main), and stops. Review, merge, and release are human-driven.

uv run turma run --feature <name>
uv run turma run --feature <name> --max-tasks 1       # smoke one task end-to-end
uv run turma run --feature <name> --backend claude-code
uv run turma run --feature <name> --backend codex     # drive Codex instead of Claude Code
uv run turma run --feature <name> --backend opencode  # drive OpenCode
uv run turma run --feature <name> --dry-run           # preflight + reconcile only

Worker backends are selectable via --backend (or [swarm] worker_backend); registered names are claude-code (default), codex, and opencode. The Codex worker drives codex exec in workspace-write mode; the OpenCode worker drives opencode run --dangerously-skip-permissions. All backends honor the same sentinel completion contract as Claude Code and share one subprocess/timeout/sentinel path, differing only in argv.

Machine-readable output

turma run --feature <name> --json emits a turma.run.v1 NDJSON event stream — one compact JSON object per line, flushed as each transition happens — instead of the text lines, for scripts and live surfaces (a VS Code extension, an MCP client, a dashboard):

uv run turma run --feature oauth-auth --json | while read -r line; do
  echo "$line" | jq -r '"\(.event) \(.task_id // "")"'
done

Every line is {"schema": "turma.run.v1", "event": "<name>", ...}. Events map 1:1 to the text lines — fetch_advanced, reconcile_summary, task_claimed, worktree_setup, worker_running, commit, push, task_opened, task_failed, merge_advancement, done, and so on. On failure the run still exits nonzero; events emitted before the failure remain valid records, and the terminal error is a final {"event": "error", "message": ...} line (in --json mode) rather than the error: <msg> text. Absent the flag, the text output is unchanged.

Config: turma run reads the [swarm] block from turma.toml for worker_backend, worker_timeout, max_retries, worktree_root, and base_branch. CLI flags take precedence — --backend overrides worker_backend, and --max-tasks is a per-invocation cap with no config equivalent. Missing or partial [swarm] blocks fall back to the defaults in turma.example.toml.

Prerequisites

  • bd (Beads) on PATH (brew install beads; see Plan-to-Beads above)
  • git on PATH
  • gh (GitHub CLI) on PATH with an authenticated session (gh auth login once; verified at startup via gh auth status)
  • the selected worker backend's CLI on PATH: claude (default claude-code), codex (--backend codex), or opencode (--backend opencode). Only the selected backend's CLI is required; --dry-run requires none because the worker is never invoked.
  • A transcribed feature: openspec/changes/<name>/APPROVED and openspec/changes/<name>/TRANSCRIBED.md must both exist. Missing either halts with a pointer back to turma plan or turma plan-to-beads.

Base-branch sync

Every non---dry-run invocation begins by fast-forwarding the local <base_branch> to origin — after preflight, before reconciliation. It is a three-call sequence: a git symbolic-ref --short HEAD precheck (refuses cleanly unless <base_branch> is the checked-out branch), then git fetch origin <base_branch>, then git merge --ff-only origin/<base_branch>. Divergent local history is refused rather than force-reconciled. Without this, chained features stall: when task A's PR merges between runs and the next turma run claims the dependent task B, B's worktree would otherwise be cut from a stale local base that lacks A's commits, and the worker would refuse to operate against the missing precondition.

The merge --ff-only step refuses to overwrite divergent local history — if local <base_branch> has commits that origin doesn't, the run halts with a typed PlanningError naming the branch and pointing at both triage commands, git log <base_branch>..origin/<base_branch> and git log origin/<base_branch>..<base_branch>, to compare directions. Operators triage manually rather than letting turma rebase or merge automatically.

--dry-run skips the fetch (a fast-forward mutates the local ref) and prints fetch: skipped (--dry-run) so the omission is explicit.

The one-feature loop

turma run's top-level state machine:

preflight → fetch_and_ff_base → reconcile → repair → merge_advancement → main_loop

main_loop runs, per ready Beads task:

claim → setup_worktree → run_worker → (sentinel) → commit → push → open_pr → mark_pr_open

mark_pr_open records the PR number on the Beads task via a turma-pr:<N> label and leaves the task in in_progress with its worktree on disk. The matching close_task + cleanup_worktree defer to the next turma run's merge advancement sweep (see below), which fires only after GitHub reports the PR as merged. This is what keeps dependents from being claimed against an unmerged base.

Failed steps enter the retry path via fail_task on the Beads task. A worker that claims success but leaves the worktree clean (.task_complete present but git status --porcelain empty) is treated as a failure with a canned reason so a non-editing worker cannot land an empty commit.

The worker signals completion via filesystem sentinels inside the worktree:

  • .task_complete — worker believes the task is done; orchestrator commits, pushes, opens a PR, and labels the Beads task with turma-pr:<N>. Close + cleanup defer to merge advancement.
  • .task_failed — worker hit an unresolvable blocker; contents are the failure reason. Orchestrator calls fail_task and leaves the worktree on disk for triage.
  • No sentinel after worker exit → failure with reason "worker exited without writing a completion marker".

Retry budget and halt conditions

Retry state lives on the Beads task:

  • turma-retries:<n> label — attempt counter, absent means zero.
  • needs_human_review label — added on budget exhaustion so list_ready_tasks filters the task out of future listings.

On failure, the orchestrator reads retries_so_far and calls fail_task(reason, retries_so_far, max_retries). Budget remaining → the task returns to open for a future re-attempt. Budget exhausted → the orchestrator halts the whole run so the operator can triage via bd list --label needs_human_review.

max_tasks caps the outer loop at N successfully-claimed tasks (claim races do not consume budget). Default is unbounded.

Reconciliation on resume

Reconciliation always runs at startup — including --dry-run — before the main loop. It walks the Beads in_progress set and classifies each task into one of six finding types based on the worktree filesystem and GitHub PR state:

Finding Cause Repair
missing-worktree Beads says in_progress, worktree absent release the claim (counts against the retry budget)
completion-pending .task_complete present, no open PR commit + push + open_pr + mark_pr_open
completion-pending-with-pr .task_complete present, PR already open mark_pr_open (no new PR; close + cleanup defer to merge advancement)
failure-pending .task_failed present fail_task with the worker's reason (worktree left for triage)
stale-no-sentinels worktree + branch exist, no sentinel halt before the main loop; operator decides
orphan-branch task/<feature>/* branch with no in_progress task log only; operator triage

Reconciliation itself is read-only: every mutation (fail_task, close_task, commit, push, gh pr create) is performed by the repair phase in the main loop, and --dry-run skips the repair phase entirely.

Merge advancement

Between the repair phase and the main loop, turma run sweeps every in_progress task that carries a turma-pr:<N> label and queries the PR's GitHub state via gh pr view <N> --json state. The dispatch is read-only-then-mutate — one gh read per labelled task, then exactly one of:

gh returns Action
MERGED unmark_pr_open + close_task + cleanup_worktree — dependents become claimable on the same turma run
OPEN leave alone (drafts return OPEN from --json state and fall through this branch unchanged)
CLOSED without merge fail_task with reason "PR #<N> closed without merge" so the retry budget applies
PR not found / 404 halt with PlanningError; the label is stale and the operator triages

Tasks without a turma-pr:<N> label are skipped (no gh call), matching the label-gated dispatch. --dry-run performs the PR-state reads but no mutations.

The sweep prints one line per processed task, prefixed with merge-advancement: so the source is unambiguous in the run log.

Failure modes (CLI)

error: <msg> starts with Cause
feature 'X' is not APPROVED no APPROVED marker; run turma plan
feature 'X' has not been transcribed no TRANSCRIBED.md; run turma plan-to-beads
bd CLI not found bd missing from PATH
gh CLI not found gh missing from PATH
gh session not authenticated run gh auth login
stale worktree for <id> has no sentinels reconcile caught ambiguous state; operator decides
retry budget exhausted on <id> task hit max_retries; triage with bd list --label needs_human_review

Worked example

Against a feature already transcribed to Beads (see Plan-to-Beads above):

# First run — opens a PR for task 1. The Beads task stays
# in_progress with a `turma-pr:<N>` label; the worktree at
# .worktrees/oauth-auth/<bd-id>/ stays on disk awaiting merge.
# A dependent task is NOT yet claimable.
uv run turma run --feature oauth-auth --max-tasks 1

# Reviewer merges the PR on GitHub (or via `gh pr merge <N>`).
# Nothing local needs to change — the merge is the only signal
# the orchestrator depends on.

# Second run — the merge advancement sweep observes MERGED,
# unmarks the label, closes the task, and removes the
# worktree. The dependent task is now ready and gets claimed
# in the same invocation.
uv run turma run --feature oauth-auth

# Operator triage after budget exhaustion.
bd list --label needs_human_review
bd show <id>

A detailed end-to-end smoke procedure against real bd + gh + claude lives in docs/smoke-turma-run.md.

Feature status

turma status --feature <name> prints a read-only readout of the feature's current Beads + GitHub PR + worktree state. The command never mutates anything — no claim_task, no close_task, no fail_task, no commits, no pushes, no gh pr create. Useful between turma run invocations to answer "where is this feature right now?"

uv run turma status --feature oauth-auth

The readout has six sections, in fixed order, each with a (none) placeholder when empty:

  • feature header — spec dir presence, APPROVED / TRANSCRIBED.md flags, with next-step hints (e.g. "run turma plan --feature ... first") inline when missing. A missing spec dir does not raise; the readout still prints.
  • task countersready / in_progress / blocked / deferred / closed / needs_human_review. Buckets are mutually exclusive: a task with the needs_human_review label is counted there regardless of its bd status; open tasks not in bd ready's view are counted as dependency-blocked.
  • ready tasks — claimable right now.
  • in-progress tasks — per-task: retries: <n> / <max>, worktree presence, sentinel state (complete | failed: "<reason>" | none). The .task_failed body is truncated to the first line in the readout; the full file stays on disk for triage. When the task carries a turma-pr:<N> label (recorded by the success path / repair phase), an extra pr: #<N> (<state>) <url> line is added below the sentinel — state and URL come from a live gh pr view, not the cached label, so MERGED PRs awaiting the next turma run sweep are visible here.
  • pull requests — every PR for task/<feature>/* head branches across all states (OPEN / MERGED / CLOSED / DRAFT).
  • orphan branches — local task branches whose Beads task isn't in_progress. Matches reconcile_feature's classification exactly; ready-task retry branches will appear here until the next turma run re-claims them.

Worked example against a feature mid-flight (synthetic; absolute paths and PR URLs depend on your repo):

$ uv run turma status --feature oauth-auth
feature: oauth-auth
  spec: openspec/changes/oauth-auth/
  approved: yes
  transcribed: yes

tasks:
  ready:              2
  in_progress:        1
  blocked / deferred: 1
  closed:             3
  needs_human_review: 0

ready tasks:
  bd-oauth-4 — Wire token refresh
  bd-oauth-5 — Add session expiry tests

in-progress tasks:
  bd-oauth-3 — Persist sessions in Redis
    retries: 0 / 1
    worktree: <repo>/.worktrees/oauth-auth/bd-oauth-3/ (present)
    sentinel: failed: "redis client connection refused"

pull requests:
  #14 OPEN — [impl] Token issuance endpoint
    head: task/oauth-auth/bd-oauth-1
    url:  https://github.com/your-org/your-repo/pull/14
  #13 MERGED — [impl] Add OAuth provider config
    head: task/oauth-auth/bd-oauth-2
    url:  https://github.com/your-org/your-repo/pull/13

orphan branches:
  (none)

Reading it: 3 closed (PR #13 already merged) + 1 in-progress (bd-oauth-3's worker wrote .task_failed with the first-line reason rendered inline; the full body is still on disk under the worktree for triage) + 2 ready + 1 dependency-blocked (counted in blocked / deferred). One PR is still open against the in-flight task; no branches without a corresponding active task, so orphan branches is (none).

Adapter failures (bd list non-zero exit, gh pr list non-zero exit, etc.) raise PlanningError and exit 1 with error: <msg> on stdout — no partial readout printed.

Machine-readable output

turma status --feature <name> --json emits the same state as a pretty-printed (indent=2) JSON object instead of text, for scripts and dashboards:

uv run turma status --feature oauth-auth --json | jq '.tasks'

The payload carries a stable top-level "schema": "turma.status.v1" and mirrors the text sections 1:1: feature, spec, tasks (counters), ready, in_progress, pull_requests, orphan_branches. Each in_progress entry has worktree { present, path }, a structured sentinel { status, reason } (statuscomplete / failed / null), and pr { number, state, url } (or null when the task carries no turma-pr: label). The same read-only / no-mutation invariant applies, and --json never emits a partial document. On failure it emits a single structured error object — {"schema": "turma.status.v1", "error": "<message>"}, exit 1 — so the surface stays parseable (matching plan --json; text mode still prints error: <msg>).

Core Docs

Next Implementation Steps

  • post-merge advancement: detect when a turma run-opened PR has been merged and unblock dependent Beads tasks automatically
  • parallel task execution + per-task backend routing (worker-backend:<id> labels)
  • Gemini worker implementation — deferred pending Google's Gemini CLI → Antigravity CLI transition (Codex and OpenCode are shipped)
  • a turma run --clean <feature> flag to bulk-remove failed worktrees and branches

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

turma-0.4.0.tar.gz (181.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

turma-0.4.0-py3-none-any.whl (92.6 kB view details)

Uploaded Python 3

File details

Details for the file turma-0.4.0.tar.gz.

File metadata

  • Download URL: turma-0.4.0.tar.gz
  • Upload date:
  • Size: 181.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.10.2 {"installer":{"name":"uv","version":"0.10.2","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for turma-0.4.0.tar.gz
Algorithm Hash digest
SHA256 46f66eaad3c7a77df04fdf6489859457698a4c8a5d945fbf324b4a6c8944d24f
MD5 5ed78714c08dd4079f7fed640989a771
BLAKE2b-256 caa516746428a82ad594cf9e41787c1b3df001f1236c10aab2f1e736202ac2ff

See more details on using hashes here.

File details

Details for the file turma-0.4.0-py3-none-any.whl.

File metadata

  • Download URL: turma-0.4.0-py3-none-any.whl
  • Upload date:
  • Size: 92.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.10.2 {"installer":{"name":"uv","version":"0.10.2","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for turma-0.4.0-py3-none-any.whl
Algorithm Hash digest
SHA256 8a17ac19c8ff36d54af6baab8313e8878e0d60da772e7c5cd0df2b1c4d058a42
MD5 4d1a2edf834254beb277a14285caf2a1
BLAKE2b-256 2f5e984bf56b532c882af54c38e3e7839d9dc4b51eec33dc037f42ab3eccda8e

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page