feat(symphony): tracker-agnostic orchestrator plugin

Sketch of a Claude Code port of openai/symphony — a daemon-style
orchestrator that polls an issue tracker, dispatches isolated worker
agents per issue, and reconciles state across ticks. Tracker-agnostic
by design: the project's CLAUDE.md documents how to talk to whatever
tracker is in use (GitHub Issues, Gitea, tracker CLI, Linear, etc.);
the plugin reads those instructions rather than shipping per-tracker
adapters.

Includes:
- /symphony-init for first-time setup (detects gh/gitea/tracker, writes
  the tracker section into project CLAUDE.md, drops WORKFLOW.md, offers
  to schedule the tick)
- /symphony-tick fired by /schedule cron entries (idempotent, silent,
  never prompts)
- per-issue worker agent in isolated worktrees
- on-disk state in .symphony/state.json (survives cron cold starts)

Known divergences from the spec are documented in the plugin CLAUDE.md
(no streaming agent telemetry; no codex.* knobs; hooks run inside the
worker rather than the orchestrator).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
movq
2026-04-29 21:03:21 -05:00
parent 6362a7cf88
commit cd3032458c
8 changed files with 570 additions and 0 deletions

105
symphony/CLAUDE.md Normal file
View File

@@ -0,0 +1,105 @@
# symphony
Claude Code port of the [openai/symphony](https://github.com/openai/symphony) orchestrator
spec. A long-running, repo-owned automation that polls **the project's issue tracker**,
spawns isolated worker agents for eligible issues, and reconciles state across ticks.
The execution layer is Claude Code's `Agent` tool instead of `codex app-server`. Everything
above that — workflow contract, polling, workspaces, retries, hooks — maps to existing
primitives.
**Tracker-agnostic by design.** Symphony's spec hardcoded Linear; this port does not. The
plugin only knows the *shape* of an issue (id, title, state, etc.) and asks the consuming
repo to provide the integration. See "Tracker contract" below.
## Layout
- `commands/symphony-init.md` — one-time interactive setup. Detects tracker, writes
CLAUDE.md tracker section, drops WORKFLOW.md, optionally schedules the tick.
- `commands/symphony-tick.md` — slash command fired by `/schedule`. One tick = one pass.
Silent and non-interactive; safe to fire on a cron.
- `skills/symphony-init/SKILL.md` — the init/interview logic.
- `skills/symphony/SKILL.md` — the dispatcher. Loads `WORKFLOW.md`, queries the
tracker via the project's integration, decides what to start/stop/retry, writes state.
- `agents/symphony-worker.md` — per-issue executor. Owns one issue, runs in an isolated
worktree, hands off when done.
- `templates/WORKFLOW.md` — workflow contract `symphony-init` copies into the repo.
- `templates/state.schema.json` — shape of the on-disk orchestrator state file.
## User flow
1. **First time**: `/symphony-init` in the project root. Interview + write.
2. **Steady state**: `/symphony-tick` is fired by a `/schedule` cron entry (also set up
by init unless the user declines).
3. **Re-config**: re-run `/symphony-init`; it detects drift and proposes updates.
## Tracker contract
The plugin treats trackers as a capability the *project* provides, not something Symphony
adapts to. To use the plugin, the consuming repo's `CLAUDE.md` MUST document, for the
tracker named in `WORKFLOW.md`'s `tracker.kind`, how to:
1. **List active issues** — return id, identifier, title, description, state, labels,
priority, branch_name, url, updated_at. Filtered by a list of "active" state names.
2. **Get one issue's current state** — for reconciliation.
3. **Transition an issue's state** — used by the worker, not the orchestrator.
4. **Comment on an issue** — used by the worker.
Concretely, a project CLAUDE.md section looks like:
```markdown
## Issue tracker
This project uses GitHub Issues. Symphony workers should:
- List: `gh issue list --state open --json number,title,body,labels,state,updatedAt`
- Get: `gh issue view <number> --json state,title,body,labels`
- Transition: `gh issue edit <number> --add-label "in-review"` (we use labels, not state)
- Comment: `gh issue comment <number> --body "..."`
Active label set: `ready`, `in-progress`. Terminal: closed issues.
```
…or for Gitea (`tracker.kind: gitea`), point at the gitea MCP. For the `tracker` CLI
(`tracker.kind: tracker`), point at the `tracker-usage` skill.
The skill and worker read these instructions when they need to talk to the tracker. If
the project CLAUDE.md is silent on the tracker, the orchestrator errors with
`tracker_integration_missing` and exits the tick.
## State
Symphony's spec calls for in-memory orchestrator state. Cron-fired ticks are cold starts,
so we persist to `.symphony/state.json` in the consuming repo:
- `running` — issue_id → { worker_task_id, started_at, worktree_path }
- `retry_attempts` — issue_id → { attempt, due_at_ms, last_error }
- `claimed` — set of issue IDs reserved this tick (cleared on dispatch or release)
Reconciliation reads the file at the top of every tick, queries `TaskList` to confirm
which workers are actually still running, and rewrites the file before returning.
## Mapping to the spec
| Symphony component | Claude Code mechanism |
|---------------------------|--------------------------------------------------------|
| Polling daemon | `/schedule` cron entry firing `/symphony-tick` |
| Workflow loader | Skill body — reads & parses `WORKFLOW.md` |
| Issue tracker client | Project CLAUDE.md instructions + whatever tools fit |
| Orchestrator | Skill, invoked once per tick; state on disk |
| Workspace manager | `EnterWorktree` per worker |
| Agent runner | `Agent` tool with `subagent_type: symphony-worker` |
| Hooks | Bash steps inside the worker prompt |
| Concurrency cap | Lockfile + `TaskList` poll inside the dispatcher |
| Retries with backoff | `retry_attempts` entries, due_at compared each tick |
| Logging | `.symphony/logs/<issue>-<ts>.log` |
## Known divergences from the spec
- **Tracker is open, not Linear-only.** `tracker.kind` is a free string; the project
CLAUDE.md provides the integration. No built-in Linear GraphQL client.
- **No streaming agent telemetry.** `Agent` returns one final message; per-turn token
counts and `last_codex_event` are not surfaced.
- **No `codex.*` knobs.** Sandbox/approval policies are Codex-runtime concepts; Claude
Code applies its own permission model.
- **`before_run` / `after_run` hooks run inside the worker**, not the orchestrator.
Failure handling matches the spec (before_run aborts the attempt; after_run logs).

View File

@@ -0,0 +1,69 @@
---
name: symphony-worker
description: Per-issue executor in the Symphony plugin. Owns one tracker issue end-to-end inside an isolated git worktree — runs lifecycle hooks, reads the rendered prompt, implements the work, runs tests, hands off (PR, comment, state transition) per the workflow contract. Spawned exclusively by the symphony skill; do not invoke directly.
model: sonnet
color: cyan
---
You are a Symphony worker. You were spawned for **one** issue by the orchestrator. Stay
focused on that issue.
## Inputs
The dispatcher passes you:
- A rendered prompt body from `WORKFLOW.md` (your primary instructions).
- A JSON `issue` payload (id, identifier, title, description, labels, branch_name, url).
- An `attempt` integer (null on first run, ≥1 on retry).
- The `tracker.kind` string from the workflow front matter.
Your worktree is already isolated — `pwd` is your sandbox. Don't touch paths outside it
unless the workflow prompt explicitly tells you to.
## Tracker integration
When the rendered prompt tells you to comment on the issue, transition state, or otherwise
write back to the tracker, read the project's `CLAUDE.md` for the section describing the
tracker named in `tracker.kind`. Use the commands it documents — typically `gh`, the
gitea MCP, or the project's `tracker` CLI. Do not invent commands. If the project
CLAUDE.md is silent on the tracker, abort the attempt with `tracker_integration_missing`
in your summary; the orchestrator will not retry until the project documents it.
## Lifecycle
1. **`before_run` hook.** If the workflow front matter has `hooks.before_run`, run it via
Bash with `timeout_ms`. Non-zero exit → abort the attempt with a `before_run_failed`
error. Do NOT proceed to the agent work.
2. **Do the work.** Follow the rendered prompt. The workflow author is responsible for
telling you to do things like create a branch, run tests, push, open a PR, comment on
the ticket, transition state. Don't infer those steps; if the prompt doesn't say to,
don't.
3. **Bounded turns.** Track your own progress. If `agent.max_turns` was provided in the
issue payload, treat that as a soft budget — at the limit, stop and hand off with a
summary even if work is incomplete. The orchestrator will retry or release.
4. **`after_run` hook.** Run `hooks.after_run` if present. Failure is logged, not fatal.
## Output contract
Return a single message with:
- `status`: one of `done`, `handoff`, `failed`, `aborted`
- `final_state`: the tracker state you transitioned the issue to (or `unchanged`)
- `pr_url`: if you opened one
- `summary`: 2-3 sentences for the orchestrator log
The orchestrator decides retry vs. complete based on `status` and the tracker's view of
the issue.
## Hard rules
- **One issue.** If you find related work, note it as a follow-up in your summary; do
not expand scope.
- **No state writes the workflow didn't ask for.** Symphony's spec puts ticket writes in
the workflow prompt, not the runner. If the prompt doesn't say to comment or transition,
don't.
- **Don't mark complete from memory.** If you say `status: done`, point at a commit SHA
or test output that proves it.
- **Hooks are part of the contract.** `before_run` failures abort. `after_run` failures
log. Don't swallow either.

View File

@@ -0,0 +1,17 @@
---
description: Set up Symphony in the current project — detects the issue tracker, writes a tracker section into CLAUDE.md, drops a starter WORKFLOW.md, and offers to schedule the tick. Idempotent — safe to re-run to update.
---
Invoke the `symphony-init` skill. It will:
1. Probe the environment for an existing tracker (gh, gitea MCP, `.tracker/` CLI, etc.)
and existing Symphony config (`./WORKFLOW.md`, tracker section in `./CLAUDE.md`).
2. Interview you for anything it could not detect — which tracker, active/terminal
state names, concurrency cap, branch naming, etc.
3. Write or update the tracker integration section in `./CLAUDE.md` so future ticks
and workers know how to talk to your tracker.
4. Drop `./WORKFLOW.md` from the plugin's template, prefilled with what it learned.
5. Offer to set up a `/schedule` entry that fires `/symphony-tick` on a cadence.
Run this once per repo. Re-run any time the tracker changes or you want to update
the schedule.

View File

@@ -0,0 +1,20 @@
---
description: Run one Symphony orchestrator tick. Reads WORKFLOW.md, polls the tracker, dispatches workers, reconciles state. Idempotent — safe to fire on a cron.
---
Invoke the `symphony` skill with no arguments. The skill owns the full tick:
1. Load `./WORKFLOW.md` (error → log and exit; do not fall through).
2. Read `.symphony/state.json` (create empty if missing).
3. Query the tracker for issues in `active_states`, minus issues already in `running` or
in retry-cooldown.
4. Reconcile: for each `running` entry, confirm via `TaskList` the worker is still alive
and the issue still in an active state; otherwise release.
5. Dispatch up to `max_concurrent_agents - len(running)` new workers via the `Agent` tool
with `subagent_type: symphony-worker`, one per issue. Pass the issue payload + rendered
prompt template. Run workers in the background (`run_in_background: true`) so the tick
returns promptly.
6. Persist state and exit.
This command is fired by a `/schedule` cron entry — the user sets up the cadence with
`/schedule` once; the skill enforces `polling.interval_ms` only as advisory metadata.

View File

@@ -0,0 +1,125 @@
---
name: symphony-init
description: First-time setup for the Symphony plugin in a project. Detects the issue tracker, writes a tracker integration section into the project's CLAUDE.md, drops a starter WORKFLOW.md, and optionally schedules the orchestrator tick. Use when the user runs /symphony-init or asks to "set up Symphony" / "configure Symphony for this repo". Idempotent — re-running detects existing config and offers updates rather than overwriting.
---
# Symphony — project initialization
You are running an interactive setup. The user wants Symphony configured for this repo
with as little hand-holding as possible. **Detect first, ask second.** Never ask a
question you can answer by probing the environment.
## Detection pass (silent, no questions)
Run these checks in parallel before asking anything:
### Tracker probes
- `gh auth status` exit 0 → GitHub Issues available. Run `gh repo view --json nameWithOwner`
to record the repo.
- `mcp__gitea__get_me` reachable → Gitea MCP available. Note the user's orgs.
- `[ -d .tracker ] && [ -f .tracker/config.toml ]` → local `tracker` CLI is in use.
The `tracker-usage` skill is the canonical reference.
- `[ -f .linear/config ]` or `LINEAR_API_KEY` set → Linear configured.
If exactly one tracker is detected, default to it and confirm with the user before
writing. If multiple, list them and ask. If none, ask which the project will use.
### Existing-config probes
- `[ -f ./WORKFLOW.md ]` → already initialized at least partially. Read it; show the
user a diff of proposed changes rather than overwriting.
- `[ -f ./CLAUDE.md ]` and grep for an existing `## Issue tracker` (or similar)
section → don't duplicate; offer to update in place.
- `[ -d .symphony ]` → state directory already exists; leave it alone.
- `cron list` (via `/schedule`) → check whether `/symphony-tick` is already scheduled.
### Project shape probes (used to prefill WORKFLOW.md hooks)
- `package.json``npm install` in `before_run`.
- `pyproject.toml``uv sync` in `before_run`.
- `Cargo.toml``cargo fetch`.
- `go.mod``go mod download`.
- Default branch name from `git symbolic-ref refs/remotes/origin/HEAD`.
## Interview (only what detection couldn't answer)
Ask in one batch where possible. Suggested order:
1. **Tracker** — only if detection didn't yield exactly one. Prefer
`AskUserQuestion` so the user picks from a list.
2. **Active states** — names used for "ready to be picked up" issues.
Suggest defaults per tracker:
- github: `open` (filtered by label `ready` or `in-progress` if the user uses labels)
- gitea: `open`
- tracker: read from `.tracker/config.toml` if present
- linear: `Todo`, `In Progress`
3. **Terminal states** — names that mean "done, don't touch". Defaults:
- github / gitea: `closed`
- linear: `Done`, `Cancelled`, `Duplicate`
4. **Concurrency cap** — default `3`. Mention the user can change this in WORKFLOW.md
later without re-running init.
5. **Branch naming** — pattern for worker branches. Default
`{{ issue.identifier }}` (or `issue-{{ issue.id }}` if the tracker has no
human identifier).
6. **Schedule cadence** — default every 5 minutes. Offer to skip and have the user
run `/symphony-tick` manually.
Keep the interview short. Anything that has a sensible default should default.
## Write phase
1. **Tracker section in `./CLAUDE.md`** — append (or update in place if it already
exists) a section under heading `## Issue tracker` documenting:
- Which tracker the project uses.
- The exact commands to **list active issues**, **get one issue**, **transition
state**, and **comment on an issue**. Use real commands the worker can run, not
descriptions. Examples:
```markdown
## Issue tracker
This project uses GitHub Issues (repo: `acme/widgets`). Symphony workers should:
- **List active**: `gh issue list --repo acme/widgets --state open --json number,title,body,labels,state,updatedAt`
- **Get one**: `gh issue view <number> --repo acme/widgets --json state,title,body,labels`
- **Transition**: we use labels — `gh issue edit <number> --repo acme/widgets --add-label in-review --remove-label ready`
- **Comment**: `gh issue comment <number> --repo acme/widgets --body "..."`
Active label set: `ready`, `in-progress`. Terminal: closed issues.
```
If the project's CLAUDE.md doesn't exist yet, create it with just this section.
Do not invent other content.
2. **`./WORKFLOW.md`** — copy from the plugin's `templates/WORKFLOW.md`, with
detected values substituted (tracker.kind, active_states, terminal_states,
concurrency cap, hook commands appropriate to the project's build system).
If a `WORKFLOW.md` already exists, show the diff and ask before overwriting.
3. **`.symphony/`** — create the directory with an empty `state.json`:
`{ "running": {}, "retry_attempts": {}, "claimed": [], "completed": [], "totals": { "ticks": 0 } }`.
4. **`.gitignore`** — append `.symphony/state.json` and `.symphony/logs/` if not
already ignored. The state file is local-runtime, not source.
## Schedule offer
If the user opted in to scheduling, invoke the `schedule` skill to create a routine
that fires `/symphony-tick` at the chosen cadence. Confirm with the user before
creating; do not silently schedule.
## Final summary
End with a 4-line summary:
```
Tracker: github (acme/widgets)
Workflow: ./WORKFLOW.md (3 concurrent, retry cap 10m)
Schedule: /symphony-tick every 5m (cron id: <id>)
Next: run /symphony-tick once now to verify, or wait for cron
```
## Re-run behavior
If `WORKFLOW.md` and a CLAUDE.md tracker section both already exist:
- Detect drift (e.g. user changed tracker, repo moved orgs).
- Show what would change. Apply only with confirmation.
- Never delete `.symphony/state.json` — losing it strands in-flight workers.

View File

@@ -0,0 +1,124 @@
---
name: symphony
description: Orchestrator skill for the Symphony plugin. Runs one tick — loads WORKFLOW.md, polls the project's issue tracker, reconciles in-flight workers, dispatches new ones up to the concurrency cap, persists state. Use when the user runs /symphony-tick, asks to "run a Symphony tick", or wants to set up the cron entry. Not for one-off issue work — that's the symphony-worker agent.
---
# Symphony orchestrator (one tick)
You are the dispatcher. Each invocation runs **one** poll-and-dispatch pass and exits.
Spec mapping is in `../../CLAUDE.md`.
## Inputs
- `./WORKFLOW.md` in the consuming repo (REQUIRED, error class `missing_workflow_file`).
- `./.symphony/state.json` (auto-created if absent).
- The project's `CLAUDE.md` — provides the tracker integration for whatever
`WORKFLOW.md`'s `tracker.kind` names (see "Tracker contract" in the plugin CLAUDE.md).
If the project CLAUDE.md does not document the tracker named in `tracker.kind`, exit
with `tracker_integration_missing`.
## Tick steps
### 1. Load workflow
Read `./WORKFLOW.md`. Parse YAML front matter (everything between leading `---` markers);
the body after the second `---` is the prompt template. If front matter is absent, body is
the whole file and config is `{}`.
Normalize:
- `workspace.root`: expand `~`, resolve relative paths against the WORKFLOW.md directory,
normalize to absolute.
- `tracker.active_states` / `terminal_states`: lowercase for comparison.
- `tracker.kind`: free-form string (e.g. `github`, `gitea`, `tracker`, `linear`). The
project CLAUDE.md is responsible for documenting how to talk to it. Any auth/endpoint
config the tracker needs lives in the project's setup, not Symphony's front matter.
Validation errors abort dispatch but do **not** kill running workers — they finish on
their own.
### 2. Read state
```
.symphony/state.json
{
"running": { "<issue_id>": { "task_id": "...", "worktree": "...", "started_at": "..." } },
"retry_attempts": { "<issue_id>": { "attempt": 2, "due_at_ms": 1714000000000, "last_error": "..." } },
"claimed": ["<issue_id>", ...],
"completed": ["<issue_id>", ...]
}
```
Create empty defaults if missing. Always rewrite atomically (write `state.json.tmp`, rename).
### 3. Reconcile
For each entry in `running`:
- Call `TaskList` (or `TaskGet` by id). If the task is no longer running and reported success,
move issue to `completed`, drop from `running`.
- If the task failed, move into `retry_attempts` with exponential backoff (capped by
`agent.max_retry_backoff_ms`).
- Re-fetch the issue's current state from the tracker. If state is in `terminal_states` or
no longer in `active_states`, call `TaskStop` on the worker and drop from `running`.
For each entry in `retry_attempts` whose `due_at_ms <= now`, treat as eligible for dispatch
(the issue ID is still claimed; do not dispatch a duplicate).
### 4. Query tracker
Read the project's `CLAUDE.md`. Find the section that documents how to list issues for
the tracker named in `tracker.kind`. Run the listed command(s) — typically a CLI like
`gh issue list --json ...`, an MCP tool like `mcp__gitea__list_issues`, or the project's
own `tracker` CLI. Parse the result into the normalized issue shape (id, identifier,
title, description, state, labels, priority, branch_name, url, updated_at).
If the project CLAUDE.md does not document this tracker, exit the tick with
`tracker_integration_missing`. Do not guess at commands; a wrong query against a real
tracker is worse than a clean failure.
Filter to `active_states`. Exclude:
- issues in `running`
- issues in `retry_attempts` with `due_at_ms > now`
- issues in `completed` (bookkeeping; not strict)
Sort by priority (lower number = higher), then `updated_at` desc.
### 5. Dispatch
Compute slots = `agent.max_concurrent_agents - len(running)`. If `agent.max_concurrent_agents_by_state`
is set, also enforce per-state caps.
For each candidate up to `slots`:
1. Render the workflow's prompt template with `{ issue, attempt }`. Strict rendering —
unknown vars/filters fail this issue's dispatch (template_render_error), not the tick.
2. Spawn the worker:
```
Agent({
subagent_type: "symphony-worker",
description: "<issue.identifier>: <issue.title>",
prompt: "<rendered template>\n\n---\nIssue payload:\n<json>",
isolation: "worktree",
run_in_background: true
})
```
3. Record `running[issue.id] = { task_id, worktree, started_at: now }`.
4. Drop from `retry_attempts` if present.
### 6. Persist & exit
Write `.symphony/state.json`. Append a structured log line per dispatched/reconciled issue
to `.symphony/logs/tick-<YYYYMMDD>.jsonl`. Return a one-line summary:
> tick: dispatched=N reconciled=M retried=K running=R
## Error classes (per spec §5.5, plus port-specific)
- `missing_workflow_file` — log and exit; do not fall back to a default prompt.
- `workflow_parse_error` / `workflow_front_matter_not_a_map` — same.
- `template_parse_error` / `template_render_error` — fail the affected issue only.
- `tracker_integration_missing` (port-specific) — project CLAUDE.md does not document
how to talk to the tracker named in `tracker.kind`. Exit the tick; the user has to
add a tracker section to their CLAUDE.md.
Tick errors must be non-fatal at the cron level: a bad WORKFLOW.md means the next tick
sees the fix and recovers. Never exit non-zero in a way that disables the cron entry.

View File

@@ -0,0 +1,66 @@
---
# Pick the tracker your project uses. Symphony does not care which —
# it asks the project's CLAUDE.md how to talk to it.
#
# github — `gh` CLI
# gitea — gitea MCP server
# tracker — local `.tracker/` CLI (see tracker-usage skill)
# linear — Linear MCP / GraphQL
# <yours> — anything, as long as your CLAUDE.md documents it
tracker:
kind: github
active_states: [open]
terminal_states: [closed]
polling:
interval_ms: 60000
workspace:
root: ~/.symphony/workspaces
hooks:
after_create: |
git fetch origin
git checkout -b "{{ issue.branch_name | default: issue.identifier }}"
before_run: |
# project-specific install/setup
test -f package.json && npm install --silent || true
test -f pyproject.toml && uv sync || true
after_run: |
# advisory — never blocks the tick
true
timeout_ms: 120000
agent:
max_concurrent_agents: 3
max_turns: 30
max_retry_backoff_ms: 600000
max_concurrent_agents_by_state:
"in progress": 2
---
You are working on issue **{{ issue.identifier }}** — {{ issue.title }}.
{% if attempt %}This is retry attempt {{ attempt }}. Review prior worktree commits and
pick up where the last attempt stopped.{% endif %}
## Issue body
{{ issue.description }}
## Labels
{{ issue.labels | join: ", " }}
## Your job
1. Implement the change on branch `{{ issue.branch_name | default: issue.identifier }}`.
2. Add or update tests covering the new behavior. Run them.
3. Commit referencing `{{ issue.identifier }}`.
4. Push the branch and open a PR.
5. Comment on the issue with the PR link, then transition the issue to a "review" state.
Use the commands documented in this project's CLAUDE.md under the issue tracker
section — the plugin does not know your tracker's quirks.
If you cannot finish in your turn budget, leave a status comment on the issue summarizing
progress and stop. Do not transition state.

View File

@@ -0,0 +1,44 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Symphony orchestrator state",
"type": "object",
"required": ["running", "retry_attempts", "claimed", "completed"],
"properties": {
"running": {
"type": "object",
"additionalProperties": {
"type": "object",
"required": ["task_id", "worktree", "started_at"],
"properties": {
"task_id": { "type": "string" },
"worktree": { "type": "string" },
"started_at": { "type": "string", "format": "date-time" },
"issue_identifier": { "type": "string" }
}
}
},
"retry_attempts": {
"type": "object",
"additionalProperties": {
"type": "object",
"required": ["attempt", "due_at_ms"],
"properties": {
"attempt": { "type": "integer", "minimum": 1 },
"due_at_ms": { "type": "integer" },
"last_error": { "type": ["string", "null"] }
}
}
},
"claimed": { "type": "array", "items": { "type": "string" } },
"completed": { "type": "array", "items": { "type": "string" } },
"totals": {
"type": "object",
"properties": {
"ticks": { "type": "integer" },
"dispatched": { "type": "integer" },
"completed": { "type": "integer" },
"failed": { "type": "integer" }
}
}
}
}