symphony-workflow-ops
Skill0 starsChanged 33 days agoMITView on GitHub
Use when working with a plan or a ticket through symphony-workflow. (1) A plan kept as Markdown (docs/epics/*.md, an Athena-style workflow): preview it as a board or task list, propose To-Dos for tasks that have none, push it to Linear or Jira or keep it as Markdown, or keep a board's status in step with the plan, both ways. Trigger on: "sync my plan", "put my epics on a board", "plan to Linear", "plan to Jira", "show my plan as tasks", "propose To-Dos", "keep the board in step", "profile this project". (2) Operating the tool itself: start the API server, drive a Jira or Linear ticket through Plan then Implement, read poller status, pick up an ADLC ticket by hand. Always read the project first (`profile`, then a `--dry-run`); never write to a tracker without the user's go-ahead. For the why/design behind these mechanics, read docs/{intent,spec,plan}.md — this skill is the short operational reference, not the design doc.
- Reads credentials
The file
We split this skill into the rules it gives an agent
12 rules. About 0 are common, 3 are distinctive, and 9 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈3
Distinctive · estimate
This author's own choices. Worth a look.
9
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 26
Before assuming the API server is down (and definitely before launching a new one), check first: curl -s http://127.0.0.1:8811/health.
- Very specific: line 102
When a Subtask finishes, symphony-workflow rollup <subtask-key> does the bookkeeping the rule above implies: Subtask Done → Story Done if …
- Very specific: line 126
If a bulk sync-decomposition pass is retroactively documenting already-*shipped* work (not real forward-looking planning), pass --status …
- Very specific: line 175
Use a throwaway object (a Linear project with a name that does not exist yet, a Jira tree in a project you own).
- Very specific: line 176
Never run against the owner's existing project: its hand-made issues have no sd-id: marker and would be duplicated.
- Distinctive: line 179
Run the whole loop: create, re-run (nothing created or updated), pull, push, conflict.
- Very specific: line 185
Every workspace's branch is {ISSUE-KEY}-{slug-of-summary} (built by workspace.py's branch_name()), e.g.
- Distinctive: line 189
If you're doing manual dev work on a ticket outside the automated Plan/Implement path, use the same pattern by hand:
- Very specific: line 207
Workspace isolation: every agent dispatch runs only inside a validated per-issue git worktree under one root (workspace.py's _validate) — …
- Distinctive: line 221
Run's Implement path is gone.
- Very specific: line 228
Each new worktree needs its own
uv sync --extra dev: before its tests will run — worktrees don't share a venv with the main checkout. - Very specific: line 267
Check the install for real: after a plugin change, without touching your own Claude config: CLAUDE_CONFIG_DIR=<scratch> claude plugin …
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
symphony-workflow operations
Read this before running anything in this repo for the first time. For the
why (Symphony, the ADLC decision log, findings from real runs) see
docs/{intent,spec,plan}.md — this file stays short on purpose and doesn't
duplicate them.
Session start — check before you start a server
Before assuming the API server is down (and definitely before launching a new
one), check first: curl -s http://127.0.0.1:8811/health. A prior session may
have left it running in the background; starting a second instance on the same
port just errors, and starting one against a different .env/target repo while
another is already polling is a real way to get two pollers double-dispatching
Plans. If /health doesn't respond, then start it as below.
Quick start
uv sync --extra dev
uv run symphony-workflow init-env # creates .env (gitignored) with empty keys; fill in JIRA_BASE_URL / JIRA_EMAIL / JIRA_API_TOKEN
uv run symphony-workflow check-env --verify # names missing keys, one read-only call; never prints a value
uv run --env-file .env uvicorn symphony_workflow.api:app --port 8811GET /health reports tracker connectivity and whether the poller is running.
Note: config.py reads os.environ directly, no python-dotenv — plain uv
run symphony-workflow ... will NOT pick up .env and fails with "Missing required
env var(s)". Either pass --env-file .env to uv run (confirmed working,
uv 0.12.3+) or export the required vars into the shell yourself.
The four ways to drive a ticket
- Manual, one ticket:
POST /runs {issue_key}→POST /runs/{id}/plan→ read the plan, it's a real judgment call, not a formality →POST /runs/{id}/implement.GET /issues/{key}/runfinds a Run's id from the issue key (needed after auto-discovery, since nothing else returns it). - Auto-discovery (the poller): any ticket with
status = "To Do"on the configured Jira project gets auto-Planned — no manualPOST /runsneeded. It never auto-Implements. That's not a missing feature, it's the whole point (seedocs/plans/0001-symphony-poc/spec.md's Phase 5 section for why) — don't "complete" it by wiring Implement into the poller. - CLI tool-call passthrough:
symphony-workflow call <tool> '<json args>'orsymphony-workflow list-tools— the same registry the API's/toolsexposes, for scripting outside the server. - CLI-native control plane (no server needed):
symphony-workflow plan <key>/symphony-workflow implement <key>run the same Plan/Implement loop as way 1 in-process, viaorchestrator.py(ADLC-45) —RunStoreis per-process here, soplansaves itsRuntodocs/runs/<key>/run.jsonfor a laterimplementto reload.symphony-workflow decompose <key>(ADLC-46) breaks a Jira Epic into Stories/Tasks for human review — read-only, no Jira writes, rendersdocs/runs/<key>/decomposition.{md,json}.symphony-workflow sync-decomposition <key>(ADLC-47) turns an already-revieweddecomposition.jsoninto real Jira Story/Subtask issues; it does not re-run the decompose agent. Seedocs/plans/0001-symphony-poc/spec.md's "CLI control plane" section for design detail.
Tracker backend — Jira or Linear
SYMPHONY_WORKFLOW_TRACKER=jira (default) or linear picks the backend for ways 1/2/4 above
(get/comment/transition/plan/implement, the poller, /health, /issues/{key},
/runs/*). Not tracker-agnostic — stay Jira-only regardless of the env var: search (raw
JQL, no Linear equivalent), call/list-tools (tools.py's field set — Epic/parent linking,
sprint/story-points — is Jira-shaped), decompose/sync-decomposition. api.py's /tools and
/tools/{name}/call return 501 when the configured backend isn't Jira, rather than silently
misrouting. See ADLC-57 / docs/plans/0005-linear-tracker-backend/intent.md's non-goals for why
those don't have a Linear equivalent yet.
Linear structure mapping (ADLC-87, confirmed against Linear's live GraphQL schema): don't
map Jira's Project→Epic→Story→Task onto Linear Team→Project→Issue — Linear Team count is capped
by plan (this workspace: 2, already at GAI+CLO), so "one Team per repo" can't scale. Use
instead: Linear Project = one repo, ProjectMilestone (inside that Project) = Epic,
Issue = Story, sub-issue (parentId) = Task. Full detail + English/繁體中文 write-up in
README.md's "Recommended Linear structure" section.
Epic decomposition — required, not optional
Never build directly under an Epic ticket. symphony-workflow decompose <epic-key> (read-only,
writes docs/runs/<epic>/decomposition.md for review) then symphony-workflow sync-decomposition
<epic-key> (materializes the reviewed decomposition as real Story/Subtask issues,
parent=<epic-key>) exist exactly for this — ADLC-46/47. Work happens against the
Stories/Subtasks, each transitioned as it completes; the Epic itself only transitions to Done once
its children are. Real incident this rule fixes (ADLC-86): ADLC-57 (an Epic) shipped
end-to-end with zero children — caught only after the fact, then retroactively decomposed. If
you're about to create_issue an Epic and start writing code, stop and decompose first instead.
When a Subtask finishes, symphony-workflow rollup <subtask-key> does the bookkeeping the rule
above implies: Subtask Done → Story Done if all its Subtasks are → Epic Done if all its Stories
are (forward-only, idempotent, --dry-run first if unsure; exit 2 = key not found, 3 = Jira error
with the partial work printed). Jira-only.
To check a project's existing cards (not an Epic you are about to build): symphony-workflow audit <PROJECT> --open
(read-only; exit 1 on a Story with no Subtasks, a Story under no Epic, an Epic with no Stories, or a Done Story
with open Subtasks; --assignee-me also requires everything assigned to the token's user). Fix with
symphony-workflow add-subtasks <plan.json> --dry-run, then without --dry-run (idempotent). Rules and naming:
docs/JIRA_HIERARCHY.md. Jira-only.
A new repo gets its own Jira project (docs/PORTABILITY.md): symphony-workflow create-project <KEY> --name "..."
--dry-run, then without --dry-run (also the create_project tool; needs Jira ADMINISTER). Team-managed by default
(--style company for classic); the token's user is the lead; a key you already lead prints "already exists" (exit 0),
one someone else leads is refused (exit 1). It reads the project back and warns when there is no Subtask issue type
(company-managed calls it Sub-task; pass that as subtask_type to the Jira sink). Never raw REST for this any more.
Filing many cards at once (follow-ups from a review, findings from a run): never loop create_issue. Write a manifest
and run symphony-workflow file-cards cards.json (a dry run; --apply writes, --write-keys stores the new keys). Each
item is tied to its card by a fu-<id> label, so a rerun creates nothing new and two cards on one label is an error; an item
with key adopts a hand-made card (label added, nothing created); every card is read back by key and a mismatch exits 1;
status only moves forward and a Done card is never reopened; a bad manifest exits 2 before any call. One card at a time:
create PROJECT "Summary" --identity-label fu-x and update KEY --add-label fu-x (labels are added, never replaced). Jira-only.
If a bulk sync-decomposition pass is retroactively documenting already-shipped work (not real
forward-looking planning), pass --status <name> (ADLC-121) — otherwise the newly-created
Stories sit in Jira's default "To Do" and the poller races your own status fix-up with a real
auto-Plan run (confirmed live, twice: ADLC-64, ADLC-89).
Content drift (ADLC-146): sync-decomposition creates once; afterwards symphony-workflow
diff-decomposition <epic> [--adopt] [--json] (read-only; exit 0 in sync, 1 drift, 2 bad input, 3 Jira
error) compares docs/runs/<epic>/decomposition.json with the live Epic -> Story -> Subtask tree
(missing / summary-changed / description-changed / orphan; status, comments and links are not drift).
sync-decomposition <epic> --update [--dry-run] [--adopt] [--yes] [--status NAME] applies the safe part:
create missing, update changed, never delete, idempotent, stops at the first Jira error. Cards are tied to
spec entries by a stable id in the JSON and a trailing sd-id: <id> line in the Jira description; plain
sync-decomposition now refuses an Epic that already has children. Jira-only.
Plan sync — a plan into Linear or Markdown (plan 0008)
For a project that keeps its plan as epics-as-markdown files (an /athena:*-style workflow, or a model-written plan
saved as docs/epics/e<N>-*.md), the plan can be pushed to a tracker or kept as Markdown. Three layers everywhere:
Category (Linear milestone, Markdown index row), Task (Linear issue, one .md file), To-Do (sub-issue, checkbox).
Target shape is about 1 : 3 : 9. None of this touches a tracker unless you pass --to linear.
profile [--path] [--json] [--write]reads how the project works (plan files, credential key names, issue keys in git history) and prints two recommendations;--writesavesdocs/context/plan-sync.jsonafter asking. Read-only.import-planshows the plan and the pyramid warnings.sync-plan --to markdownwrites Athena-compatible files (idempotent, only managedsd:beginblocks, never deletes); with no config it prints the recommendation and stops.- A Task with no breakdown of its own gets To-Dos from
todo_source:split-todosproposes them with one read-only agent turn intodocs/context/todo-proposal.{json,md}, you edit,split-todos --approveunlocks them; otherwise the five steps (Spec, Implement, Tests and QA, Commit, Merge). Nothing reaches a tracker before the approval. sync-plan --to linear [--project NAME] [--dry-run]creates the Project, milestones, issues and sub-issues (ansd-id:line makes a re-run update in place; hand-made issues without the marker are ignored, so a re-run against a project built another way duplicates them: use a fresh--project).sync-plan --to jira --project KEY [--dry-run]writes Category = Epic, Task = Story, To-Do = Subtask (the samesd-id:marker; the issue keys it made are kept indocs/context/sync-state.jsonbecause a JQL child list lags a create).sync-status --to linear|jira [--direction both|push|pull] [--dry-run]moves To-Do status both ways; a change on both sides is a conflict, reported and left alone (exit 1). The progress matrix is not written back. For Jira runsync-plan --to jirafirst: it only looks at the cards in that key map.- Both sinks were run against the real services on a throwaway project (and deleted afterwards), but still start with
--dry-run. Git is only ever run throughsafepath.run_git. - With
todo_sourcehybrid,sync-planwaits for a pendingsplit-todosproposal instead of writing the five steps (they would read as the author's own); a project with no phase table is grouped in eights (unphased,unphased-2, ...);status_mapindocs/context/plan-sync.jsonmaps a Linear or Jira status name to a plan status.
Prove a change for real, not only mocked
A mock cannot show how a real service reshapes what you send. Running the Linear sink, the Jira sink and
split-todos once for real found four bugs mocks never could: Linear puts a blank line before a list, so every
Task looked changed on a re-run; a Task moved while one of its To-Dos was in conflict; a freely worded tests item got a
duplicate; a key map naming someone else's ticket was trusted. So for any change to a sink or the agent step:
- Use a throwaway object (a Linear project with a name that does not exist yet, a Jira tree in a project you own).
Never run against the owner's existing project: its hand-made issues have no
sd-id:marker and would be duplicated. - Record what exists before (for Jira the highest key number) and delete only what the run created afterwards,
in a
finally, then read back that it is gone. - Run the whole loop: create, re-run (nothing created or updated), pull, push, conflict. Report what was created and deleted, one line each.
- Credentials come from the project's
.env; never print them, and say so if one was pasted into a chat.
Branch naming — required, not optional
Every workspace's branch is {ISSUE-KEY}-{slug-of-summary} (built by
workspace.py's branch_name()), e.g.
ADLC-24-poller-live-verify-add-a-version-constant. The branch must start
with the issue key — Jira's Development-panel auto-linking depends on it.
If you're doing manual dev work on a ticket outside the automated Plan/Implement
path, use the same pattern by hand:
git checkout -b ADLC-24-poller-live-verify-add-a-version-constantConfirmed 2026-09-24: the GitHub-for-Jira app is now installed and connected
(ADLC-30, Done) — repository.byInstanceType is populated where it used to be
{}. Branch-level linking is still unverified, though: nothing has actually
pushed a correctly-named branch to GitHub since the app connected (Plan-phase
worktrees never commit), so branch.byInstanceType hasn't been exercised
end-to-end yet. Don't assume the Development panel populates until a real push
confirms it.
Safety invariants — don't "helpfully" remove these
- The Plan/Implement human gate is enforced in
RunStore(RunTransitionError), not just in the API layer — Implement is unreachable without aplan_readyRun, on purpose. - Workspace isolation: every agent dispatch runs only inside a validated
per-issue git worktree under one root (
workspace.py's_validate) — Symphony §9.5's invariants, adopted deliberately. - Never
claude -p. This repo's Agent SDK usage exists specifically to avoid that failure mode — seedocs/plans/0001-symphony-poc/spec.md's "Why Claude Agent SDK" section and a personal memory note. - The poller auto-Plans, never auto-Implements. See point 2 above — this is the load-bearing safety property of Phase 5, not an oversight to "fix."
Known limitations — work around, don't rediscover
RunStoreis in-memory only. A poller-created Run'ssession_iddoes not survive a server restart — confirmed in practice (docs/runs/ADLC-24/review.md), not just theoretical. If you restart the server mid-flow, aplan_readyRun's Implement path is gone; the ticket's Jira state (comment, status) persists, the ability to resume that exact agent session does not.permission_mode="acceptEdits"can't run Bash. Implement writes a diff, it cannot run its own tests/lint or transition the ticket itself — it will say so honestly in its result text rather than claim false verification. Always runuv run pytest+uv run ruff check .yourself in the worktree before trusting an Implement result.- Each new worktree needs its own
uv sync --extra devbefore its tests will run — worktrees don't share a venv with the main checkout.
Portability
This repo can act as the orchestrator for a different repo/Jira project — see
docs/PORTABILITY.md for the two env vars that matter
(SYMPHONY_WORKFLOW_TARGET_REPO, SYMPHONY_WORKFLOW_JIRA_PROJECT) and what's still
hardcoded (the prompt templates — Symphony's per-repo WORKFLOW.md model isn't
implemented yet).
Shipping a ticket — branch, PR, merge, release
start <KEY> makes branch {KEY}-{slug} and a worktree from a fresh origin/main (Jira links a branch, its
commits and its PR to a card from the key in the branch name), pr <KEY> opens the PR with the key in its title
and body, land <KEY> verifies the merge on GitHub and cleans up (remote branch, worktree, local branch, card
Done and rolled up), check-commit checks a subject is type(scope): subject (KEY). The shipping skill has the
rules; docs/JIRA_GIT_SETUP.md has the optional Jira settings.
Releasing — a plugin only updates when version changes
An installed plugin keeps its cached copy until its version string changes, so a skill edit that is not
released is invisible to every other session. Order matters:
- Bump all four together in the feature PR:
versioninpyproject.toml,plugin/.claude-plugin/plugin.jsonand.claude-plugin/marketplace.json, thesymphony-workflowpackage version line inuv.lock, and a## X.Y.Zentry inCHANGELOG.md. Edit the lock line by hand: a neweruv lockcan rewrite unrelated dependency markers. New feature = minor, fix = patch. - Validate:
uv run pytest,uv run ruff check .,claude plugin validate --strict pluginandclaude plugin validate --strict .(the marketplace; it is a different check and failed once for a missing description). CI runs ruff and pytest on every push and pull request (.github/workflows/ci.yml). - Merge first, tag second. Confirm the merge with
gh pr view <n> --json state,mergeCommit, update localmain(git fetch && git merge --ff-only origin/main), thensymphony-workflow release X.Y.Z. It runsscripts/release.sh(the gate: the three versions agree,marketplace.jsonlists the plugin at that version, the CHANGELOG heading exists, the tag is new, the version test passes, then it tags), pushes the tag, and creates the GitHub Release with the CHANGELOG section as its note. Re-running skips what is done.changelog-draft X.Y.Zwrites the section from the commits since the last tag.release.shalone still tags and never pushes (ADLC-249, ADLC-340). - Close tickets after the merge, never before: transition the Stories Done, then
rollup <subtask>. - Check the install for real after a plugin change, without touching your own Claude config:
CLAUDE_CONFIG_DIR=<scratch> claude plugin marketplace add cloud-f1/symphony-workflow, then... plugin install symphony-workflow-ops@symphony-workflow-marketplace --scope local. A tag is not a GitHub Release:releasemakes both, andrelease-backfill --dry-runlists tags that have none (creating them is public, ask first). The README's version badge reads the tag. - Tell the consumers: the companion status board runs this CLI through
uvx ... @vX.Y.Z; bump its pin. Installed copies of the plugin refresh through/pluginonce the version differs.
gh pr merge can be refused by the auto-mode classifier ("Merge Without Review") for a PR nobody has
reviewed, even when the user asked for it. Do the review a reviewer would (merge all open heads on a scratch
checkout, full tests, a leak scan for this public repo), report it, and let the user run the merge; do not
look for another way around it.
Git and PR gotchas
gh pr mergecan be refused by the auto-mode classifier ("Merge Without Review") for a PR nobody reviewed, and it worked once the user asked explicitly. Verify, report, and if it is refused hand the user the command; do not look for another way. Confirm any merge withgh pr view <n> --json state,mergeCommit.- A user-level
git-guardhook blocks a Bash command line that has both a force-push flag and the wordmain, even inside a heredoc. Run the push as its own command; write notes and memory with the file tool, not a heredoc. - A squash-merged stack needs
git rebase --onto origin/main <old tip> <branch>for each branch above the one that merged, then a lease push of that branch (--force-with-lease, never plain force) and a retarget of its PR. - Deleting remote branches is also classifier-gated. The repo deletes a head branch on merge now, so only old ones need a hand.
- The Jira sink keeps an id-to-key map in
docs/context/sync-state.jsonand trusts a key only when it is in the target project and the card carries that entry's ownsd-id:marker. Never loosen that: the file may come from someone else's checkout.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

