Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/make-all.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,17 @@ jobs:

steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4

- name: Set up mise tools
uses: jdx/mise-action@v3
uses: jdx/mise-action@5228313ee0372e111a38da051671ca30fc5a96db # v3
with:
install: true
cache: true
working_directory: elixir

- name: Cache deps and build
uses: actions/cache@v4
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
with:
path: |
elixir/deps
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/pr-description-lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ jobs:

steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4

- name: Set up mise tools
uses: jdx/mise-action@v3
uses: jdx/mise-action@5228313ee0372e111a38da051671ca30fc5a96db # v3
with:
install: true
cache: true
Expand Down
77 changes: 71 additions & 6 deletions SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,7 @@ Fields:
- Derive from `issue.identifier` by replacing any character not in `[A-Za-z0-9._-]` with `_`.
- Use the sanitized value for the workspace directory name.
- `Normalized Issue State`
- Compare states after `trim` + `lowercase`.
- Compare states after `lowercase`.
- `Session ID`
- Compose from coding-agent `thread_id` and `turn_id` as `<thread_id>-<turn_id>`.

Expand Down Expand Up @@ -351,9 +351,9 @@ Fields:
- If `$VAR_NAME` resolves to an empty string, treat the key as missing.
- `project_slug` (string)
- Required for dispatch when `tracker.kind == "linear"`.
- `active_states` (list of strings or comma-separated string)
- `active_states` (list of strings)
- Default: `Todo`, `In Progress`
- `terminal_states` (list of strings or comma-separated string)
- `terminal_states` (list of strings)
- Default: `Closed`, `Cancelled`, `Canceled`, `Duplicate`, `Done`

#### 5.3.2 `polling` (object)
Expand Down Expand Up @@ -410,7 +410,7 @@ Fields:
- Changes should be re-applied at runtime and affect future retry scheduling.
- `max_concurrent_agents_by_state` (map `state_name -> positive integer`)
- Default: empty map.
- State keys are normalized (`trim` + `lowercase`) for lookup.
- State keys are normalized (`lowercase`) for lookup.
- Invalid entries (non-positive or non-numeric) are ignored.

#### 5.3.6 `codex` (object)
Expand Down Expand Up @@ -555,10 +555,14 @@ This section is intentionally redundant so a coding agent can implement the conf
- `tracker.endpoint`: string, default `https://api.linear.app/graphql` when `tracker.kind=linear`
- `tracker.api_key`: string or `$VAR`, canonical env `LINEAR_API_KEY` when `tracker.kind=linear`
- `tracker.project_slug`: string, required when `tracker.kind=linear`
- `tracker.active_states`: list/string, default `Todo, In Progress`
- `tracker.terminal_states`: list/string, default `Closed, Cancelled, Canceled, Duplicate, Done`
- `tracker.active_states`: list of strings, default `["Todo", "In Progress"]`
- `tracker.terminal_states`: list of strings, default `["Closed", "Cancelled", "Canceled", "Duplicate", "Done"]`
- `polling.interval_ms`: integer, default `30000`
- `workspace.root`: path, default `<system-temp>/symphony_workspaces`
- `worker.ssh_hosts` (extension): list of SSH host strings, optional; when omitted, work runs
locally
- `worker.max_concurrent_agents_per_host` (extension): positive integer, optional; shared per-host
cap applied across configured SSH hosts
- `hooks.after_create`: shell script or null
- `hooks.before_run`: shell script or null
- `hooks.after_run`: shell script or null
Expand Down Expand Up @@ -729,6 +733,12 @@ Per-state limit:

The runtime counts issues by their current tracked state in the `running` map.

Optional SSH host limit:

- When `worker.max_concurrent_agents_per_host` is set, each configured SSH host may run at most
that many concurrent agents at once.
- Hosts at that cap are skipped for new dispatch until capacity frees up.

### 8.4 Retry and Backoff

Retry entry creation:
Expand Down Expand Up @@ -2108,3 +2118,58 @@ Use the same validation profiles as Section 17:
- Verify hook execution and workflow path resolution on the target host OS/shell environment.
- If the optional HTTP server is shipped, verify the configured port behavior and loopback/default
bind expectations on the target environment.

## Appendix A. SSH Worker Extension (Optional)

This appendix describes a common extension profile in which Symphony keeps one central
orchestrator but executes worker runs on one or more remote hosts over SSH.

### A.1 Execution Model

- The orchestrator remains the single source of truth for polling, claims, retries, and
reconciliation.
- `worker.ssh_hosts` provides the candidate SSH destinations for remote execution.
- Each worker run is assigned to one host at a time, and that host becomes part of the run's
effective execution identity along with the issue workspace.
- `workspace.root` is interpreted on the remote host, not on the orchestrator host.
- The coding-agent app-server is launched over SSH stdio instead of as a local subprocess, so the
orchestrator still owns the session lifecycle even though commands execute remotely.
- Continuation turns inside one worker lifetime should stay on the same host and workspace.
- A remote host should satisfy the same basic contract as a local worker environment: reachable
shell, writable workspace root, coding-agent executable, and any required auth or repository
prerequisites.

### A.2 Scheduling Notes

- SSH hosts may be treated as a pool for dispatch.
- Implementations may prefer the previously used host on retries when that host is still
available.
- `worker.max_concurrent_agents_per_host` is an optional shared per-host cap across configured SSH
hosts.
- When all SSH hosts are at capacity, dispatch should wait rather than silently falling back to a
different execution mode.
- Implementations may fail over to another host when the original host is unavailable before work
has meaningfully started.
- Once a run has already produced side effects, a transparent rerun on another host should be
treated as a new attempt, not as invisible failover.

### A.3 Problems to Consider

- Remote environment drift:
- Each host needs the expected shell environment, coding-agent executable, auth, and repository
prerequisites.
- Workspace locality:
- Workspaces are usually host-local, so moving an issue to a different host is typically a cold
restart unless shared storage exists.
- Path and command safety:
- Remote path resolution, shell quoting, and workspace-boundary checks matter more once execution
crosses a machine boundary.
- Startup and failover semantics:
- Implementations should distinguish host-connectivity/startup failures from in-workspace agent
failures so the same ticket is not accidentally re-executed on multiple hosts.
- Host health and saturation:
- A dead or overloaded host should reduce available capacity, not cause duplicate execution or an
accidental fallback to local work.
- Cleanup and observability:
- Operators need to know which host owns a run, where its workspace lives, and whether cleanup
happened on the right machine.
Loading