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