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:
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.
|
||||
Reference in New Issue
Block a user