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:
105
symphony/CLAUDE.md
Normal file
105
symphony/CLAUDE.md
Normal 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).
|
||||||
69
symphony/agents/symphony-worker.md
Normal file
69
symphony/agents/symphony-worker.md
Normal 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.
|
||||||
17
symphony/commands/symphony-init.md
Normal file
17
symphony/commands/symphony-init.md
Normal 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.
|
||||||
20
symphony/commands/symphony-tick.md
Normal file
20
symphony/commands/symphony-tick.md
Normal 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.
|
||||||
125
symphony/skills/symphony-init/SKILL.md
Normal file
125
symphony/skills/symphony-init/SKILL.md
Normal 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.
|
||||||
124
symphony/skills/symphony/SKILL.md
Normal file
124
symphony/skills/symphony/SKILL.md
Normal 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.
|
||||||
66
symphony/templates/WORKFLOW.md
Normal file
66
symphony/templates/WORKFLOW.md
Normal 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.
|
||||||
44
symphony/templates/state.schema.json
Normal file
44
symphony/templates/state.schema.json
Normal 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" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user