Context-free agents that write the documents around code — issues, pull request bodies, feature plans, code reviews and repo audits — each to a fixed set of rules, and each stamped with the versions that produced it.
Every agent here is spawned in isolation. It cannot narrate work it remembers doing, because it did not do any: it has to read the diff, the repo, or the issue. That is the whole idea, and it is why what comes out describes what is actually there.
| Agent | Writes |
|---|---|
issue-scribe |
One standalone issue from a spec |
pr-scribe |
A pull request body, from the real diff |
plan-scribe |
A feature plan in four stages, from reading the repo |
review-scribe |
A pull request review, drafted before it is posted |
audit-scribe |
An audit of a section of a repo, with no pull request open |
They share one list of checks in rules/review-checks.md, one writing style in
rules/writing-style.md, and one stamp generator that names the versions behind
every document.
Before a draft goes out, a second reader reads it with nothing else in front of
it. The reader says what each sentence means and flags any it had to read twice.
The scribe then rewrites those sentences, as rules/draft-reading.md sets out.
Every scribe does this before its draft is posted, filed, published or returned.
A plan's page is built by bin/render-plan.sh from the plan issue. Every plan
gets the same layout, from templates/plan-page.css, so no page is designed by
hand.
Three commands drive them: /feature-plan, /review and /audit.
Needs git, zsh, jq, and the GitHub CLI gh already authenticated.
The quickest way, with no clone:
claude plugin marketplace add DYB-Development/symphony
claude plugin install symphony@symphonyOr clone it, which lets you run either install and edit the rules in place:
git clone https://github.com/DYB-Development/symphony.git
cd symphony
./install.sh --helpThere are two ways to install from a clone, and you want one, not both. Installing both registers every hook twice, so whichever you run second refuses.
./install.shLinks rules, agents and bin into ~/.claude, links each command into
~/.claude/commands one file at a time, and merges the hooks into
~/.claude/settings.json. Anything already at one of those paths is moved aside
to <path>.backup first, and running it again changes nothing. Set
CLAUDE_CONFIG_DIR to install somewhere other than ~/.claude.
Commands are run as /review. A file edited in this clone takes effect in the
next session, with no reinstall, which is what makes this the mode to use while
changing the rules themselves.
./install.sh --pluginRegisters this clone as a marketplace and installs it. Commands are namespaced,
so a review is /symphony:review, and updates come through
claude plugin update rather than git pull.
Rules under ~/.claude/rules are read in every session on the machine, so the
writing style and the development process apply everywhere, not only here.
The commands are linked file by file rather than as a directory, so your own commands can live alongside these.
The plugin carries its hooks itself. A linked install merges the same entries into your settings, replacing any it put there before, and leaving hooks and settings that are not its own alone:
| Event | Runs | Why |
|---|---|---|
SessionStart |
writing-style-hook.sh |
Carries the writing rules and the banned phrase list into the session |
SubagentStart |
writing-style-hook.sh |
A subagent receives no rules of its own, so it gets them here |
PostToolUse |
decision-gate.sh arm |
Answering a question settles a choice, which has to be recorded |
PreToolUse |
main-clone-gate.sh check |
Refuses an edit or a branch-changing git command aimed at a repo's main clone, which is kept for its owner |
PreToolUse |
decision-gate.sh check |
Refuses a commit while that choice is still unrecorded |
PreToolUse |
agent-progress.sh record |
Logs the step a subagent marks and each script it runs |
SubagentStop |
agent-progress.sh record |
Logs that a subagent finished |
Stop |
turn-sound.sh play |
Plays a sound when a session finishes its turn |
Stop |
stray-test-workers.sh |
Stops any Rails test worker whose test run has gone, since nothing else will and it can run for days |
Notification |
turn-sound.sh play |
Plays the same sound when a session asks permission to run a tool |
Skip the hooks and the package still loads, but the writing rules never reach a subagent, the decision gate never fires, agents can work in the main clone, and no progress is logged, all without saying so.
To stop the sound in every open session, and to bring it back:
~/.claude/bin/turn-sound.sh off
~/.claude/bin/turn-sound.sh onIt plays with afplay, so it is silent on a machine without it.
To see which step each running scribe is on:
~/.claude/bin/agent-progress.shThe rules keep three working-tree files that must never reach a commit — the decision log, the ticket reference and the resume bookmark. Add them to your global gitignore:
.decisions.md
.ticket
start_here.md
Then, once per repo you use the issue schema in, create its labels:
~/.claude/bin/issue-bootstrap.shThree commands start the work. Each one spawns exactly one scribe, and the scribe reads the repo rather than the conversation.
| Run | What happens |
|---|---|
/feature-plan |
A request too big for one issue becomes a plan in four stages, filed as an issue with its units already written as issue bodies |
/review <n> |
A pull request is reviewed against every check, drafted for you to read, and posted only when you say so |
/audit |
One named section of a repo is measured and scored, and nothing is posted or filed |
The other two scribes are spawned by name when you need them: issue-scribe
writes one standalone issue from a spec, and pr-scribe writes a pull request
body from the real diff. Ask for either and one is spawned.
Three files appear in a repo as you work, and none of them should be committed:
| File | Holds |
|---|---|
.decisions.md |
A choice settled while working, which the pull request scribe renders into the body |
.ticket |
The ticket the branch's hours are billed to |
start_here.md |
Where to pick up, written only when stopping mid-issue |
Two things worth knowing. Nothing posts without being read first — a review is drafted and rendered for a person, and an audit posts nothing at all. Nothing merges or approves — a review is always a comment.
One workflow in this repo releases every gem, so no gem holds a copy of the release steps and no release needs an API key typed in.
.github/workflows/gem-release.yml is called by a gem rather than run on its
own. Registering it against a gem on rubygems.org lets GitHub hand RubyGems a
short-lived token for that one run, which is what replaces the key.
Run the setup script against the gem's working copy:
~/.claude/bin/gem-release-setup.sh ~/projects/gems/tally
It writes .github/workflows/release.yml into the gem and prints the trusted
publisher to register, field by field. Register it at the address the script
prints, commit the workflow, and the gem is wired.
The fields matter in one non-obvious way: the workflow filename RubyGems wants
is gem-release.yml, the workflow in this repo, not release.yml, the one in
the gem. The token names the workflow that actually ran, and that is this one.
Raise the version in the gem's version.rb, open a pull request, and merge it.
The workflow reads the gemspec on every push to main, compares the version
with rubygems.org, and releases when it is higher. A push that does not change
the version is a run that says there is nothing to release and stops.
The release builds the gem, pushes it to rubygems.org, and then tags the commit it was built from. Pushing first is deliberate: a rejected gem leaves no tag behind, so the next run is a clean retry. Nothing is done by hand and nothing is typed in.
The same run can be started from the gem's Actions tab, under Release, with Run workflow. It reads the gemspec and compares it with rubygems.org exactly as a merge does, so a version already published is still a run that stops. Reach for it when a release failed and the fix was somewhere other than the gem, or when a version was merged before the gem was wired to the workflow.
Before publishing, every check runs and one run names every problem it found rather than the first. It refuses a version whose tag already exists, a gemspec Ruby cannot read, and a push host trusted publishing cannot authenticate against.
A failed release opens an issue in the gem's own repository, labelled
release-failure, carrying what the release printed before it stopped. A second failure of the same
version comments on that issue rather than opening another.
Everything works unset. These change what the package reads, and they belong in your own shell or settings rather than in a file here, so an update never overwrites them.
| Variable | Changes |
|---|---|
SYMPHONY_OVERLAY_DIR |
Where your own rules files are read from, instead of ~/.config/symphony/rules |
CLAUDE_CONFIG_DIR |
Where the installer links to, instead of ~/.claude |
CLAUDE_WRITING_STYLE_FILE |
One writing rules file, ahead of the overlay and the shipped one |
CLAUDE_BANNED_PHRASES_FILE |
One phrase list, on the same terms |
AUDIT_RUBRIC_FILE |
The cost bands and horizons an audit is scored against |
ISSUE_BOOTSTRAP_OWNERS |
The accounts issue-bootstrap.sh --all-repos syncs labels across |
SYMPHONY_IDENTIFIERS |
A file of terms no shipped file may name, checked by the suite |
READ_DRAFT_SMOKE |
Set to 1 and the suite runs one real read through claude |
RENDER_PLAN_SMOKE |
Set to a plan issue as owner/repo#N and the suite renders it through GitHub |
ISSUE_BOOTSTRAP_OWNERS is empty by default and --all-repos does nothing until
you set it, because that flag writes to every non-archived repo of every account
named.
SYMPHONY_IDENTIFIERS is unset by default, so the suite reports nothing to check
and passes. A list of names cannot be kept here without carrying the names the
check exists to keep out.
Every rules file this package ships can be replaced without editing this package,
so claude plugin update and git pull never overwrite your version.
Put a file of the same name in ~/.config/symphony/rules/ and it wins:
mkdir -p ~/.config/symphony/rules
cp rules/pr-body.md ~/.config/symphony/rules/pr-body.mdEdit that copy. Every session and every scribe is told at startup that the overlay exists and that a file in it replaces the shipped one, so the pull request scribe now follows your template and everything else follows the rules here.
| To change | Replace |
|---|---|
| How a pull request body reads | pr-body.md |
| What an issue must contain, and its labels | issue-schema.md |
| What a review looks for | review-checks.md |
| How a review is written and posted | pr-review.md |
| What an audit measures and reports | repo-audit.md |
| What an audit's findings cost | audit-rubric.json |
| The shape of a feature plan | feature-plan.md |
| How everything is written | writing-style.md |
| Who a draft is read for before it goes out | draft-reading.md |
| The phrases nothing may use | banned-phrases.txt |
| The development process followed | develop_process_rules.md |
| When a decision is recorded | decision-log.md |
Replace a whole file, not part of one — the overlay swaps files, it does not merge them. Take a copy of the shipped one and edit it, so nothing a scribe expects to find goes missing.
Set SYMPHONY_OVERLAY_DIR to keep the overlay somewhere else, such as a
directory your team shares.
./run_tests.shEvery file named *_test.zsh anywhere under the repo root is a suite and is
picked up with no registration.
No suite calls the network by default. READ_DRAFT_SMOKE=1 ./run_tests.sh also
sends one sentence to a real reader, which needs claude logged in and is billed
like any other run. RENDER_PLAN_SMOKE=owner/repo#N ./run_tests.sh renders that
plan issue's page through GitHub's markdown service.
MIT. See LICENSE.