smol-symphony 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +105 -38
- package/PRODUCT.md +2 -1
- package/README.md +195 -98
- package/SPEC.md +543 -1915
- package/WORKFLOW.md +654 -179
- package/WORKFLOW.template.md +761 -121
- package/dist/acp-bridge.js +324 -0
- package/dist/acp-bridge.js.map +1 -0
- package/dist/actions/cache.js +191 -0
- package/dist/actions/cache.js.map +1 -0
- package/dist/actions/effects.js +41 -0
- package/dist/actions/effects.js.map +1 -0
- package/dist/actions/executor.js +570 -0
- package/dist/actions/executor.js.map +1 -0
- package/dist/actions/index.js +13 -0
- package/dist/actions/index.js.map +1 -0
- package/dist/actions/parsing.js +273 -0
- package/dist/actions/parsing.js.map +1 -0
- package/dist/actions/predicate-env.js +27 -0
- package/dist/actions/predicate-env.js.map +1 -0
- package/dist/actions/predicates.js +49 -0
- package/dist/actions/predicates.js.map +1 -0
- package/dist/actions/templating.js +66 -0
- package/dist/actions/templating.js.map +1 -0
- package/dist/actions/types.js +15 -0
- package/dist/actions/types.js.map +1 -0
- package/dist/agent/acp.js +232 -63
- package/dist/agent/acp.js.map +1 -1
- package/dist/agent/adapter-names.js +159 -0
- package/dist/agent/adapter-names.js.map +1 -0
- package/dist/agent/adapters.js +338 -102
- package/dist/agent/adapters.js.map +1 -1
- package/dist/agent/credential-extractors.js +342 -0
- package/dist/agent/credential-extractors.js.map +1 -0
- package/dist/agent/credential-secrets.js +628 -0
- package/dist/agent/credential-secrets.js.map +1 -0
- package/dist/agent/credential-ticker.js +57 -0
- package/dist/agent/credential-ticker.js.map +1 -0
- package/dist/agent/gondolin-creds-staging.js +356 -0
- package/dist/agent/gondolin-creds-staging.js.map +1 -0
- package/dist/agent/gondolin-dispatch.js +375 -0
- package/dist/agent/gondolin-dispatch.js.map +1 -0
- package/dist/agent/gondolin.js +124 -0
- package/dist/agent/gondolin.js.map +1 -0
- package/dist/agent/runner-decisions.js +134 -0
- package/dist/agent/runner-decisions.js.map +1 -0
- package/dist/agent/runner.js +1352 -290
- package/dist/agent/runner.js.map +1 -1
- package/dist/agent/tool-call-summary.js +102 -0
- package/dist/agent/tool-call-summary.js.map +1 -0
- package/dist/agent/vm-acp-mapping.js +73 -0
- package/dist/agent/vm-acp-mapping.js.map +1 -0
- package/dist/agent/vm-guards.js +262 -0
- package/dist/agent/vm-guards.js.map +1 -0
- package/dist/agent/vm-port.js +22 -0
- package/dist/agent/vm-port.js.map +1 -0
- package/dist/agent/vm-process-registry.js +79 -0
- package/dist/agent/vm-process-registry.js.map +1 -0
- package/dist/bin/cli-args.js +105 -0
- package/dist/bin/cli-args.js.map +1 -0
- package/dist/bin/symphony.js +719 -130
- package/dist/bin/symphony.js.map +1 -1
- package/dist/errors.js +15 -0
- package/dist/errors.js.map +1 -0
- package/dist/http-disk.js +135 -0
- package/dist/http-disk.js.map +1 -0
- package/dist/http-handlers.js +180 -0
- package/dist/http-handlers.js.map +1 -0
- package/dist/http.js +1476 -764
- package/dist/http.js.map +1 -1
- package/dist/issues.js +178 -0
- package/dist/issues.js.map +1 -0
- package/dist/logging.js +163 -5
- package/dist/logging.js.map +1 -1
- package/dist/mcp.js +391 -163
- package/dist/mcp.js.map +1 -1
- package/dist/memory.js +85 -0
- package/dist/memory.js.map +1 -0
- package/dist/orchestrator-decisions.js +331 -0
- package/dist/orchestrator-decisions.js.map +1 -0
- package/dist/orchestrator.js +1189 -303
- package/dist/orchestrator.js.map +1 -1
- package/dist/prompt.js +5 -5
- package/dist/prompt.js.map +1 -1
- package/dist/reconciler/cache.js +65 -0
- package/dist/reconciler/cache.js.map +1 -0
- package/dist/reconciler/index.js +448 -0
- package/dist/reconciler/index.js.map +1 -0
- package/dist/reconciler/ledger.js +131 -0
- package/dist/reconciler/ledger.js.map +1 -0
- package/dist/reconciler/pr-adapters.js +174 -0
- package/dist/reconciler/pr-adapters.js.map +1 -0
- package/dist/reconciler/pr-decide.js +167 -0
- package/dist/reconciler/pr-decide.js.map +1 -0
- package/dist/reconciler/pr.js +422 -0
- package/dist/reconciler/pr.js.map +1 -0
- package/dist/reconciler/types.js +12 -0
- package/dist/reconciler/types.js.map +1 -0
- package/dist/reconciler/vm.js +243 -0
- package/dist/reconciler/vm.js.map +1 -0
- package/dist/reconciler/workspace-defaults.js +83 -0
- package/dist/reconciler/workspace-defaults.js.map +1 -0
- package/dist/reconciler/workspace.js +272 -0
- package/dist/reconciler/workspace.js.map +1 -0
- package/dist/runlog.js +403 -0
- package/dist/runlog.js.map +1 -0
- package/dist/scaffold.js +165 -0
- package/dist/scaffold.js.map +1 -0
- package/dist/trackers/local.js +234 -133
- package/dist/trackers/local.js.map +1 -1
- package/dist/trackers/types.js +1 -1
- package/dist/trackers/types.js.map +1 -1
- package/dist/types.js +1 -1
- package/dist/util/clock.js +12 -0
- package/dist/util/clock.js.map +1 -0
- package/dist/util/crypto.js +25 -0
- package/dist/util/crypto.js.map +1 -0
- package/dist/util/frontmatter.js +70 -0
- package/dist/util/frontmatter.js.map +1 -0
- package/dist/util/fs-issues.js +22 -0
- package/dist/util/fs-issues.js.map +1 -0
- package/dist/util/process.js +152 -0
- package/dist/util/process.js.map +1 -0
- package/dist/util/workspace-key.js +10 -0
- package/dist/util/workspace-key.js.map +1 -0
- package/dist/workflow-loader.js +147 -0
- package/dist/workflow-loader.js.map +1 -0
- package/dist/workflow.js +656 -219
- package/dist/workflow.js.map +1 -1
- package/dist/workspace-types.js +8 -0
- package/dist/workspace-types.js.map +1 -0
- package/dist/workspace.js +367 -120
- package/dist/workspace.js.map +1 -1
- package/package.json +14 -6
- package/scripts/vm-agent.mjs +211 -0
- package/dist/agent/codex.js +0 -439
- package/dist/agent/codex.js.map +0 -1
- package/dist/agent/smolvm.js +0 -174
- package/dist/agent/smolvm.js.map +0 -1
- package/scripts/build-vm.sh +0 -67
package/SPEC.md
CHANGED
|
@@ -1,294 +1,161 @@
|
|
|
1
|
-
# Symphony Service Specification
|
|
1
|
+
# Symphony Service Specification (smol-symphony)
|
|
2
2
|
|
|
3
|
-
Status:
|
|
3
|
+
Status: trimmed reference v1
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
> **Scope.** This document captures the contracts that smol-symphony's code
|
|
6
|
+
> references — workspace safety, ACP approval posture, tracker adapter
|
|
7
|
+
> contract, prompt rendering, logging, etc. The broader architectural
|
|
8
|
+
> narrative was originally derived from
|
|
9
|
+
> [openai/symphony](https://github.com/openai/symphony/blob/main/SPEC.md); see
|
|
10
|
+
> that document for the original design context. This trimmed version is what
|
|
11
|
+
> stays in sync with this repo's code. Sections describing the polling loop,
|
|
12
|
+
> reconciler subsystem, retry mechanics, and reference algorithms have been
|
|
13
|
+
> removed — `src/orchestrator.ts`, `src/reconciler/`, and the test suite under
|
|
14
|
+
> `tests/` are the authoritative source for those behaviors.
|
|
6
15
|
|
|
7
16
|
## Normative Language
|
|
8
17
|
|
|
9
|
-
The key words `MUST`, `MUST NOT`, `REQUIRED`, `SHOULD`, `SHOULD NOT`,
|
|
10
|
-
`OPTIONAL` in this document are to be interpreted as
|
|
18
|
+
The key words `MUST`, `MUST NOT`, `REQUIRED`, `SHOULD`, `SHOULD NOT`,
|
|
19
|
+
`RECOMMENDED`, `MAY`, and `OPTIONAL` in this document are to be interpreted as
|
|
20
|
+
described in RFC 2119.
|
|
11
21
|
|
|
12
|
-
`Implementation-defined` means the behavior is part of the implementation
|
|
13
|
-
specification does not prescribe one universal policy.
|
|
14
|
-
behavior.
|
|
22
|
+
`Implementation-defined` means the behavior is part of the implementation
|
|
23
|
+
contract, but this specification does not prescribe one universal policy.
|
|
15
24
|
|
|
16
25
|
## 1. Problem Statement
|
|
17
26
|
|
|
18
|
-
Symphony is a long-running automation service that continuously reads work
|
|
19
|
-
|
|
20
|
-
coding agent session for that issue inside the workspace.
|
|
27
|
+
Symphony is a long-running automation service that continuously reads work
|
|
28
|
+
from an issue tracker, creates an isolated workspace for each issue, and runs
|
|
29
|
+
a coding agent session for that issue inside the workspace.
|
|
21
30
|
|
|
22
31
|
The service solves four operational problems:
|
|
23
32
|
|
|
24
|
-
- It turns issue execution into a repeatable daemon workflow instead of manual
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
33
|
+
- It turns issue execution into a repeatable daemon workflow instead of manual
|
|
34
|
+
scripts.
|
|
35
|
+
- It isolates agent execution in per-issue workspaces so agent commands run
|
|
36
|
+
only inside per-issue workspace directories.
|
|
37
|
+
- It keeps the workflow policy in-repo (`WORKFLOW.md`) so teams version the
|
|
38
|
+
agent prompt and runtime settings with their code.
|
|
39
|
+
- It provides enough observability to operate and debug multiple concurrent
|
|
40
|
+
agent runs.
|
|
30
41
|
|
|
31
|
-
|
|
32
|
-
specification does not require a single approval, sandbox, or operator-confirmation policy; some
|
|
33
|
-
implementations target trusted environments with a high-trust configuration, while others require
|
|
34
|
-
stricter approvals or sandboxing.
|
|
35
|
-
|
|
36
|
-
Important boundary:
|
|
42
|
+
Boundary:
|
|
37
43
|
|
|
38
44
|
- Symphony is a scheduler/runner and tracker reader.
|
|
39
|
-
- Ticket writes (state transitions, comments, PR links) are typically
|
|
40
|
-
using tools available in the workflow/runtime
|
|
41
|
-
|
|
42
|
-
|
|
45
|
+
- Ticket writes (state transitions, comments, PR links) are typically
|
|
46
|
+
performed by the coding agent using tools available in the workflow/runtime
|
|
47
|
+
environment.
|
|
48
|
+
- A successful run can end at a workflow-defined handoff state (for example
|
|
49
|
+
`Review`), not necessarily `Done`.
|
|
43
50
|
|
|
44
51
|
## 2. Goals and Non-Goals
|
|
45
52
|
|
|
46
53
|
### 2.1 Goals
|
|
47
54
|
|
|
48
|
-
- Poll the issue tracker on a fixed cadence and dispatch work with bounded
|
|
49
|
-
|
|
55
|
+
- Poll the issue tracker on a fixed cadence and dispatch work with bounded
|
|
56
|
+
concurrency.
|
|
57
|
+
- Maintain a single authoritative orchestrator state for dispatch, retries,
|
|
58
|
+
and reconciliation.
|
|
50
59
|
- Create deterministic per-issue workspaces and preserve them across runs.
|
|
51
60
|
- Stop active runs when issue state changes make them ineligible.
|
|
52
61
|
- Recover from transient failures with exponential backoff.
|
|
53
62
|
- Load runtime behavior from a repository-owned `WORKFLOW.md` contract.
|
|
54
63
|
- Expose operator-visible observability (at minimum structured logs).
|
|
55
|
-
- Support tracker/filesystem-driven restart recovery without requiring a
|
|
56
|
-
in-memory scheduler state is not restored.
|
|
64
|
+
- Support tracker/filesystem-driven restart recovery without requiring a
|
|
65
|
+
persistent database; exact in-memory scheduler state is not restored.
|
|
57
66
|
|
|
58
67
|
### 2.2 Non-Goals
|
|
59
68
|
|
|
60
69
|
- Rich web UI or multi-tenant control plane.
|
|
61
70
|
- Prescribing a specific dashboard or terminal UI implementation.
|
|
62
71
|
- General-purpose workflow engine or distributed job scheduler.
|
|
63
|
-
- Built-in business logic for how to edit tickets, PRs, or comments. (That
|
|
64
|
-
workflow prompt and agent tooling.)
|
|
65
|
-
- Mandating strong sandbox controls beyond what the coding agent and host OS
|
|
66
|
-
|
|
67
|
-
implementations.
|
|
68
|
-
|
|
69
|
-
## 3. System Overview
|
|
70
|
-
|
|
71
|
-
### 3.1 Main Components
|
|
72
|
-
|
|
73
|
-
1. `Workflow Loader`
|
|
74
|
-
- Reads `WORKFLOW.md`.
|
|
75
|
-
- Parses YAML front matter and prompt body.
|
|
76
|
-
- Returns `{config, prompt_template}`.
|
|
77
|
-
|
|
78
|
-
2. `Config Layer`
|
|
79
|
-
- Exposes typed getters for workflow config values.
|
|
80
|
-
- Applies defaults and environment variable indirection.
|
|
81
|
-
- Performs validation used by the orchestrator before dispatch.
|
|
82
|
-
|
|
83
|
-
3. `Issue Tracker Client`
|
|
84
|
-
- Fetches candidate issues in active states.
|
|
85
|
-
- Fetches current states for specific issue IDs (reconciliation).
|
|
86
|
-
- Fetches terminal-state issues during startup cleanup.
|
|
87
|
-
- Normalizes tracker payloads into a stable issue model.
|
|
88
|
-
|
|
89
|
-
4. `Orchestrator`
|
|
90
|
-
- Owns the poll tick.
|
|
91
|
-
- Owns the in-memory runtime state.
|
|
92
|
-
- Decides which issues to dispatch, retry, stop, or release.
|
|
93
|
-
- Tracks session metrics and retry queue state.
|
|
94
|
-
|
|
95
|
-
5. `Workspace Manager`
|
|
96
|
-
- Maps issue identifiers to workspace paths.
|
|
97
|
-
- Ensures per-issue workspace directories exist.
|
|
98
|
-
- Runs workspace lifecycle hooks.
|
|
99
|
-
- Cleans workspaces for terminal issues.
|
|
100
|
-
|
|
101
|
-
6. `Agent Runner`
|
|
102
|
-
- Creates workspace.
|
|
103
|
-
- Builds prompt from issue + workflow template.
|
|
104
|
-
- Launches the coding agent app-server client.
|
|
105
|
-
- Streams agent updates back to the orchestrator.
|
|
106
|
-
|
|
107
|
-
7. `Status Surface` (OPTIONAL)
|
|
108
|
-
- Presents human-readable runtime status (for example terminal output, dashboard, or other
|
|
109
|
-
operator-facing view).
|
|
110
|
-
|
|
111
|
-
8. `Logging`
|
|
112
|
-
- Emits structured runtime logs to one or more configured sinks.
|
|
113
|
-
|
|
114
|
-
### 3.2 Abstraction Levels
|
|
115
|
-
|
|
116
|
-
Symphony is easiest to port when kept in these layers:
|
|
117
|
-
|
|
118
|
-
1. `Policy Layer` (repo-defined)
|
|
119
|
-
- `WORKFLOW.md` prompt body.
|
|
120
|
-
- Team-specific rules for ticket handling, validation, and handoff.
|
|
72
|
+
- Built-in business logic for how to edit tickets, PRs, or comments. (That
|
|
73
|
+
logic lives in the workflow prompt and agent tooling.)
|
|
74
|
+
- Mandating strong sandbox controls beyond what the coding agent and host OS
|
|
75
|
+
provide.
|
|
121
76
|
|
|
122
|
-
|
|
123
|
-
- Parses front matter into typed runtime settings.
|
|
124
|
-
- Handles defaults, environment tokens, and path normalization.
|
|
77
|
+
## 3. Core Domain Model
|
|
125
78
|
|
|
126
|
-
|
|
127
|
-
|
|
79
|
+
This section defines the entities the orchestrator passes around. The
|
|
80
|
+
TypeScript view lives in `src/types.ts`.
|
|
128
81
|
|
|
129
|
-
|
|
130
|
-
- Filesystem lifecycle, workspace preparation, coding-agent protocol.
|
|
82
|
+
### 3.1 Entities
|
|
131
83
|
|
|
132
|
-
|
|
133
|
-
- API calls and normalization for tracker data.
|
|
84
|
+
#### 3.1.1 Issue
|
|
134
85
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
### 3.3 External Dependencies
|
|
139
|
-
|
|
140
|
-
- Issue tracker API (Linear for `tracker.kind: linear` in this specification version).
|
|
141
|
-
- Local filesystem for workspaces and logs.
|
|
142
|
-
- OPTIONAL workspace population tooling (for example Git CLI, if used).
|
|
143
|
-
- Coding-agent executable that supports the targeted Codex app-server mode.
|
|
144
|
-
- Host environment authentication for the issue tracker and coding agent.
|
|
145
|
-
|
|
146
|
-
## 4. Core Domain Model
|
|
147
|
-
|
|
148
|
-
### 4.1 Entities
|
|
149
|
-
|
|
150
|
-
#### 4.1.1 Issue
|
|
151
|
-
|
|
152
|
-
Normalized issue record used by orchestration, prompt rendering, and observability output.
|
|
86
|
+
Normalized issue record used by orchestration, prompt rendering, and
|
|
87
|
+
observability output.
|
|
153
88
|
|
|
154
89
|
Fields:
|
|
155
90
|
|
|
156
|
-
- `id` (string)
|
|
157
|
-
|
|
158
|
-
- `identifier` (string)
|
|
159
|
-
- Human-readable ticket key (example: `ABC-123`).
|
|
91
|
+
- `id` (string) — stable tracker-internal ID.
|
|
92
|
+
- `identifier` (string) — human-readable ticket key (example: `ABC-123`).
|
|
160
93
|
- `title` (string)
|
|
161
94
|
- `description` (string or null)
|
|
162
|
-
- `priority` (integer or null)
|
|
163
|
-
|
|
164
|
-
- `state` (string)
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
- Tracker-provided branch metadata if available.
|
|
95
|
+
- `priority` (integer or null) — lower numbers are higher priority in dispatch
|
|
96
|
+
sorting.
|
|
97
|
+
- `state` (string) — current tracker state name.
|
|
98
|
+
- `branch_name` (string or null) — tracker-provided branch metadata if
|
|
99
|
+
available.
|
|
168
100
|
- `url` (string or null)
|
|
169
|
-
- `labels` (list of strings)
|
|
170
|
-
|
|
171
|
-
- `
|
|
172
|
-
-
|
|
173
|
-
|
|
174
|
-
- `identifier` (string or null)
|
|
175
|
-
- `state` (string or null)
|
|
101
|
+
- `labels` (list of strings) — normalized to lowercase.
|
|
102
|
+
- `blocked_by` (list of blocker refs); each blocker ref contains:
|
|
103
|
+
- `id` (string or null)
|
|
104
|
+
- `identifier` (string or null)
|
|
105
|
+
- `state` (string or null)
|
|
176
106
|
- `created_at` (timestamp or null)
|
|
177
107
|
- `updated_at` (timestamp or null)
|
|
178
108
|
|
|
179
|
-
####
|
|
109
|
+
#### 3.1.2 Workflow Definition
|
|
180
110
|
|
|
181
111
|
Parsed `WORKFLOW.md` payload:
|
|
182
112
|
|
|
183
|
-
- `config` (map)
|
|
184
|
-
|
|
185
|
-
- `prompt_template` (string)
|
|
186
|
-
- Markdown body after front matter, trimmed.
|
|
187
|
-
|
|
188
|
-
#### 4.1.3 Service Config (Typed View)
|
|
189
|
-
|
|
190
|
-
Typed runtime values derived from `WorkflowDefinition.config` plus environment resolution.
|
|
191
|
-
|
|
192
|
-
Examples:
|
|
193
|
-
|
|
194
|
-
- poll interval
|
|
195
|
-
- workspace root
|
|
196
|
-
- active and terminal issue states
|
|
197
|
-
- concurrency limits
|
|
198
|
-
- coding-agent executable/args/timeouts
|
|
199
|
-
- workspace hooks
|
|
200
|
-
|
|
201
|
-
#### 4.1.4 Workspace
|
|
202
|
-
|
|
203
|
-
Filesystem workspace assigned to one issue identifier.
|
|
204
|
-
|
|
205
|
-
Fields (logical):
|
|
113
|
+
- `config` (map) — YAML front matter root object.
|
|
114
|
+
- `prompt_template` (string) — Markdown body after front matter, trimmed.
|
|
206
115
|
|
|
207
|
-
|
|
208
|
-
- `workspace_key` (sanitized issue identifier)
|
|
209
|
-
- `created_now` (boolean, used to gate `after_create` hook)
|
|
116
|
+
#### 3.1.3 Service Config (Typed View)
|
|
210
117
|
|
|
211
|
-
|
|
118
|
+
Typed runtime values derived from `WorkflowDefinition.config` plus environment
|
|
119
|
+
resolution: poll interval, workspace root, active/terminal states, concurrency
|
|
120
|
+
limits, coding-agent executable/args/timeouts.
|
|
212
121
|
|
|
213
|
-
|
|
122
|
+
#### 3.1.4 Workspace
|
|
214
123
|
|
|
215
|
-
|
|
124
|
+
Filesystem workspace assigned to one issue identifier. Logical fields:
|
|
125
|
+
`path` (absolute), `workspace_key` (sanitized identifier), `created_now`
|
|
126
|
+
(boolean; true only when this call created the directory, gating the
|
|
127
|
+
one-time canonical setup).
|
|
216
128
|
|
|
217
|
-
|
|
218
|
-
- `issue_identifier`
|
|
219
|
-
- `attempt` (integer or null, `null` for first run, `>=1` for retries/continuation)
|
|
220
|
-
- `workspace_path`
|
|
221
|
-
- `started_at`
|
|
222
|
-
- `status`
|
|
223
|
-
- `error` (OPTIONAL)
|
|
129
|
+
#### 3.1.5 Live Session
|
|
224
130
|
|
|
225
|
-
|
|
131
|
+
State tracked while a coding-agent subprocess is running: `session_id`
|
|
132
|
+
(`<thread_id>-<turn_id>`), `thread_id`, `turn_id`, `adapter_pid`,
|
|
133
|
+
`last_event`, `last_event_at`, `last_message`, cumulative
|
|
134
|
+
`input_tokens`/`output_tokens`/`total_tokens`, `last_reported_*` counters
|
|
135
|
+
used to convert absolute totals to deltas, and `turn_count` (turns started
|
|
136
|
+
within the current worker lifetime).
|
|
226
137
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
Fields:
|
|
230
|
-
|
|
231
|
-
- `session_id` (string, `<thread_id>-<turn_id>`)
|
|
232
|
-
- `thread_id` (string)
|
|
233
|
-
- `turn_id` (string)
|
|
234
|
-
- `codex_app_server_pid` (string or null)
|
|
235
|
-
- `last_codex_event` (string/enum or null)
|
|
236
|
-
- `last_codex_timestamp` (timestamp or null)
|
|
237
|
-
- `last_codex_message` (summarized payload)
|
|
238
|
-
- `codex_input_tokens` (integer)
|
|
239
|
-
- `codex_output_tokens` (integer)
|
|
240
|
-
- `codex_total_tokens` (integer)
|
|
241
|
-
- `last_reported_input_tokens` (integer)
|
|
242
|
-
- `last_reported_output_tokens` (integer)
|
|
243
|
-
- `last_reported_total_tokens` (integer)
|
|
244
|
-
- `turn_count` (integer)
|
|
245
|
-
- Number of coding-agent turns started within the current worker lifetime.
|
|
246
|
-
|
|
247
|
-
#### 4.1.7 Retry Entry
|
|
248
|
-
|
|
249
|
-
Scheduled retry state for an issue.
|
|
250
|
-
|
|
251
|
-
Fields:
|
|
138
|
+
#### 3.1.6 Retry Entry
|
|
252
139
|
|
|
253
|
-
|
|
254
|
-
- `
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
- `timer_handle` (runtime-specific timer reference)
|
|
258
|
-
- `error` (string or null)
|
|
140
|
+
Scheduled retry state for an issue: `issue_id`, `identifier`, `attempt`
|
|
141
|
+
(1-based), `due_at_ms`, `error`, `kind` (`continuation` after a normal exit,
|
|
142
|
+
`failure` after an abnormal exit), and `target_state` (the state the next
|
|
143
|
+
attempt dispatches into).
|
|
259
144
|
|
|
260
|
-
|
|
145
|
+
### 3.2 Stable Identifiers and Normalization Rules
|
|
261
146
|
|
|
262
|
-
|
|
147
|
+
- `Issue ID` — use for tracker lookups and internal map keys.
|
|
148
|
+
- `Issue Identifier` — use for human-readable logs and workspace naming.
|
|
149
|
+
- `Workspace Key` — derive from `issue.identifier` by replacing any character
|
|
150
|
+
not in `[A-Za-z0-9._-]` with `_`. Use the sanitized value for the workspace
|
|
151
|
+
directory name.
|
|
152
|
+
- `Normalized Issue State` — compare states after `lowercase`.
|
|
153
|
+
- `Session ID` — compose from coding-agent `thread_id` and `turn_id` as
|
|
154
|
+
`<thread_id>-<turn_id>`.
|
|
263
155
|
|
|
264
|
-
|
|
156
|
+
## 4. Workflow Specification (Repository Contract)
|
|
265
157
|
|
|
266
|
-
|
|
267
|
-
- `max_concurrent_agents` (current effective global concurrency limit)
|
|
268
|
-
- `running` (map `issue_id -> running entry`)
|
|
269
|
-
- `claimed` (set of issue IDs reserved/running/retrying)
|
|
270
|
-
- `retry_attempts` (map `issue_id -> RetryEntry`)
|
|
271
|
-
- `completed` (set of issue IDs; bookkeeping only, not dispatch gating)
|
|
272
|
-
- `codex_totals` (aggregate tokens + runtime seconds)
|
|
273
|
-
- `codex_rate_limits` (latest rate-limit snapshot from agent events)
|
|
274
|
-
|
|
275
|
-
### 4.2 Stable Identifiers and Normalization Rules
|
|
276
|
-
|
|
277
|
-
- `Issue ID`
|
|
278
|
-
- Use for tracker lookups and internal map keys.
|
|
279
|
-
- `Issue Identifier`
|
|
280
|
-
- Use for human-readable logs and workspace naming.
|
|
281
|
-
- `Workspace Key`
|
|
282
|
-
- Derive from `issue.identifier` by replacing any character not in `[A-Za-z0-9._-]` with `_`.
|
|
283
|
-
- Use the sanitized value for the workspace directory name.
|
|
284
|
-
- `Normalized Issue State`
|
|
285
|
-
- Compare states after `lowercase`.
|
|
286
|
-
- `Session ID`
|
|
287
|
-
- Compose from coding-agent `thread_id` and `turn_id` as `<thread_id>-<turn_id>`.
|
|
288
|
-
|
|
289
|
-
## 5. Workflow Specification (Repository Contract)
|
|
290
|
-
|
|
291
|
-
### 5.1 File Discovery and Path Resolution
|
|
158
|
+
### 4.1 File Discovery and Path Resolution
|
|
292
159
|
|
|
293
160
|
Workflow file path precedence:
|
|
294
161
|
|
|
@@ -300,21 +167,17 @@ Loader behavior:
|
|
|
300
167
|
- If the file cannot be read, return `missing_workflow_file` error.
|
|
301
168
|
- The workflow file is expected to be repository-owned and version-controlled.
|
|
302
169
|
|
|
303
|
-
###
|
|
170
|
+
### 4.2 File Format
|
|
304
171
|
|
|
305
172
|
`WORKFLOW.md` is a Markdown file with OPTIONAL YAML front matter.
|
|
306
173
|
|
|
307
|
-
Design note:
|
|
308
|
-
|
|
309
|
-
- `WORKFLOW.md` SHOULD be self-contained enough to describe and run different workflows (prompt,
|
|
310
|
-
runtime settings, hooks, and tracker selection/config) without requiring out-of-band
|
|
311
|
-
service-specific configuration.
|
|
312
|
-
|
|
313
174
|
Parsing rules:
|
|
314
175
|
|
|
315
|
-
- If file starts with `---`, parse lines until the next `---` as YAML front
|
|
176
|
+
- If file starts with `---`, parse lines until the next `---` as YAML front
|
|
177
|
+
matter.
|
|
316
178
|
- Remaining lines become the prompt body.
|
|
317
|
-
- If front matter is absent, treat the entire file as prompt body and use an
|
|
179
|
+
- If front matter is absent, treat the entire file as prompt body and use an
|
|
180
|
+
empty config map.
|
|
318
181
|
- YAML front matter MUST decode to a map/object; non-map YAML is an error.
|
|
319
182
|
- Prompt body is trimmed before use.
|
|
320
183
|
|
|
@@ -323,138 +186,134 @@ Returned workflow object:
|
|
|
323
186
|
- `config`: front matter root object (not nested under a `config` key).
|
|
324
187
|
- `prompt_template`: trimmed Markdown body.
|
|
325
188
|
|
|
326
|
-
###
|
|
327
|
-
|
|
328
|
-
Top-level keys:
|
|
329
|
-
|
|
330
|
-
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
- `
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
- `
|
|
362
|
-
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
- `
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
- `
|
|
403
|
-
-
|
|
404
|
-
-
|
|
405
|
-
|
|
406
|
-
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
- `
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
- `
|
|
444
|
-
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
- Default: `300000` (5 minutes)
|
|
455
|
-
- If `<= 0`, stall detection is disabled.
|
|
456
|
-
|
|
457
|
-
### 5.4 Prompt Template Contract
|
|
189
|
+
### 4.3 Front Matter Schema
|
|
190
|
+
|
|
191
|
+
Top-level keys: `tracker`, `states`, `polling`, `workspace`, `agent`,
|
|
192
|
+
`acp`. Unknown keys SHOULD be ignored for forward compatibility. Extensions
|
|
193
|
+
MAY define additional top-level keys without changing the core schema.
|
|
194
|
+
|
|
195
|
+
`WORKFLOW.template.md` is the annotated reference for every recognized field;
|
|
196
|
+
this section captures the contract the loader enforces.
|
|
197
|
+
|
|
198
|
+
#### 4.3.1 `tracker` (object)
|
|
199
|
+
|
|
200
|
+
- `kind` (string) — REQUIRED for dispatch; tracker-specific.
|
|
201
|
+
|
|
202
|
+
The set of recognised states and their roles is declared in the top-level
|
|
203
|
+
`states:` block (see §4.3.6), not under `tracker`. The tracker reads role
|
|
204
|
+
membership via `activeStateNames(states)` / `terminalStateNames(states)` (or
|
|
205
|
+
by inspecting `states[*].role` directly).
|
|
206
|
+
|
|
207
|
+
Tracker kinds MAY require additional fields under `tracker`. Implementations
|
|
208
|
+
MUST document the required fields, defaults, and validation rules for each
|
|
209
|
+
supported kind.
|
|
210
|
+
|
|
211
|
+
#### 4.3.2 `polling` (object)
|
|
212
|
+
|
|
213
|
+
- `interval_ms` (integer, default `30000`)
|
|
214
|
+
|
|
215
|
+
#### 4.3.3 `workspace` (object)
|
|
216
|
+
|
|
217
|
+
- `root` (path string or `$VAR`, default `<system-temp>/symphony_workspaces`)
|
|
218
|
+
- `~` is expanded; relative paths are resolved relative to the directory
|
|
219
|
+
containing `WORKFLOW.md`. The effective workspace root is normalized to an
|
|
220
|
+
absolute path before use.
|
|
221
|
+
|
|
222
|
+
#### 4.3.4 `agent` (object)
|
|
223
|
+
|
|
224
|
+
- `max_concurrent_agents` (integer, default `10`) — the GLOBAL host ceiling on
|
|
225
|
+
simultaneously-running agents across every state. This is the cross-state RAM
|
|
226
|
+
bound memory admission clamps, and the value the sum of per-state
|
|
227
|
+
`max_concurrent` caps (§4.3.6) is validated against at startup. It stays
|
|
228
|
+
top-level because it bounds total host memory across all VMs at once.
|
|
229
|
+
- `max_turns` (positive integer, default `20`) — coding-agent turns within
|
|
230
|
+
one worker session.
|
|
231
|
+
- `max_retry_backoff_ms` (integer, default `300000`)
|
|
232
|
+
|
|
233
|
+
#### 4.3.5 `acp` (object)
|
|
234
|
+
|
|
235
|
+
Coding-agent launch is mediated by the Agent Client Protocol. The runtime
|
|
236
|
+
selects an adapter profile and runs it inside the per-issue sandbox; the host
|
|
237
|
+
opens an authenticated TCP bridge that the in-sandbox proxy dials back over
|
|
238
|
+
to carry ACP frames. See `WORKFLOW.template.md` (`acp:` section) for the full
|
|
239
|
+
annotated field list. Adapter credentials are handled out of band of this
|
|
240
|
+
config — there is no credential-mode knob; see §6.3 for the host credential
|
|
241
|
+
proxy and identity-staging contract.
|
|
242
|
+
|
|
243
|
+
Fields read by the runtime:
|
|
244
|
+
|
|
245
|
+
- `adapter` (string, default `claude`) — selects an adapter profile (`claude`,
|
|
246
|
+
`codex`, …).
|
|
247
|
+
- `model` (string or null, default `null`)
|
|
248
|
+
- `shell` (string, default `bash`)
|
|
249
|
+
- `prompt_timeout_ms` (integer, default `3600000`)
|
|
250
|
+
- `read_timeout_ms` (integer, default `30000`)
|
|
251
|
+
- `stall_timeout_ms` (integer, default `300000`; `<= 0` disables stall
|
|
252
|
+
detection)
|
|
253
|
+
- `bridge` (object) — `bind_host`, `bind_port`, `reach_host`, `reach_url`,
|
|
254
|
+
`connect_timeout_ms`; see `WORKFLOW.template.md`.
|
|
255
|
+
|
|
256
|
+
#### 4.3.6 `states` (map)
|
|
257
|
+
|
|
258
|
+
REQUIRED. Declares every tracker state the workflow recognises and the
|
|
259
|
+
per-state dispatch configuration. A workflow that omits the block, or that
|
|
260
|
+
omits any of the three roles (`active`, `terminal`, `holding`), is rejected
|
|
261
|
+
at parse time.
|
|
262
|
+
|
|
263
|
+
Each entry has the shape:
|
|
264
|
+
|
|
265
|
+
- `role` (enum, REQUIRED)
|
|
266
|
+
- `active` — orchestrator dispatches issues in this state.
|
|
267
|
+
- `terminal` — orchestrator treats issues in this state as complete; the
|
|
268
|
+
workspace is removed after the run unwinds.
|
|
269
|
+
- `holding` — directory exists on disk but the orchestrator never
|
|
270
|
+
dispatches it. The landing state for `symphony.propose_issue` is the
|
|
271
|
+
first declared `holding` state.
|
|
272
|
+
- `adapter` (string, OPTIONAL) — overrides the workflow-level `acp.adapter`
|
|
273
|
+
for agents dispatched in this state.
|
|
274
|
+
- `model` (string or null, OPTIONAL) — overrides `acp.model` for this state.
|
|
275
|
+
- `max_turns` (integer, OPTIONAL) — overrides `agent.max_turns` for this
|
|
276
|
+
state.
|
|
277
|
+
- `max_concurrent` (positive integer, OPTIONAL) — caps how many agents the
|
|
278
|
+
orchestrator runs simultaneously for issues in this state. Symmetric with
|
|
279
|
+
`max_turns`; omit for "no per-state cap, only the global
|
|
280
|
+
`agent.max_concurrent_agents` ceiling applies". The sum of every state's
|
|
281
|
+
`max_concurrent` must not exceed `agent.max_concurrent_agents` (validated at
|
|
282
|
+
startup).
|
|
283
|
+
- `allowed_transitions` (list of strings or null, OPTIONAL) — when non-null,
|
|
284
|
+
restricts which states agents in this state may move to via the MCP
|
|
285
|
+
`symphony.transition` tool. `null` (or omitted) means "any declared state
|
|
286
|
+
is reachable"; `[]` means "no transitions allowed out of this state".
|
|
287
|
+
- `pr` (map, OPTIONAL) — PR autopilot routing, valid only on a `terminal`
|
|
288
|
+
state and acting only when the top-level `pr:` engine is enabled (§4.3.7).
|
|
289
|
+
The merge state declares `{ auto_merge: squash|merge|rebase, on_conflict: {
|
|
290
|
+
route_to: <active state> } }`; the close state declares `{ close: true }`.
|
|
291
|
+
The merge/close/route targets are derived by scanning states for this field —
|
|
292
|
+
at most one terminal state may declare `auto_merge`, at most one may declare
|
|
293
|
+
`close`, and an `on_conflict.route_to` naming an undeclared state is rejected
|
|
294
|
+
at parse time.
|
|
295
|
+
|
|
296
|
+
Declaration order is preserved: role-filtered listings and the dashboard
|
|
297
|
+
render columns in the same order.
|
|
298
|
+
|
|
299
|
+
#### 4.3.7 `pr` (object)
|
|
300
|
+
|
|
301
|
+
PR autopilot engine toggle. Optional; default off. The host-global half only —
|
|
302
|
+
the merge/close/route targets and the auto-merge strategy live ON the terminal
|
|
303
|
+
states they describe (`states.<name>.pr`, §4.3.6) and are derived by scanning
|
|
304
|
+
states, not named here.
|
|
305
|
+
|
|
306
|
+
- `enabled` (bool, default `false`) — when true the reconciler grows a `pr`
|
|
307
|
+
resource that arms GitHub auto-merge on the merge state's mergeable PRs,
|
|
308
|
+
routes CONFLICTING ones back to that state's `pr.on_conflict.route_to`,
|
|
309
|
+
closes the close state's open PRs, and reaps workspace + remote branch once a
|
|
310
|
+
PR merges or closes. While enabled, transitions into the merge state defer
|
|
311
|
+
the standard terminal workspace cleanup (the pr resource owns the workspace
|
|
312
|
+
until its PR merges/closes); other terminal states clean up as usual.
|
|
313
|
+
- `poll_interval_ms` (integer, default `30000`; must be non-negative) — per-PR
|
|
314
|
+
`gh pr view` cache TTL.
|
|
315
|
+
|
|
316
|
+
### 4.4 Prompt Template Contract
|
|
458
317
|
|
|
459
318
|
The Markdown body of `WORKFLOW.md` is the per-issue prompt template.
|
|
460
319
|
|
|
@@ -466,20 +325,19 @@ Rendering requirements:
|
|
|
466
325
|
|
|
467
326
|
Template input variables:
|
|
468
327
|
|
|
469
|
-
- `issue` (object)
|
|
470
|
-
|
|
471
|
-
- `attempt` (integer or null)
|
|
472
|
-
|
|
473
|
-
- Integer on retry or continuation run.
|
|
328
|
+
- `issue` (object) — includes all normalized issue fields, including labels
|
|
329
|
+
and blockers.
|
|
330
|
+
- `attempt` (integer or null) — `null`/absent on first attempt; integer on
|
|
331
|
+
retry or continuation run.
|
|
474
332
|
|
|
475
333
|
Fallback prompt behavior:
|
|
476
334
|
|
|
477
|
-
- If the workflow prompt body is empty, the runtime MAY use a minimal default
|
|
478
|
-
(`You are working on an issue
|
|
479
|
-
- Workflow file read/parse failures are configuration/validation errors and
|
|
480
|
-
back to a prompt.
|
|
335
|
+
- If the workflow prompt body is empty, the runtime MAY use a minimal default
|
|
336
|
+
prompt (`You are working on an issue.`).
|
|
337
|
+
- Workflow file read/parse failures are configuration/validation errors and
|
|
338
|
+
SHOULD NOT silently fall back to a prompt.
|
|
481
339
|
|
|
482
|
-
###
|
|
340
|
+
### 4.5 Workflow Validation and Error Surface
|
|
483
341
|
|
|
484
342
|
Error classes:
|
|
485
343
|
|
|
@@ -494,403 +352,96 @@ Dispatch gating behavior:
|
|
|
494
352
|
- Workflow file read/YAML errors block new dispatches until fixed.
|
|
495
353
|
- Template errors fail only the affected run attempt.
|
|
496
354
|
|
|
497
|
-
##
|
|
498
|
-
|
|
499
|
-
### 6.1 Configuration Resolution Pipeline
|
|
500
|
-
|
|
501
|
-
Configuration is resolved in this order:
|
|
502
|
-
|
|
503
|
-
1. Select the workflow file path (explicit runtime setting, otherwise cwd default).
|
|
504
|
-
2. Parse YAML front matter into a raw config map.
|
|
505
|
-
3. Apply built-in defaults for missing OPTIONAL fields.
|
|
506
|
-
4. Resolve `$VAR_NAME` indirection only for config values that explicitly contain `$VAR_NAME`.
|
|
507
|
-
5. Coerce and validate typed values.
|
|
508
|
-
|
|
509
|
-
Environment variables do not globally override YAML values. They are used only when a config value
|
|
510
|
-
explicitly references them.
|
|
511
|
-
|
|
512
|
-
Value coercion semantics:
|
|
513
|
-
|
|
514
|
-
- Path/command fields support:
|
|
515
|
-
- `~` home expansion
|
|
516
|
-
- `$VAR` expansion for env-backed path values
|
|
517
|
-
- Apply expansion only to values intended to be local filesystem paths; do not rewrite URIs or
|
|
518
|
-
arbitrary shell command strings.
|
|
519
|
-
- Relative `workspace.root` values resolve relative to the directory containing the selected
|
|
520
|
-
`WORKFLOW.md`.
|
|
521
|
-
|
|
522
|
-
### 6.2 Dynamic Reload Semantics
|
|
523
|
-
|
|
524
|
-
Dynamic reload is REQUIRED:
|
|
525
|
-
|
|
526
|
-
- The software MUST detect `WORKFLOW.md` changes.
|
|
527
|
-
- On change, it MUST re-read and re-apply workflow config and prompt template without restart.
|
|
528
|
-
- The software MUST attempt to adjust live behavior to the new config (for example polling
|
|
529
|
-
cadence, concurrency limits, active/terminal states, codex settings, workspace paths/hooks, and
|
|
530
|
-
prompt content for future runs).
|
|
531
|
-
- Reloaded config applies to future dispatch, retry scheduling, reconciliation decisions, hook
|
|
532
|
-
execution, and agent launches.
|
|
533
|
-
- Implementations are not REQUIRED to restart in-flight agent sessions automatically when config
|
|
534
|
-
changes.
|
|
535
|
-
- Extensions that manage their own listeners/resources (for example an HTTP server port change) MAY
|
|
536
|
-
require restart unless the implementation explicitly supports live rebind.
|
|
537
|
-
- Implementations SHOULD also re-validate/reload defensively during runtime operations (for example
|
|
538
|
-
before dispatch) in case filesystem watch events are missed.
|
|
539
|
-
- Invalid reloads MUST NOT crash the service; keep operating with the last known good effective
|
|
540
|
-
configuration and emit an operator-visible error.
|
|
541
|
-
|
|
542
|
-
### 6.3 Dispatch Preflight Validation
|
|
543
|
-
|
|
544
|
-
This validation is a scheduler preflight run before attempting to dispatch new work. It validates
|
|
545
|
-
the workflow/config needed to poll and launch workers, not a full audit of all possible workflow
|
|
546
|
-
behavior.
|
|
547
|
-
|
|
548
|
-
Startup validation:
|
|
549
|
-
|
|
550
|
-
- Validate configuration before starting the scheduling loop.
|
|
551
|
-
- If startup validation fails, fail startup and emit an operator-visible error.
|
|
552
|
-
|
|
553
|
-
Per-tick dispatch validation:
|
|
554
|
-
|
|
555
|
-
- Re-validate before each dispatch cycle.
|
|
556
|
-
- If validation fails, skip dispatch for that tick, keep reconciliation active, and emit an
|
|
557
|
-
operator-visible error.
|
|
558
|
-
|
|
559
|
-
Validation checks:
|
|
560
|
-
|
|
561
|
-
- Workflow file can be loaded and parsed.
|
|
562
|
-
- `tracker.kind` is present and supported.
|
|
563
|
-
- `tracker.api_key` is present after `$` resolution.
|
|
564
|
-
- `tracker.project_slug` is present when REQUIRED by the selected tracker kind.
|
|
565
|
-
- `codex.command` is present and non-empty.
|
|
566
|
-
|
|
567
|
-
### 6.4 Core Config Fields Summary (Cheat Sheet)
|
|
568
|
-
|
|
569
|
-
This section is intentionally redundant so a coding agent can implement the config layer quickly.
|
|
570
|
-
Extension fields are documented in the extension section that defines them. Core conformance does
|
|
571
|
-
not require recognizing or validating extension fields unless that extension is implemented.
|
|
572
|
-
|
|
573
|
-
- `tracker.kind`: string, REQUIRED, currently `linear`
|
|
574
|
-
- `tracker.endpoint`: string, default `https://api.linear.app/graphql` when `tracker.kind=linear`
|
|
575
|
-
- `tracker.api_key`: string or `$VAR`, canonical env `LINEAR_API_KEY` when `tracker.kind=linear`
|
|
576
|
-
- `tracker.project_slug`: string, REQUIRED when `tracker.kind=linear`
|
|
577
|
-
- `tracker.active_states`: list of strings, default `["Todo", "In Progress"]`
|
|
578
|
-
- `tracker.terminal_states`: list of strings, default `["Closed", "Cancelled", "Canceled", "Duplicate", "Done"]`
|
|
579
|
-
- `polling.interval_ms`: integer, default `30000`
|
|
580
|
-
- `workspace.root`: path resolved to absolute, default `<system-temp>/symphony_workspaces`
|
|
581
|
-
- `hooks.after_create`: shell script or null
|
|
582
|
-
- `hooks.before_run`: shell script or null
|
|
583
|
-
- `hooks.after_run`: shell script or null
|
|
584
|
-
- `hooks.before_remove`: shell script or null
|
|
585
|
-
- `hooks.timeout_ms`: integer, default `60000`
|
|
586
|
-
- `agent.max_concurrent_agents`: integer, default `10`
|
|
587
|
-
- `agent.max_turns`: integer, default `20`
|
|
588
|
-
- `agent.max_retry_backoff_ms`: integer, default `300000` (5m)
|
|
589
|
-
- `agent.max_concurrent_agents_by_state`: map of positive integers, default `{}`
|
|
590
|
-
- `codex.command`: shell command string, default `codex app-server`
|
|
591
|
-
- `codex.approval_policy`: Codex `AskForApproval` value, default implementation-defined
|
|
592
|
-
- `codex.thread_sandbox`: Codex `SandboxMode` value, default implementation-defined
|
|
593
|
-
- `codex.turn_sandbox_policy`: Codex `SandboxPolicy` value, default implementation-defined
|
|
594
|
-
- `codex.turn_timeout_ms`: integer, default `3600000`
|
|
595
|
-
- `codex.read_timeout_ms`: integer, default `5000`
|
|
596
|
-
- `codex.stall_timeout_ms`: integer, default `300000`
|
|
597
|
-
|
|
598
|
-
## 7. Orchestration State Machine
|
|
599
|
-
|
|
600
|
-
The orchestrator is the only component that mutates scheduling state. All worker outcomes are
|
|
601
|
-
reported back to it and converted into explicit state transitions.
|
|
602
|
-
|
|
603
|
-
### 7.1 Issue Orchestration States
|
|
604
|
-
|
|
605
|
-
This is not the same as tracker states (`Todo`, `In Progress`, etc.). This is the service's internal
|
|
606
|
-
claim state.
|
|
607
|
-
|
|
608
|
-
1. `Unclaimed`
|
|
609
|
-
- Issue is not running and has no retry scheduled.
|
|
610
|
-
|
|
611
|
-
2. `Claimed`
|
|
612
|
-
- Orchestrator has reserved the issue to prevent duplicate dispatch.
|
|
613
|
-
- In practice, claimed issues are either `Running` or `RetryQueued`.
|
|
614
|
-
|
|
615
|
-
3. `Running`
|
|
616
|
-
- Worker task exists and the issue is tracked in `running` map.
|
|
617
|
-
|
|
618
|
-
4. `RetryQueued`
|
|
619
|
-
- Worker is not running, but a retry timer exists in `retry_attempts`.
|
|
620
|
-
|
|
621
|
-
5. `Released`
|
|
622
|
-
- Claim removed because issue is terminal, non-active, missing, or retry path completed without
|
|
623
|
-
re-dispatch.
|
|
624
|
-
|
|
625
|
-
Important nuance:
|
|
626
|
-
|
|
627
|
-
- A successful worker exit does not mean the issue is done forever.
|
|
628
|
-
- The worker MAY continue through multiple back-to-back coding-agent turns before it exits.
|
|
629
|
-
- After each normal turn completion, the worker re-checks the tracker issue state.
|
|
630
|
-
- If the issue is still in an active state, the worker SHOULD start another turn on the same live
|
|
631
|
-
coding-agent thread in the same workspace, up to `agent.max_turns`.
|
|
632
|
-
- The first turn SHOULD use the full rendered task prompt.
|
|
633
|
-
- Continuation turns SHOULD send only continuation guidance to the existing thread, not resend the
|
|
634
|
-
original task prompt that is already present in thread history.
|
|
635
|
-
- Once the worker exits normally, the orchestrator still schedules a short continuation retry
|
|
636
|
-
(about 1 second) so it can re-check whether the issue remains active and needs another worker
|
|
637
|
-
session.
|
|
638
|
-
|
|
639
|
-
### 7.2 Run Attempt Lifecycle
|
|
640
|
-
|
|
641
|
-
A run attempt transitions through these phases:
|
|
642
|
-
|
|
643
|
-
1. `PreparingWorkspace`
|
|
644
|
-
2. `BuildingPrompt`
|
|
645
|
-
3. `LaunchingAgentProcess`
|
|
646
|
-
4. `InitializingSession`
|
|
647
|
-
5. `StreamingTurn`
|
|
648
|
-
6. `Finishing`
|
|
649
|
-
7. `Succeeded`
|
|
650
|
-
8. `Failed`
|
|
651
|
-
9. `TimedOut`
|
|
652
|
-
10. `Stalled`
|
|
653
|
-
11. `CanceledByReconciliation`
|
|
654
|
-
|
|
655
|
-
Distinct terminal reasons are important because retry logic and logs differ.
|
|
656
|
-
|
|
657
|
-
### 7.3 Transition Triggers
|
|
658
|
-
|
|
659
|
-
- `Poll Tick`
|
|
660
|
-
- Reconcile active runs.
|
|
661
|
-
- Validate config.
|
|
662
|
-
- Fetch candidate issues.
|
|
663
|
-
- Dispatch until slots are exhausted.
|
|
664
|
-
|
|
665
|
-
- `Worker Exit (normal)`
|
|
666
|
-
- Remove running entry.
|
|
667
|
-
- Update aggregate runtime totals.
|
|
668
|
-
- Schedule continuation retry (attempt `1`) after the worker exhausts or finishes its in-process
|
|
669
|
-
turn loop.
|
|
670
|
-
|
|
671
|
-
- `Worker Exit (abnormal)`
|
|
672
|
-
- Remove running entry.
|
|
673
|
-
- Update aggregate runtime totals.
|
|
674
|
-
- Schedule exponential-backoff retry.
|
|
675
|
-
|
|
676
|
-
- `Codex Update Event`
|
|
677
|
-
- Update live session fields, token counters, and rate limits.
|
|
678
|
-
|
|
679
|
-
- `Retry Timer Fired`
|
|
680
|
-
- Re-fetch active candidates and attempt re-dispatch, or release claim if no longer eligible.
|
|
681
|
-
|
|
682
|
-
- `Reconciliation State Refresh`
|
|
683
|
-
- Stop runs whose issue states are terminal or no longer active.
|
|
684
|
-
|
|
685
|
-
- `Stall Timeout`
|
|
686
|
-
- Kill worker and schedule retry.
|
|
687
|
-
|
|
688
|
-
### 7.4 Idempotency and Recovery Rules
|
|
689
|
-
|
|
690
|
-
- The orchestrator serializes state mutations through one authority to avoid duplicate dispatch.
|
|
691
|
-
- `claimed` and `running` checks are REQUIRED before launching any worker.
|
|
692
|
-
- Reconciliation runs before dispatch on every tick.
|
|
693
|
-
- Restart recovery is tracker-driven and filesystem-driven (without a durable orchestrator DB).
|
|
694
|
-
- Startup terminal cleanup removes stale workspaces for issues already in terminal states.
|
|
695
|
-
|
|
696
|
-
## 8. Polling, Scheduling, and Reconciliation
|
|
697
|
-
|
|
698
|
-
### 8.1 Poll Loop
|
|
699
|
-
|
|
700
|
-
At startup, the service validates config, performs startup cleanup, schedules an immediate tick, and
|
|
701
|
-
then repeats every `polling.interval_ms`.
|
|
702
|
-
|
|
703
|
-
The effective poll interval SHOULD be updated when workflow config changes are re-applied.
|
|
704
|
-
|
|
705
|
-
Tick sequence:
|
|
706
|
-
|
|
707
|
-
1. Reconcile running issues.
|
|
708
|
-
2. Run dispatch preflight validation.
|
|
709
|
-
3. Fetch candidate issues from tracker using active states.
|
|
710
|
-
4. Sort issues by dispatch priority.
|
|
711
|
-
5. Dispatch eligible issues while slots remain.
|
|
712
|
-
6. Notify observability/status consumers of state changes.
|
|
713
|
-
|
|
714
|
-
If per-tick validation fails, dispatch is skipped for that tick, but reconciliation still happens
|
|
715
|
-
first.
|
|
716
|
-
|
|
717
|
-
### 8.2 Candidate Selection Rules
|
|
718
|
-
|
|
719
|
-
An issue is dispatch-eligible only if all are true:
|
|
720
|
-
|
|
721
|
-
- It has `id`, `identifier`, `title`, and `state`.
|
|
722
|
-
- Its state is in `active_states` and not in `terminal_states`.
|
|
723
|
-
- It is not already in `running`.
|
|
724
|
-
- It is not already in `claimed`.
|
|
725
|
-
- Global concurrency slots are available.
|
|
726
|
-
- Per-state concurrency slots are available.
|
|
727
|
-
- Blocker rule for `Todo` state passes:
|
|
728
|
-
- If the issue state is `Todo`, do not dispatch when any blocker is non-terminal.
|
|
729
|
-
|
|
730
|
-
Sorting order (stable intent):
|
|
731
|
-
|
|
732
|
-
1. `priority` ascending (1..4 are preferred; null/unknown sorts last)
|
|
733
|
-
2. `created_at` oldest first
|
|
734
|
-
3. `identifier` lexicographic tie-breaker
|
|
735
|
-
|
|
736
|
-
### 8.3 Concurrency Control
|
|
737
|
-
|
|
738
|
-
Global limit:
|
|
739
|
-
|
|
740
|
-
- `available_slots = max(max_concurrent_agents - running_count, 0)`
|
|
741
|
-
|
|
742
|
-
Per-state limit:
|
|
743
|
-
|
|
744
|
-
- `max_concurrent_agents_by_state[state]` if present (state key normalized)
|
|
745
|
-
- otherwise fallback to global limit
|
|
746
|
-
|
|
747
|
-
The runtime counts issues by their current tracked state in the `running` map.
|
|
748
|
-
|
|
749
|
-
### 8.4 Retry and Backoff
|
|
750
|
-
|
|
751
|
-
Retry entry creation:
|
|
752
|
-
|
|
753
|
-
- Cancel any existing retry timer for the same issue.
|
|
754
|
-
- Store `attempt`, `identifier`, `error`, `due_at_ms`, and new timer handle.
|
|
755
|
-
|
|
756
|
-
Backoff formula:
|
|
757
|
-
|
|
758
|
-
- Normal continuation retries after a clean worker exit use a short fixed delay of `1000` ms.
|
|
759
|
-
- Failure-driven retries use `delay = min(10000 * 2^(attempt - 1), agent.max_retry_backoff_ms)`.
|
|
760
|
-
- Power is capped by the configured max retry backoff (default `300000` / 5m).
|
|
761
|
-
|
|
762
|
-
Retry handling behavior:
|
|
763
|
-
|
|
764
|
-
1. Fetch active candidate issues (not all issues).
|
|
765
|
-
2. Find the specific issue by `issue_id`.
|
|
766
|
-
3. If not found, release claim.
|
|
767
|
-
4. If found and still candidate-eligible:
|
|
768
|
-
- Dispatch if slots are available.
|
|
769
|
-
- Otherwise requeue with error `no available orchestrator slots`.
|
|
770
|
-
5. If found but no longer active, release claim.
|
|
771
|
-
|
|
772
|
-
Note:
|
|
773
|
-
|
|
774
|
-
- Terminal-state workspace cleanup is handled by startup cleanup and active-run reconciliation
|
|
775
|
-
(including terminal transitions for currently running issues).
|
|
776
|
-
- Retry handling mainly operates on active candidates and releases claims when the issue is absent,
|
|
777
|
-
rather than performing terminal cleanup itself.
|
|
778
|
-
|
|
779
|
-
### 8.5 Active Run Reconciliation
|
|
780
|
-
|
|
781
|
-
Reconciliation runs every tick and has two parts.
|
|
782
|
-
|
|
783
|
-
Part A: Stall detection
|
|
784
|
-
|
|
785
|
-
- For each running issue, compute `elapsed_ms` since:
|
|
786
|
-
- `last_codex_timestamp` if any event has been seen, else
|
|
787
|
-
- `started_at`
|
|
788
|
-
- If `elapsed_ms > codex.stall_timeout_ms`, terminate the worker and queue a retry.
|
|
789
|
-
- If `stall_timeout_ms <= 0`, skip stall detection entirely.
|
|
790
|
-
|
|
791
|
-
Part B: Tracker state refresh
|
|
355
|
+
## 5. Workspace Management and Safety
|
|
792
356
|
|
|
793
|
-
|
|
794
|
-
- For each running issue:
|
|
795
|
-
- If tracker state is terminal: terminate worker and clean workspace.
|
|
796
|
-
- If tracker state is still active: update the in-memory issue snapshot.
|
|
797
|
-
- If tracker state is neither active nor terminal: terminate worker without workspace cleanup.
|
|
798
|
-
- If state refresh fails, keep workers running and try again on the next tick.
|
|
357
|
+
### 5.1 Workspace Layout
|
|
799
358
|
|
|
800
|
-
|
|
359
|
+
Workspace root: `workspace.root` (normalized absolute path). Per-issue
|
|
360
|
+
workspace path: `<workspace.root>/<sanitized_issue_identifier>`. Workspaces
|
|
361
|
+
are reused across runs for the same issue; successful runs do not auto-delete
|
|
362
|
+
workspaces.
|
|
801
363
|
|
|
802
|
-
|
|
364
|
+
### 5.2 Workspace Creation and Reuse
|
|
803
365
|
|
|
804
|
-
|
|
805
|
-
2. For each returned issue identifier, remove the corresponding workspace directory.
|
|
806
|
-
3. If the terminal-issues fetch fails, log a warning and continue startup.
|
|
807
|
-
|
|
808
|
-
This prevents stale terminal workspaces from accumulating after restarts.
|
|
809
|
-
|
|
810
|
-
## 9. Workspace Management and Safety
|
|
811
|
-
|
|
812
|
-
### 9.1 Workspace Layout
|
|
813
|
-
|
|
814
|
-
Workspace root:
|
|
815
|
-
|
|
816
|
-
- `workspace.root` (normalized absolute path)
|
|
817
|
-
|
|
818
|
-
Per-issue workspace path:
|
|
819
|
-
|
|
820
|
-
- `<workspace.root>/<sanitized_issue_identifier>`
|
|
821
|
-
|
|
822
|
-
Workspace persistence:
|
|
823
|
-
|
|
824
|
-
- Workspaces are reused across runs for the same issue.
|
|
825
|
-
- Successful runs do not auto-delete workspaces.
|
|
826
|
-
|
|
827
|
-
### 9.2 Workspace Creation and Reuse
|
|
828
|
-
|
|
829
|
-
Input: `issue.identifier`
|
|
366
|
+
Input: `issue.identifier`.
|
|
830
367
|
|
|
831
368
|
Algorithm summary:
|
|
832
369
|
|
|
833
370
|
1. Sanitize identifier to `workspace_key`.
|
|
834
371
|
2. Compute workspace path under workspace root.
|
|
835
372
|
3. Ensure the workspace path exists as a directory.
|
|
836
|
-
4. Mark `created_now=true` only if the directory was created during this
|
|
837
|
-
`created_now=false`.
|
|
838
|
-
5. If `created_now=true`, run
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
373
|
+
4. Mark `created_now=true` only if the directory was created during this
|
|
374
|
+
call; otherwise `created_now=false`.
|
|
375
|
+
5. If `created_now=true`, run the built-in canonical workspace setup
|
|
376
|
+
(§5.3).
|
|
377
|
+
|
|
378
|
+
Concurrent callers for the same identifier MUST coalesce so the canonical
|
|
379
|
+
setup runs exactly once per workspace creation.
|
|
380
|
+
|
|
381
|
+
### 5.3 Built-in Workspace Population
|
|
382
|
+
|
|
383
|
+
The workspace lifecycle is owned by the orchestrator, not the workflow. On
|
|
384
|
+
first creation, the implementation MUST clone the source repository into the
|
|
385
|
+
workspace path, check out the configured base branch, and cut a per-issue
|
|
386
|
+
branch off the base. There is no shell-glue step; per-VM tooling belongs in
|
|
387
|
+
the agent image and arbitrary in-sandbox commands run via a `run_in_vm`
|
|
388
|
+
action.
|
|
389
|
+
|
|
390
|
+
Canonical setup steps, in order, against the empty workspace directory:
|
|
391
|
+
|
|
392
|
+
1. Validate the source repository looks like a git repository.
|
|
393
|
+
2. `git clone --local --no-tags --branch <base>` from the source repo into
|
|
394
|
+
the workspace path (hardlinked clone so the workspace's object store is a
|
|
395
|
+
cheap delta over the source's at clone time).
|
|
396
|
+
3. Strip every remote the clone copied over and unset any inherited
|
|
397
|
+
`credential.helper`, so any subsequent `git push`/`git fetch` from inside
|
|
398
|
+
the workspace (including from within a dispatched VM) fails closed by
|
|
399
|
+
default.
|
|
400
|
+
4. When configured for a remote repository (e.g. an `origin` URL known via
|
|
401
|
+
env), restore `origin` pointing at the canonical HTTPS URL. The restore
|
|
402
|
+
MUST NOT embed credentials; auth is provided host-side (e.g. by
|
|
403
|
+
`gh auth setup-git`) so the host-side terminal-state push action can push
|
|
404
|
+
without the token ever entering the workspace or any VM derived from it.
|
|
405
|
+
5. Pin commit identity in `--local` git config so commits carry a stable
|
|
406
|
+
author/committer that never leaks into the operator's global git config.
|
|
407
|
+
6. `git checkout -b <branch>` for the per-issue branch (typically
|
|
408
|
+
`agent/<id>`) off the base SHA.
|
|
409
|
+
|
|
410
|
+
The source repository's local `<base>` is the single source of truth for the
|
|
411
|
+
workspace's base ref. Implementations MUST NOT implicitly fetch from a
|
|
412
|
+
different ref (e.g. `origin/<base>`) and reset the workspace base to it; a
|
|
413
|
+
divergent source of truth would produce false-positive drift on a freshly
|
|
414
|
+
created workspace. Operators pick up a new base by updating the source repo
|
|
415
|
+
before the next dispatch.
|
|
852
416
|
|
|
853
417
|
Failure handling:
|
|
854
418
|
|
|
855
|
-
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
- Reused workspaces
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
### 9.4 Workspace Hooks
|
|
862
|
-
|
|
863
|
-
Supported hooks:
|
|
419
|
+
- Failure of any canonical setup step is fatal to workspace creation. The
|
|
420
|
+
partially prepared directory MUST be removed so the next dispatch tick
|
|
421
|
+
re-enters cleanly.
|
|
422
|
+
- Reused workspaces are NOT destructively reset on subsequent dispatches;
|
|
423
|
+
canonical setup runs only when the directory was created during the
|
|
424
|
+
current ensure call.
|
|
864
425
|
|
|
865
|
-
|
|
866
|
-
- `hooks.before_run`
|
|
867
|
-
- `hooks.after_run`
|
|
868
|
-
- `hooks.before_remove`
|
|
426
|
+
### 5.4 Workspace Removal
|
|
869
427
|
|
|
870
|
-
|
|
428
|
+
Once a run reaches a terminal tracker state and fully unwinds, the per-issue
|
|
429
|
+
workspace directory is removed (a best-effort recursive delete). A symlink or
|
|
430
|
+
non-directory at the workspace path is left untouched (containment safety).
|
|
871
431
|
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
- Hook timeout uses `hooks.timeout_ms`; default: `60000 ms`.
|
|
877
|
-
- Log hook start, failures, and timeouts.
|
|
432
|
+
There is no pre-removal shell hook. Anything worth preserving past the run
|
|
433
|
+
(e.g. a patch, a PR) is produced by a terminal-state `actions:` block before
|
|
434
|
+
the workspace is torn down — the canonical example is the Done state's
|
|
435
|
+
`push_branch` + `create_pr_if_missing`.
|
|
878
436
|
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
- `after_create` failure or timeout is fatal to workspace creation.
|
|
882
|
-
- `before_run` failure or timeout is fatal to the current run attempt.
|
|
883
|
-
- `after_run` failure or timeout is logged and ignored.
|
|
884
|
-
- `before_remove` failure or timeout is logged and ignored.
|
|
885
|
-
|
|
886
|
-
### 9.5 Safety Invariants
|
|
437
|
+
### 5.5 Safety Invariants
|
|
887
438
|
|
|
888
439
|
This is the most important portability constraint.
|
|
889
440
|
|
|
890
441
|
Invariant 1: Run the coding agent only in the per-issue workspace path.
|
|
891
442
|
|
|
892
|
-
- Before launching the coding-agent subprocess, validate
|
|
893
|
-
|
|
443
|
+
- Before launching the coding-agent subprocess, validate `cwd ==
|
|
444
|
+
workspace_path`.
|
|
894
445
|
|
|
895
446
|
Invariant 2: Workspace path MUST stay inside workspace root.
|
|
896
447
|
|
|
@@ -903,315 +454,209 @@ Invariant 3: Workspace key is sanitized.
|
|
|
903
454
|
- Only `[A-Za-z0-9._-]` allowed in workspace directory names.
|
|
904
455
|
- Replace all other characters with `_`.
|
|
905
456
|
|
|
906
|
-
##
|
|
907
|
-
|
|
908
|
-
This section defines Symphony's language-neutral responsibilities when integrating a Codex
|
|
909
|
-
app-server. The Codex app-server protocol for the targeted Codex version is the source of truth for
|
|
910
|
-
protocol schemas, message payloads, transport framing, and method names.
|
|
911
|
-
|
|
912
|
-
Protocol source of truth:
|
|
913
|
-
|
|
914
|
-
- Implementations MUST send messages that are valid for the targeted Codex app-server version.
|
|
915
|
-
- Implementations MUST consult the targeted Codex app-server documentation or generated schema
|
|
916
|
-
instead of treating this specification as a protocol schema.
|
|
917
|
-
- If this specification appears to conflict with the targeted Codex app-server protocol, the Codex
|
|
918
|
-
protocol controls protocol shape and transport behavior.
|
|
919
|
-
- Symphony-specific requirements in this section still control orchestration behavior, workspace
|
|
920
|
-
selection, prompt construction, continuation handling, and observability extraction.
|
|
921
|
-
|
|
922
|
-
### 10.1 Launch Contract
|
|
923
|
-
|
|
924
|
-
Subprocess launch parameters:
|
|
925
|
-
|
|
926
|
-
- Command: `codex.command`
|
|
927
|
-
- Invocation: `bash -lc <codex.command>`
|
|
928
|
-
- Working directory: workspace path
|
|
929
|
-
- Transport/framing: the protocol transport required by the targeted Codex app-server version
|
|
930
|
-
|
|
931
|
-
Notes:
|
|
932
|
-
|
|
933
|
-
- The default command is `codex app-server`.
|
|
934
|
-
- Approval policy, sandbox policy, cwd, prompt input, and OPTIONAL tool declarations are supplied
|
|
935
|
-
using fields supported by the targeted Codex app-server version.
|
|
457
|
+
## 6. Agent Runner Protocol (Coding Agent Integration)
|
|
936
458
|
|
|
937
|
-
|
|
459
|
+
The reference implementation speaks the Agent Client Protocol (ACP) to one
|
|
460
|
+
of the known adapter profiles (`claude` via `claude-agent-acp`, `codex` via
|
|
461
|
+
`codex-acp`). ACP is the source of truth for protocol schemas, message
|
|
462
|
+
payloads, transport framing, and method names — implementations MUST consult
|
|
463
|
+
ACP documentation (https://agentclientprotocol.com) or its generated schema
|
|
464
|
+
rather than treating this specification as a protocol schema.
|
|
938
465
|
|
|
939
|
-
|
|
466
|
+
### 6.1 Approval, Tool Calls, and User Input Policy
|
|
940
467
|
|
|
941
|
-
|
|
468
|
+
Approval, sandbox, and user-input behavior is implementation-defined, with
|
|
469
|
+
two requirements:
|
|
942
470
|
|
|
943
|
-
|
|
471
|
+
- Each implementation MUST document its chosen approval, sandbox, and
|
|
472
|
+
operator-confirmation posture.
|
|
473
|
+
- Approval requests and user-input-required events MUST NOT leave a run
|
|
474
|
+
stalled indefinitely. An implementation MAY either satisfy them, surface
|
|
475
|
+
them to an operator, auto-resolve them, or fail the run according to its
|
|
476
|
+
documented policy.
|
|
944
477
|
|
|
945
|
-
|
|
946
|
-
client to:
|
|
947
|
-
|
|
948
|
-
- Start the app-server subprocess in the per-issue workspace.
|
|
949
|
-
- Initialize the app-server session using the targeted Codex app-server protocol.
|
|
950
|
-
- Create or resume a coding-agent thread according to the targeted protocol.
|
|
951
|
-
- Supply the absolute per-issue workspace path as the thread/turn working directory wherever the
|
|
952
|
-
targeted protocol accepts cwd.
|
|
953
|
-
- Start the first turn with the rendered issue prompt.
|
|
954
|
-
- Start later in-worker continuation turns on the same live thread with continuation guidance rather
|
|
955
|
-
than resending the original issue prompt.
|
|
956
|
-
- Supply the implementation's documented approval and sandbox policy using fields supported by the
|
|
957
|
-
targeted protocol.
|
|
958
|
-
- Include issue-identifying metadata, such as `<issue.identifier>: <issue.title>`, when the targeted
|
|
959
|
-
protocol supports turn or session titles.
|
|
960
|
-
- Advertise implemented client-side tools using the targeted protocol.
|
|
961
|
-
|
|
962
|
-
Session identifiers:
|
|
963
|
-
|
|
964
|
-
- Extract `thread_id` from the thread identity returned by the targeted Codex app-server protocol.
|
|
965
|
-
- Extract `turn_id` from each turn identity returned by the targeted Codex app-server protocol.
|
|
966
|
-
- Emit `session_id = "<thread_id>-<turn_id>"`
|
|
967
|
-
- Reuse the same `thread_id` for all continuation turns inside one worker run
|
|
968
|
-
|
|
969
|
-
### 10.3 Streaming Turn Processing
|
|
970
|
-
|
|
971
|
-
The client processes app-server updates according to the targeted Codex app-server protocol until
|
|
972
|
-
the active turn terminates.
|
|
973
|
-
|
|
974
|
-
Completion conditions:
|
|
975
|
-
|
|
976
|
-
- Targeted-protocol turn completion signal -> success
|
|
977
|
-
- Targeted-protocol turn failure signal -> failure
|
|
978
|
-
- Targeted-protocol turn cancellation signal -> failure
|
|
979
|
-
- turn timeout (`turn_timeout_ms`) -> failure
|
|
980
|
-
- subprocess exit -> failure
|
|
981
|
-
|
|
982
|
-
Continuation processing:
|
|
983
|
-
|
|
984
|
-
- If the worker decides to continue after a successful turn, it SHOULD start another turn on the same
|
|
985
|
-
live thread using the targeted protocol.
|
|
986
|
-
- The app-server subprocess SHOULD remain alive across those continuation turns and be stopped only
|
|
987
|
-
when the worker run is ending.
|
|
988
|
-
|
|
989
|
-
Transport handling requirements:
|
|
990
|
-
|
|
991
|
-
- Follow the transport and framing rules of the targeted Codex app-server version.
|
|
992
|
-
- For stdio-based transports, keep protocol stream handling separate from diagnostic stderr
|
|
993
|
-
handling unless the targeted protocol specifies otherwise.
|
|
994
|
-
|
|
995
|
-
### 10.4 Emitted Runtime Events (Upstream to Orchestrator)
|
|
996
|
-
|
|
997
|
-
The app-server client emits structured events to the orchestrator callback. Each event SHOULD
|
|
998
|
-
include:
|
|
999
|
-
|
|
1000
|
-
- `event` (enum/string)
|
|
1001
|
-
- `timestamp` (UTC timestamp)
|
|
1002
|
-
- `codex_app_server_pid` (if available)
|
|
1003
|
-
- OPTIONAL `usage` map (token counts)
|
|
1004
|
-
- payload fields as needed
|
|
1005
|
-
|
|
1006
|
-
Important emitted events include, for example:
|
|
1007
|
-
|
|
1008
|
-
- `session_started`
|
|
1009
|
-
- `startup_failed`
|
|
1010
|
-
- `turn_completed`
|
|
1011
|
-
- `turn_failed`
|
|
1012
|
-
- `turn_cancelled`
|
|
1013
|
-
- `turn_ended_with_error`
|
|
1014
|
-
- `turn_input_required`
|
|
1015
|
-
- `approval_auto_approved`
|
|
1016
|
-
- `unsupported_tool_call`
|
|
1017
|
-
- `notification`
|
|
1018
|
-
- `other_message`
|
|
1019
|
-
- `malformed`
|
|
1020
|
-
|
|
1021
|
-
### 10.5 Approval, Tool Calls, and User Input Policy
|
|
1022
|
-
|
|
1023
|
-
Approval, sandbox, and user-input behavior is implementation-defined.
|
|
1024
|
-
|
|
1025
|
-
Policy requirements:
|
|
1026
|
-
|
|
1027
|
-
- Each implementation MUST document its chosen approval, sandbox, and operator-confirmation
|
|
1028
|
-
posture.
|
|
1029
|
-
- Approval requests and user-input-required events MUST NOT leave a run stalled indefinitely. An
|
|
1030
|
-
implementation MAY either satisfy them, surface them to an operator, auto-resolve them, or
|
|
1031
|
-
fail the run according to its documented policy.
|
|
1032
|
-
|
|
1033
|
-
Example high-trust behavior:
|
|
478
|
+
**smol-symphony "high-trust" posture (the implementation in this repo):**
|
|
1034
479
|
|
|
1035
480
|
- Auto-approve command execution approvals for the session.
|
|
1036
|
-
- Auto-approve file-change approvals for the session.
|
|
481
|
+
- Auto-approve file-change approvals for the session (`allow_always`).
|
|
1037
482
|
- Treat user-input-required turns as hard failure.
|
|
1038
483
|
|
|
1039
484
|
Unsupported dynamic tool calls:
|
|
1040
485
|
|
|
1041
|
-
- Supported dynamic tool calls that are explicitly implemented and advertised
|
|
1042
|
-
be handled according to their extension contract.
|
|
1043
|
-
- If the agent requests a dynamic tool call that is not supported, return a
|
|
1044
|
-
using the targeted protocol and continue the session.
|
|
1045
|
-
|
|
486
|
+
- Supported dynamic tool calls that are explicitly implemented and advertised
|
|
487
|
+
by the runtime SHOULD be handled according to their extension contract.
|
|
488
|
+
- If the agent requests a dynamic tool call that is not supported, return a
|
|
489
|
+
tool failure response using the targeted protocol and continue the session.
|
|
490
|
+
This prevents the session from stalling on unsupported tool execution
|
|
491
|
+
paths.
|
|
1046
492
|
|
|
1047
493
|
Optional client-side tool extension:
|
|
1048
494
|
|
|
1049
|
-
- An implementation MAY expose a limited set of client-side tools to the
|
|
1050
|
-
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
- Purpose: execute a raw GraphQL query or mutation against Linear using Symphony's configured
|
|
1059
|
-
tracker auth for the current session.
|
|
1060
|
-
- Availability: only meaningful when `tracker.kind == "linear"` and valid Linear auth is configured.
|
|
1061
|
-
- Preferred input shape:
|
|
1062
|
-
|
|
1063
|
-
```json
|
|
1064
|
-
{
|
|
1065
|
-
"query": "single GraphQL query or mutation document",
|
|
1066
|
-
"variables": {
|
|
1067
|
-
"optional": "graphql variables object"
|
|
1068
|
-
}
|
|
1069
|
-
}
|
|
1070
|
-
```
|
|
1071
|
-
|
|
1072
|
-
- `query` MUST be a non-empty string.
|
|
1073
|
-
- `query` MUST contain exactly one GraphQL operation.
|
|
1074
|
-
- `variables` is OPTIONAL and, when present, MUST be a JSON object.
|
|
1075
|
-
- Implementations MAY additionally accept a raw GraphQL query string as shorthand input.
|
|
1076
|
-
- Execute one GraphQL operation per tool call.
|
|
1077
|
-
- If the provided document contains multiple operations, reject the tool call as invalid input.
|
|
1078
|
-
- `operationName` selection is intentionally out of scope for this extension.
|
|
1079
|
-
- Reuse the configured Linear endpoint and auth from the active Symphony workflow/runtime config; do
|
|
1080
|
-
not require the coding agent to read raw tokens from disk.
|
|
1081
|
-
- Tool result semantics:
|
|
1082
|
-
- transport success + no top-level GraphQL `errors` -> `success=true`
|
|
1083
|
-
- top-level GraphQL `errors` present -> `success=false`, but preserve the GraphQL response body
|
|
1084
|
-
for debugging
|
|
1085
|
-
- invalid input, missing auth, or transport failure -> `success=false` with an error payload
|
|
1086
|
-
- Return the GraphQL response or error payload as structured tool output that the model can inspect
|
|
1087
|
-
in-session.
|
|
1088
|
-
|
|
1089
|
-
User-input-required policy:
|
|
1090
|
-
|
|
1091
|
-
- Implementations MUST document how targeted-protocol user-input-required signals are handled.
|
|
1092
|
-
- A run MUST NOT stall indefinitely waiting for user input.
|
|
1093
|
-
- A conforming implementation MAY fail the run, surface the request to an operator, satisfy it
|
|
1094
|
-
through an approved operator channel, or auto-resolve it according to its documented policy.
|
|
1095
|
-
- The example high-trust behavior above fails user-input-required turns immediately.
|
|
1096
|
-
|
|
1097
|
-
### 10.6 Timeouts and Error Mapping
|
|
1098
|
-
|
|
1099
|
-
Timeouts:
|
|
1100
|
-
|
|
1101
|
-
- `codex.read_timeout_ms`: request/response timeout during startup and sync requests
|
|
1102
|
-
- `codex.turn_timeout_ms`: total turn stream timeout
|
|
1103
|
-
- `codex.stall_timeout_ms`: enforced by orchestrator based on event inactivity
|
|
1104
|
-
|
|
1105
|
-
Error mapping (RECOMMENDED normalized categories):
|
|
1106
|
-
|
|
1107
|
-
- `codex_not_found`
|
|
1108
|
-
- `invalid_workspace_cwd`
|
|
1109
|
-
- `response_timeout`
|
|
1110
|
-
- `turn_timeout`
|
|
1111
|
-
- `port_exit`
|
|
1112
|
-
- `response_error`
|
|
1113
|
-
- `turn_failed`
|
|
1114
|
-
- `turn_cancelled`
|
|
1115
|
-
- `turn_input_required`
|
|
1116
|
-
|
|
1117
|
-
### 10.7 Agent Runner Contract
|
|
1118
|
-
|
|
1119
|
-
The `Agent Runner` wraps workspace + prompt + app-server client.
|
|
495
|
+
- An implementation MAY expose a limited set of client-side tools to the ACP
|
|
496
|
+
session, advertised through the per-issue MCP endpoint stamped as a client
|
|
497
|
+
capability during the ACP initialize handshake. smol-symphony advertises
|
|
498
|
+
`symphony.transition`, `symphony.request_human_steering`, and
|
|
499
|
+
`symphony.propose_issue`.
|
|
500
|
+
|
|
501
|
+
### 6.2 Agent Runner Contract
|
|
502
|
+
|
|
503
|
+
The `Agent Runner` wraps workspace + prompt + ACP adapter client.
|
|
1120
504
|
|
|
1121
505
|
Behavior:
|
|
1122
506
|
|
|
1123
507
|
1. Create/reuse workspace for issue.
|
|
1124
508
|
2. Build prompt from workflow template.
|
|
1125
|
-
3. Start
|
|
1126
|
-
4. Forward
|
|
509
|
+
3. Start ACP session via the configured adapter.
|
|
510
|
+
4. Forward ACP events to orchestrator.
|
|
1127
511
|
5. On any error, fail the worker attempt (the orchestrator will retry).
|
|
1128
512
|
|
|
1129
|
-
Note:
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
513
|
+
Note: workspaces are intentionally preserved after successful runs.
|
|
514
|
+
|
|
515
|
+
### 6.3 Credential Handling (Host Proxy + Identity Staging)
|
|
516
|
+
|
|
517
|
+
This section is normative for the `claude` adapter; `codex` is covered at the
|
|
518
|
+
end. The governing property: **no OAuth credential bytes ever enter a VM.**
|
|
519
|
+
The operator's `~/.claude/.credentials.json` (and the `refreshToken` /
|
|
520
|
+
`accessToken` it holds) is owned solely by the host; a VM filesystem MUST NOT
|
|
521
|
+
contain that file, a `refreshToken`, or an `accessToken` field at any point
|
|
522
|
+
during a dispatch.
|
|
523
|
+
|
|
524
|
+
**Host credential proxy.** All Anthropic API traffic from an in-VM
|
|
525
|
+
`claude-agent-acp` MUST be mediated by a host-side credential proxy (reference
|
|
526
|
+
implementation: `src/agent/credential-proxy.ts`). The proxy listens on host
|
|
527
|
+
loopback; the VM reaches it transparently via the same guest→host loopback
|
|
528
|
+
path the ACP bridge (§4.3.5) uses. The in-VM
|
|
529
|
+
client never holds a real token — it authenticates to the proxy with a
|
|
530
|
+
per-dispatch sentinel, and the proxy substitutes the real access token
|
|
531
|
+
host-side before forwarding to `api.anthropic.com`.
|
|
532
|
+
|
|
533
|
+
**Per-dispatch sentinel registry.** For each `claude` dispatch the runtime:
|
|
534
|
+
|
|
535
|
+
1. Mints an opaque per-dispatch sentinel and registers it against
|
|
536
|
+
`(issueId, identifier)` in the proxy's in-memory registry. Registration
|
|
537
|
+
MUST happen after VM start + bridge register so an early failure cannot
|
|
538
|
+
leak a registry entry.
|
|
539
|
+
2. Stages the sentinel as `ANTHROPIC_AUTH_TOKEN` and the proxy's reachable URL
|
|
540
|
+
as `ANTHROPIC_BASE_URL` in the VM launch env. These are the only
|
|
541
|
+
credential-shaped values the VM receives, and the sentinel authorizes
|
|
542
|
+
nothing upstream on its own.
|
|
543
|
+
3. On every inbound request the proxy validates the presented bearer against
|
|
544
|
+
the registry using a constant-time comparison; an unknown sentinel is
|
|
545
|
+
rejected (401). On a match the proxy strips the inbound auth, attaches
|
|
546
|
+
`Authorization: Bearer <real access token>`, forwards to
|
|
547
|
+
`api.anthropic.com`, and streams the response back unchanged (including the
|
|
548
|
+
`anthropic-ratelimit-unified-*` and `anthropic-organization-id` headers, so
|
|
549
|
+
the operator's Max-window consumption is observable).
|
|
550
|
+
4. On dispatch teardown the runtime deregisters the sentinel; subsequent
|
|
551
|
+
requests under it are rejected.
|
|
552
|
+
|
|
553
|
+
**"Only the host refreshes" invariant.** Only the host process reads or
|
|
554
|
+
rotates `~/.claude/.credentials.json`. When the cached access token is at or
|
|
555
|
+
past its `expiresAt` (within a small margin) the proxy triggers a refresh by
|
|
556
|
+
spawning the operator's own Claude client (`claude -p`), which performs the
|
|
557
|
+
OAuth exchange and atomically rewrites the credential tuple — symphony never
|
|
558
|
+
implements OAuth itself and never writes credential bytes. Concurrent refresh
|
|
559
|
+
attempts (in-process and cross-process) MUST collapse to a single refresh:
|
|
560
|
+
the reference implementation serializes them under a kernel `flock(2)`
|
|
561
|
+
advisory lock so the kernel owns mutual exclusion and releases automatically
|
|
562
|
+
on holder death. A host-side ticker keeps the cache warm during idle periods
|
|
563
|
+
so the first VM request after expiry does not pay refresh latency. Because the
|
|
564
|
+
refresh token lives only on the host, a multi-hour fleet run needs zero
|
|
565
|
+
in-VM re-login and a VM that outlives the access-token TTL keeps working.
|
|
566
|
+
|
|
567
|
+
**Identity vs. credential distinction.** A VM IS staged with a *sanitized
|
|
568
|
+
identity file*, distinct from the credential file:
|
|
569
|
+
|
|
570
|
+
- **Staged (identity, not credentials):** a minimal `~/.claude.json`
|
|
571
|
+
containing only `oauthAccount.accountUuid` and
|
|
572
|
+
`oauthAccount.organizationUuid`. The runtime extracts just those two UUIDs
|
|
573
|
+
from the host's `~/.claude.json`, writes them into the workspace runtime
|
|
574
|
+
tree (§5.3 staging root, `identity/` subdir), and copies the file to
|
|
575
|
+
`~/.claude.json` inside the VM. Its sole purpose is to let the in-VM client
|
|
576
|
+
emit a well-formed `metadata.user_id` (defensive against server-side OAuth
|
|
577
|
+
fingerprint validation). It carries no `refreshToken`, no `accessToken`, no
|
|
578
|
+
credential bytes. If the host has no `oauthAccount`, staging is skipped.
|
|
579
|
+
- **Never staged (credentials):** the credential file
|
|
580
|
+
`~/.claude/.credentials.json` and the token bytes it holds. There is no
|
|
581
|
+
file-staging path that copies it into a VM, and no runtime directory that
|
|
582
|
+
holds it host-side for staging.
|
|
583
|
+
|
|
584
|
+
**Codex.** The `codex` adapter is also proxied (issue #116). It reuses the
|
|
585
|
+
same per-dispatch sentinel registry as `claude`, but with an adapter-keyed
|
|
586
|
+
upstream profile: the proxy forwards to `api.openai.com`, reads the live
|
|
587
|
+
credential from the host's `~/.codex/auth.json` (`tokens.access_token` or
|
|
588
|
+
`OPENAI_API_KEY`, with an `OPENAI_API_KEY` env fallback) and attaches it as
|
|
589
|
+
`Authorization: Bearer`. The VM is launched with `OPENAI_BASE_URL=<proxy>` and
|
|
590
|
+
`OPENAI_API_KEY=<sentinel>`, and the runtime strips the real `OPENAI_API_KEY`
|
|
591
|
+
from the forwarded VM boot env so no real OpenAI credential reaches the VM.
|
|
592
|
+
The proxy MUST NEVER read or forward the codex `refresh_token`. Because codex
|
|
593
|
+
access tokens are long-lived (~8 days), the proxy currently re-reads on expiry
|
|
594
|
+
rather than driving the OpenAI refresh dance itself (the host remains the sole
|
|
595
|
+
refresher); a host-owned refresh path is a deferred follow-up. No identity file
|
|
596
|
+
is staged for codex (OpenAI ships no third-party fingerprint check). Only
|
|
597
|
+
adapters that resolve to `claude` require the host `~/.claude/.credentials.json`
|
|
598
|
+
at startup; a `codex` state requires `~/.codex/auth.json` (or `OPENAI_API_KEY`)
|
|
599
|
+
on the host instead.
|
|
600
|
+
|
|
601
|
+
## 7. Issue Tracker Integration Contract
|
|
602
|
+
|
|
603
|
+
### 7.1 REQUIRED Operations
|
|
1136
604
|
|
|
1137
605
|
An implementation MUST support these tracker adapter operations:
|
|
1138
606
|
|
|
1139
|
-
1. `fetch_candidate_issues()`
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
607
|
+
1. `fetch_candidate_issues()` — return issues in configured active states.
|
|
608
|
+
2. `fetch_issues_by_states(state_names)` — used for workspace lifecycle
|
|
609
|
+
reconciliation.
|
|
610
|
+
3. `fetch_issue_states_by_ids(issue_ids)` — used for active-run
|
|
611
|
+
reconciliation.
|
|
1144
612
|
|
|
1145
|
-
|
|
1146
|
-
|
|
613
|
+
Empty input to `fetch_issues_by_states([])` MUST return empty without an
|
|
614
|
+
external call.
|
|
1147
615
|
|
|
1148
|
-
###
|
|
616
|
+
### 7.2 Implementation Notes
|
|
1149
617
|
|
|
1150
|
-
|
|
618
|
+
- Tracker-kind-specific transport, auth, and query mechanics are defined by
|
|
619
|
+
the implementation of each tracker adapter.
|
|
620
|
+
- Normalized outputs MUST match the domain model in §3 regardless of
|
|
621
|
+
transport.
|
|
1151
622
|
|
|
1152
|
-
|
|
1153
|
-
- GraphQL endpoint (default `https://api.linear.app/graphql`)
|
|
1154
|
-
- Auth token sent in `Authorization` header
|
|
1155
|
-
- `tracker.project_slug` maps to Linear project `slugId`
|
|
1156
|
-
- Candidate issue query filters project using `project: { slugId: { eq: $projectSlug } }`
|
|
1157
|
-
- Issue-state refresh query uses GraphQL issue IDs with variable type `[ID!]`
|
|
1158
|
-
- Pagination REQUIRED for candidate issues
|
|
1159
|
-
- Page size default: `50`
|
|
1160
|
-
- Network timeout: `30000 ms`
|
|
623
|
+
### 7.3 Normalization Rules
|
|
1161
624
|
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
- Linear GraphQL schema details can drift. Keep query construction isolated and test the exact query
|
|
1165
|
-
fields/types REQUIRED by this specification.
|
|
1166
|
-
|
|
1167
|
-
A non-Linear implementation MAY change transport details, but the normalized outputs MUST match the
|
|
1168
|
-
domain model in Section 4.
|
|
1169
|
-
|
|
1170
|
-
### 11.3 Normalization Rules
|
|
1171
|
-
|
|
1172
|
-
Candidate issue normalization SHOULD produce fields listed in Section 4.1.1.
|
|
625
|
+
Candidate issue normalization SHOULD produce fields listed in §3.1.1.
|
|
1173
626
|
|
|
1174
627
|
Additional normalization details:
|
|
1175
628
|
|
|
1176
|
-
- `labels`
|
|
1177
|
-
- `blocked_by`
|
|
1178
|
-
- `
|
|
1179
|
-
- `
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
RECOMMENDED error categories:
|
|
1184
|
-
|
|
1185
|
-
- `unsupported_tracker_kind`
|
|
1186
|
-
- `missing_tracker_api_key`
|
|
1187
|
-
- `missing_tracker_project_slug`
|
|
1188
|
-
- `linear_api_request` (transport failures)
|
|
1189
|
-
- `linear_api_status` (non-200 HTTP)
|
|
1190
|
-
- `linear_graphql_errors`
|
|
1191
|
-
- `linear_unknown_payload`
|
|
1192
|
-
- `linear_missing_end_cursor` (pagination integrity error)
|
|
629
|
+
- `labels` → lowercase strings.
|
|
630
|
+
- `blocked_by` → resolved from tracker-defined blocker references (for the
|
|
631
|
+
local tracker, the front-matter `blocked_by` list of identifiers).
|
|
632
|
+
- `priority` → integer only (non-integers become null).
|
|
633
|
+
- `created_at` and `updated_at` → parse ISO-8601 timestamps.
|
|
634
|
+
- State comparison is case-insensitive.
|
|
1193
635
|
|
|
1194
|
-
|
|
636
|
+
### 7.4 Tracker Writes (Important Boundary)
|
|
1195
637
|
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
- Startup terminal cleanup failure: log warning and continue startup.
|
|
638
|
+
Symphony does not require first-class tracker write APIs in the
|
|
639
|
+
orchestrator.
|
|
1199
640
|
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
Symphony does not require first-class tracker write APIs in the orchestrator.
|
|
1203
|
-
|
|
1204
|
-
- Ticket mutations (state transitions, comments, PR metadata) are typically handled by the coding
|
|
1205
|
-
agent using tools defined by the workflow prompt.
|
|
641
|
+
- Ticket mutations (state transitions, comments, PR metadata) are handled by
|
|
642
|
+
the coding agent using tools defined by the workflow prompt.
|
|
1206
643
|
- The service remains a scheduler/runner and tracker reader.
|
|
1207
|
-
- Workflow-specific success often means "reached the next handoff state"
|
|
1208
|
-
`
|
|
1209
|
-
- If the `linear_graphql` client-side tool extension is implemented, it is still part of the agent
|
|
1210
|
-
toolchain rather than orchestrator business logic.
|
|
644
|
+
- Workflow-specific success often means "reached the next handoff state"
|
|
645
|
+
(for example `Review`) rather than tracker terminal state `Done`.
|
|
1211
646
|
|
|
1212
|
-
|
|
647
|
+
State-transition primitives are exposed to in-VM agents through the MCP
|
|
648
|
+
surface — specifically the `symphony.transition({ to_state, notes? })` tool.
|
|
649
|
+
The tracker is the authoritative writer: the MCP layer validates `to_state`
|
|
650
|
+
against the workflow's declared `states:` map and any per-state
|
|
651
|
+
`allowed_transitions`, then delegates the notes-append + atomic file move to
|
|
652
|
+
`tracker.moveIssueToState`. The per-issue workspace and `agent/<id>` git
|
|
653
|
+
branch persist across non-terminal transitions (active ↔ active,
|
|
654
|
+
active → holding); cleanup is driven by the target state's role
|
|
655
|
+
(`role: terminal` ⇒ remove workspace, otherwise keep).
|
|
1213
656
|
|
|
1214
|
-
|
|
657
|
+
## 8. Prompt Construction and Context Assembly
|
|
658
|
+
|
|
659
|
+
### 8.1 Inputs
|
|
1215
660
|
|
|
1216
661
|
Inputs to prompt rendering:
|
|
1217
662
|
|
|
@@ -1219,32 +664,30 @@ Inputs to prompt rendering:
|
|
|
1219
664
|
- normalized `issue` object
|
|
1220
665
|
- OPTIONAL `attempt` integer (retry/continuation metadata)
|
|
1221
666
|
|
|
1222
|
-
###
|
|
667
|
+
### 8.2 Rendering Rules
|
|
1223
668
|
|
|
1224
|
-
- Render with strict variable checking.
|
|
1225
|
-
- Render with strict filter checking.
|
|
669
|
+
- Render with strict variable checking (unknown variables fail).
|
|
670
|
+
- Render with strict filter checking (unknown filters fail).
|
|
1226
671
|
- Convert issue object keys to strings for template compatibility.
|
|
1227
672
|
- Preserve nested arrays/maps (labels, blockers) so templates can iterate.
|
|
1228
673
|
|
|
1229
|
-
###
|
|
674
|
+
### 8.3 Retry/Continuation Semantics
|
|
1230
675
|
|
|
1231
|
-
`attempt` SHOULD be passed to the template because the workflow prompt can
|
|
1232
|
-
instructions for:
|
|
676
|
+
`attempt` SHOULD be passed to the template because the workflow prompt can
|
|
677
|
+
provide different instructions for:
|
|
1233
678
|
|
|
1234
679
|
- first run (`attempt` null or absent)
|
|
1235
680
|
- continuation run after a successful prior session
|
|
1236
681
|
- retry after error/timeout/stall
|
|
1237
682
|
|
|
1238
|
-
###
|
|
1239
|
-
|
|
1240
|
-
If prompt rendering fails:
|
|
683
|
+
### 8.4 Failure Semantics
|
|
1241
684
|
|
|
1242
|
-
|
|
1243
|
-
|
|
685
|
+
If prompt rendering fails, fail the run attempt immediately and let the
|
|
686
|
+
orchestrator treat it like any other worker failure.
|
|
1244
687
|
|
|
1245
|
-
##
|
|
688
|
+
## 9. Logging, Status, and Observability
|
|
1246
689
|
|
|
1247
|
-
###
|
|
690
|
+
### 9.1 Logging Conventions
|
|
1248
691
|
|
|
1249
692
|
REQUIRED context fields for issue-related logs:
|
|
1250
693
|
|
|
@@ -1262,908 +705,93 @@ Message formatting requirements:
|
|
|
1262
705
|
- Include concise failure reason when present.
|
|
1263
706
|
- Avoid logging large raw payloads unless necessary.
|
|
1264
707
|
|
|
1265
|
-
###
|
|
708
|
+
### 9.2 Logging Outputs and Sinks
|
|
1266
709
|
|
|
1267
|
-
The spec does not prescribe where logs are written (stderr, file, remote
|
|
710
|
+
The spec does not prescribe where logs are written (stderr, file, remote
|
|
711
|
+
sink, etc.).
|
|
1268
712
|
|
|
1269
713
|
Requirements:
|
|
1270
714
|
|
|
1271
|
-
- Operators MUST be able to see startup/validation/dispatch failures without
|
|
715
|
+
- Operators MUST be able to see startup/validation/dispatch failures without
|
|
716
|
+
attaching a debugger.
|
|
1272
717
|
- Implementations MAY write to one or more sinks.
|
|
1273
|
-
- If a configured log sink fails, the service SHOULD continue running when
|
|
1274
|
-
operator-visible warning through any remaining sink.
|
|
1275
|
-
|
|
1276
|
-
### 13.3 Runtime Snapshot / Monitoring Interface (OPTIONAL but RECOMMENDED)
|
|
1277
|
-
|
|
1278
|
-
If the implementation exposes a synchronous runtime snapshot (for dashboards or monitoring), it
|
|
1279
|
-
SHOULD return:
|
|
1280
|
-
|
|
1281
|
-
- `running` (list of running session rows)
|
|
1282
|
-
- each running row SHOULD include `turn_count`
|
|
1283
|
-
- `retrying` (list of retry queue rows)
|
|
1284
|
-
- `codex_totals`
|
|
1285
|
-
- `input_tokens`
|
|
1286
|
-
- `output_tokens`
|
|
1287
|
-
- `total_tokens`
|
|
1288
|
-
- `seconds_running` (aggregate runtime seconds as of snapshot time, including active sessions)
|
|
1289
|
-
- `rate_limits` (latest coding-agent rate limit payload, if available)
|
|
718
|
+
- If a configured log sink fails, the service SHOULD continue running when
|
|
719
|
+
possible and emit an operator-visible warning through any remaining sink.
|
|
720
|
+
A failed sink MUST NOT crash the orchestrator.
|
|
1290
721
|
|
|
1291
|
-
|
|
722
|
+
### 9.3 Runtime Snapshot / Monitoring Interface (OPTIONAL but RECOMMENDED)
|
|
1292
723
|
|
|
1293
|
-
|
|
1294
|
-
|
|
724
|
+
If the implementation exposes a synchronous runtime snapshot (for dashboards
|
|
725
|
+
or monitoring), it SHOULD return:
|
|
1295
726
|
|
|
1296
|
-
|
|
727
|
+
- `running` (list of running session rows); each row SHOULD include
|
|
728
|
+
`turn_count`.
|
|
729
|
+
- `retrying` (list of retry queue rows).
|
|
730
|
+
- `session_totals`: `input_tokens`, `output_tokens`, `total_tokens`,
|
|
731
|
+
`seconds_running` (aggregate runtime seconds as of snapshot time,
|
|
732
|
+
including active sessions).
|
|
733
|
+
- `rate_limits` (latest coding-agent rate limit payload, if available).
|
|
1297
734
|
|
|
1298
|
-
|
|
1299
|
-
implementation-defined.
|
|
735
|
+
RECOMMENDED snapshot error modes: `timeout`, `unavailable`.
|
|
1300
736
|
|
|
1301
|
-
|
|
1302
|
-
correctness.
|
|
1303
|
-
|
|
1304
|
-
### 13.5 Session Metrics and Token Accounting
|
|
737
|
+
### 9.4 Session Metrics and Token Accounting
|
|
1305
738
|
|
|
1306
739
|
Token accounting rules:
|
|
1307
740
|
|
|
1308
741
|
- Agent events can include token counts in multiple payload shapes.
|
|
1309
|
-
- Prefer absolute thread totals when available, such as
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
- Ignore delta-style payloads such as `last_token_usage` for dashboard/API
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
-
|
|
1317
|
-
|
|
742
|
+
- Prefer absolute thread totals when available, such as
|
|
743
|
+
`thread/tokenUsage/updated` payloads or `total_token_usage` within
|
|
744
|
+
token-count wrapper events.
|
|
745
|
+
- Ignore delta-style payloads such as `last_token_usage` for dashboard/API
|
|
746
|
+
totals.
|
|
747
|
+
- Extract input/output/total token counts leniently from common field names
|
|
748
|
+
within the selected payload.
|
|
749
|
+
- For absolute totals, track deltas relative to last reported totals to
|
|
750
|
+
avoid double-counting.
|
|
751
|
+
- Do not treat generic `usage` maps as cumulative totals unless the event
|
|
752
|
+
type defines them that way.
|
|
1318
753
|
- Accumulate aggregate totals in orchestrator state.
|
|
1319
754
|
|
|
1320
755
|
Runtime accounting:
|
|
1321
756
|
|
|
1322
757
|
- Runtime SHOULD be reported as a live aggregate at snapshot/render time.
|
|
1323
|
-
- Implementations MAY maintain a cumulative counter for ended sessions and
|
|
1324
|
-
elapsed time derived from `running` entries (for
|
|
1325
|
-
snapshot/status view.
|
|
1326
|
-
- Add run duration seconds to the cumulative ended-session runtime when a session ends (normal exit
|
|
1327
|
-
or cancellation/termination).
|
|
758
|
+
- Implementations MAY maintain a cumulative counter for ended sessions and
|
|
759
|
+
add active-session elapsed time derived from `running` entries (for
|
|
760
|
+
example `started_at`) when producing a snapshot/status view.
|
|
1328
761
|
- Continuous background ticking of runtime totals is not REQUIRED.
|
|
1329
762
|
|
|
1330
763
|
Rate-limit tracking:
|
|
1331
764
|
|
|
1332
765
|
- Track the latest rate-limit payload seen in any agent update.
|
|
1333
|
-
- Any human-readable presentation of rate-limit data is
|
|
1334
|
-
|
|
1335
|
-
### 13.6 Humanized Agent Event Summaries (OPTIONAL)
|
|
1336
|
-
|
|
1337
|
-
Humanized summaries of raw agent protocol events are OPTIONAL.
|
|
1338
|
-
|
|
1339
|
-
If implemented:
|
|
1340
|
-
|
|
1341
|
-
- Treat them as observability-only output.
|
|
1342
|
-
- Do not make orchestrator logic depend on humanized strings.
|
|
1343
|
-
|
|
1344
|
-
### 13.7 OPTIONAL HTTP Server Extension
|
|
1345
|
-
|
|
1346
|
-
This section defines an OPTIONAL HTTP interface for observability and operational control.
|
|
766
|
+
- Any human-readable presentation of rate-limit data is
|
|
767
|
+
implementation-defined.
|
|
1347
768
|
|
|
1348
|
-
|
|
769
|
+
### 9.5 OPTIONAL HTTP Server Extension
|
|
1349
770
|
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
orchestrator correctness.
|
|
771
|
+
An OPTIONAL HTTP interface for observability and operational control. The
|
|
772
|
+
dashboard/API MUST be observability/control surfaces only and MUST NOT become
|
|
773
|
+
REQUIRED for orchestrator correctness.
|
|
1354
774
|
|
|
1355
775
|
Extension config:
|
|
1356
776
|
|
|
1357
|
-
- `server.port` (integer, OPTIONAL)
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
- CLI `--port` overrides `server.port` when both are present.
|
|
1361
|
-
|
|
1362
|
-
Enablement (extension):
|
|
1363
|
-
|
|
1364
|
-
- Start the HTTP server when a CLI `--port` argument is provided.
|
|
1365
|
-
- Start the HTTP server when `server.port` is present in `WORKFLOW.md` front matter.
|
|
1366
|
-
- The `server` top-level key is owned by this extension.
|
|
1367
|
-
- Positive `server.port` values bind that port.
|
|
1368
|
-
- Implementations SHOULD bind loopback by default (`127.0.0.1` or host equivalent) unless explicitly
|
|
777
|
+
- `server.port` (integer, OPTIONAL) — enables the HTTP server. `0` requests
|
|
778
|
+
an ephemeral port. CLI `--port` overrides `server.port` when both are
|
|
779
|
+
present. Implementations SHOULD bind loopback by default unless explicitly
|
|
1369
780
|
configured otherwise.
|
|
1370
|
-
- Changes to HTTP listener settings (for example `server.port`) do not need to hot-rebind;
|
|
1371
|
-
restart-required behavior is conformant.
|
|
1372
|
-
|
|
1373
|
-
#### 13.7.1 Human-Readable Dashboard (`/`)
|
|
1374
|
-
|
|
1375
|
-
- Host a human-readable dashboard at `/`.
|
|
1376
|
-
- The returned document SHOULD depict the current state of the system (for example active sessions,
|
|
1377
|
-
retry delays, token consumption, runtime totals, recent events, and health/error indicators).
|
|
1378
|
-
- It is up to the implementation whether this is server-generated HTML or a client-side app that
|
|
1379
|
-
consumes the JSON API below.
|
|
1380
|
-
|
|
1381
|
-
#### 13.7.2 JSON REST API (`/api/v1/*`)
|
|
1382
|
-
|
|
1383
|
-
Provide a JSON REST API under `/api/v1/*` for current runtime state and operational debugging.
|
|
1384
|
-
|
|
1385
|
-
Minimum endpoints:
|
|
1386
|
-
|
|
1387
|
-
- `GET /api/v1/state`
|
|
1388
|
-
- Returns a summary view of the current system state (running sessions, retry queue/delays,
|
|
1389
|
-
aggregate token/runtime totals, latest rate limits, and any additional tracked summary fields).
|
|
1390
|
-
- Suggested response shape:
|
|
1391
|
-
|
|
1392
|
-
```json
|
|
1393
|
-
{
|
|
1394
|
-
"generated_at": "2026-02-24T20:15:30Z",
|
|
1395
|
-
"counts": {
|
|
1396
|
-
"running": 2,
|
|
1397
|
-
"retrying": 1
|
|
1398
|
-
},
|
|
1399
|
-
"running": [
|
|
1400
|
-
{
|
|
1401
|
-
"issue_id": "abc123",
|
|
1402
|
-
"issue_identifier": "MT-649",
|
|
1403
|
-
"state": "In Progress",
|
|
1404
|
-
"session_id": "thread-1-turn-1",
|
|
1405
|
-
"turn_count": 7,
|
|
1406
|
-
"last_event": "turn_completed",
|
|
1407
|
-
"last_message": "",
|
|
1408
|
-
"started_at": "2026-02-24T20:10:12Z",
|
|
1409
|
-
"last_event_at": "2026-02-24T20:14:59Z",
|
|
1410
|
-
"tokens": {
|
|
1411
|
-
"input_tokens": 1200,
|
|
1412
|
-
"output_tokens": 800,
|
|
1413
|
-
"total_tokens": 2000
|
|
1414
|
-
}
|
|
1415
|
-
}
|
|
1416
|
-
],
|
|
1417
|
-
"retrying": [
|
|
1418
|
-
{
|
|
1419
|
-
"issue_id": "def456",
|
|
1420
|
-
"issue_identifier": "MT-650",
|
|
1421
|
-
"attempt": 3,
|
|
1422
|
-
"due_at": "2026-02-24T20:16:00Z",
|
|
1423
|
-
"error": "no available orchestrator slots"
|
|
1424
|
-
}
|
|
1425
|
-
],
|
|
1426
|
-
"codex_totals": {
|
|
1427
|
-
"input_tokens": 5000,
|
|
1428
|
-
"output_tokens": 2400,
|
|
1429
|
-
"total_tokens": 7400,
|
|
1430
|
-
"seconds_running": 1834.2
|
|
1431
|
-
},
|
|
1432
|
-
"rate_limits": null
|
|
1433
|
-
}
|
|
1434
|
-
```
|
|
1435
|
-
|
|
1436
|
-
- `GET /api/v1/<issue_identifier>`
|
|
1437
|
-
- Returns issue-specific runtime/debug details for the identified issue, including any information
|
|
1438
|
-
the implementation tracks that is useful for debugging.
|
|
1439
|
-
- Suggested response shape:
|
|
1440
|
-
|
|
1441
|
-
```json
|
|
1442
|
-
{
|
|
1443
|
-
"issue_identifier": "MT-649",
|
|
1444
|
-
"issue_id": "abc123",
|
|
1445
|
-
"status": "running",
|
|
1446
|
-
"workspace": {
|
|
1447
|
-
"path": "/tmp/symphony_workspaces/MT-649"
|
|
1448
|
-
},
|
|
1449
|
-
"attempts": {
|
|
1450
|
-
"restart_count": 1,
|
|
1451
|
-
"current_retry_attempt": 2
|
|
1452
|
-
},
|
|
1453
|
-
"running": {
|
|
1454
|
-
"session_id": "thread-1-turn-1",
|
|
1455
|
-
"turn_count": 7,
|
|
1456
|
-
"state": "In Progress",
|
|
1457
|
-
"started_at": "2026-02-24T20:10:12Z",
|
|
1458
|
-
"last_event": "notification",
|
|
1459
|
-
"last_message": "Working on tests",
|
|
1460
|
-
"last_event_at": "2026-02-24T20:14:59Z",
|
|
1461
|
-
"tokens": {
|
|
1462
|
-
"input_tokens": 1200,
|
|
1463
|
-
"output_tokens": 800,
|
|
1464
|
-
"total_tokens": 2000
|
|
1465
|
-
}
|
|
1466
|
-
},
|
|
1467
|
-
"retry": null,
|
|
1468
|
-
"logs": {
|
|
1469
|
-
"codex_session_logs": [
|
|
1470
|
-
{
|
|
1471
|
-
"label": "latest",
|
|
1472
|
-
"path": "/var/log/symphony/codex/MT-649/latest.log",
|
|
1473
|
-
"url": null
|
|
1474
|
-
}
|
|
1475
|
-
]
|
|
1476
|
-
},
|
|
1477
|
-
"recent_events": [
|
|
1478
|
-
{
|
|
1479
|
-
"at": "2026-02-24T20:14:59Z",
|
|
1480
|
-
"event": "notification",
|
|
1481
|
-
"message": "Working on tests"
|
|
1482
|
-
}
|
|
1483
|
-
],
|
|
1484
|
-
"last_error": null,
|
|
1485
|
-
"tracked": {}
|
|
1486
|
-
}
|
|
1487
|
-
```
|
|
1488
|
-
|
|
1489
|
-
- If the issue is unknown to the current in-memory state, return `404` with an error response (for
|
|
1490
|
-
example `{\"error\":{\"code\":\"issue_not_found\",\"message\":\"...\"}}`).
|
|
1491
|
-
|
|
1492
|
-
- `POST /api/v1/refresh`
|
|
1493
|
-
- Queues an immediate tracker poll + reconciliation cycle (best-effort trigger; implementations
|
|
1494
|
-
MAY coalesce repeated requests).
|
|
1495
|
-
- Suggested request body: empty body or `{}`.
|
|
1496
|
-
- Suggested response (`202 Accepted`) shape:
|
|
1497
|
-
|
|
1498
|
-
```json
|
|
1499
|
-
{
|
|
1500
|
-
"queued": true,
|
|
1501
|
-
"coalesced": false,
|
|
1502
|
-
"requested_at": "2026-02-24T20:15:30Z",
|
|
1503
|
-
"operations": ["poll", "reconcile"]
|
|
1504
|
-
}
|
|
1505
|
-
```
|
|
1506
|
-
|
|
1507
|
-
API design notes:
|
|
1508
|
-
|
|
1509
|
-
- The JSON shapes above are the RECOMMENDED baseline for interoperability and debugging ergonomics.
|
|
1510
|
-
- Implementations MAY add fields, but SHOULD avoid breaking existing fields within a version.
|
|
1511
|
-
- Endpoints SHOULD be read-only except for operational triggers like `/refresh`.
|
|
1512
|
-
- Unsupported methods on defined routes SHOULD return `405 Method Not Allowed`.
|
|
1513
|
-
- API errors SHOULD use a JSON envelope such as `{"error":{"code":"...","message":"..."}}`.
|
|
1514
|
-
- If the dashboard is a client-side app, it SHOULD consume this API rather than duplicating state
|
|
1515
|
-
logic.
|
|
1516
|
-
|
|
1517
|
-
## 14. Failure Model and Recovery Strategy
|
|
1518
|
-
|
|
1519
|
-
### 14.1 Failure Classes
|
|
1520
|
-
|
|
1521
|
-
1. `Workflow/Config Failures`
|
|
1522
|
-
- Missing `WORKFLOW.md`
|
|
1523
|
-
- Invalid YAML front matter
|
|
1524
|
-
- Unsupported tracker kind or missing tracker credentials/project slug
|
|
1525
|
-
- Missing coding-agent executable
|
|
1526
|
-
|
|
1527
|
-
2. `Workspace Failures`
|
|
1528
|
-
- Workspace directory creation failure
|
|
1529
|
-
- Workspace population/synchronization failure (implementation-defined; can come from hooks)
|
|
1530
|
-
- Invalid workspace path configuration
|
|
1531
|
-
- Hook timeout/failure
|
|
1532
|
-
|
|
1533
|
-
3. `Agent Session Failures`
|
|
1534
|
-
- Startup handshake failure
|
|
1535
|
-
- Turn failed/cancelled
|
|
1536
|
-
- Turn timeout
|
|
1537
|
-
- User input requested and handled as failure by the implementation's documented policy
|
|
1538
|
-
- Subprocess exit
|
|
1539
|
-
- Stalled session (no activity)
|
|
1540
|
-
|
|
1541
|
-
4. `Tracker Failures`
|
|
1542
|
-
- API transport errors
|
|
1543
|
-
- Non-200 status
|
|
1544
|
-
- GraphQL errors
|
|
1545
|
-
- malformed payloads
|
|
1546
|
-
|
|
1547
|
-
5. `Observability Failures`
|
|
1548
|
-
- Snapshot timeout
|
|
1549
|
-
- Dashboard render errors
|
|
1550
|
-
- Log sink configuration failure
|
|
1551
|
-
|
|
1552
|
-
### 14.2 Recovery Behavior
|
|
1553
|
-
|
|
1554
|
-
- Dispatch validation failures:
|
|
1555
|
-
- Skip new dispatches.
|
|
1556
|
-
- Keep service alive.
|
|
1557
|
-
- Continue reconciliation where possible.
|
|
1558
|
-
|
|
1559
|
-
- Worker failures:
|
|
1560
|
-
- Convert to retries with exponential backoff.
|
|
1561
|
-
|
|
1562
|
-
- Tracker candidate-fetch failures:
|
|
1563
|
-
- Skip this tick.
|
|
1564
|
-
- Try again on next tick.
|
|
1565
|
-
|
|
1566
|
-
- Reconciliation state-refresh failures:
|
|
1567
|
-
- Keep current workers.
|
|
1568
|
-
- Retry on next tick.
|
|
1569
|
-
|
|
1570
|
-
- Dashboard/log failures:
|
|
1571
|
-
- Do not crash the orchestrator.
|
|
1572
|
-
|
|
1573
|
-
### 14.3 Partial State Recovery (Restart)
|
|
1574
|
-
|
|
1575
|
-
Current design is intentionally in-memory for scheduler state.
|
|
1576
|
-
Restart recovery means the service can resume useful operation by polling tracker state and reusing
|
|
1577
|
-
preserved workspaces. It does not mean retry timers, running sessions, or live worker state survive
|
|
1578
|
-
process restart.
|
|
1579
|
-
|
|
1580
|
-
After restart:
|
|
1581
|
-
|
|
1582
|
-
- No retry timers are restored from prior process memory.
|
|
1583
|
-
- No running sessions are assumed recoverable.
|
|
1584
|
-
- Service recovers by:
|
|
1585
|
-
- startup terminal workspace cleanup
|
|
1586
|
-
- fresh polling of active issues
|
|
1587
|
-
- re-dispatching eligible work
|
|
1588
|
-
|
|
1589
|
-
### 14.4 Operator Intervention Points
|
|
1590
|
-
|
|
1591
|
-
Operators can control behavior by:
|
|
1592
|
-
|
|
1593
|
-
- Editing `WORKFLOW.md` (prompt and most runtime settings).
|
|
1594
|
-
- `WORKFLOW.md` changes are detected and re-applied automatically without restart according to
|
|
1595
|
-
Section 6.2.
|
|
1596
|
-
- Changing issue states in the tracker:
|
|
1597
|
-
- terminal state -> running session is stopped and workspace cleaned when reconciled
|
|
1598
|
-
- non-active state -> running session is stopped without cleanup
|
|
1599
|
-
- Restarting the service for process recovery or deployment (not as the normal path for applying
|
|
1600
|
-
workflow config changes).
|
|
1601
|
-
|
|
1602
|
-
## 15. Security and Operational Safety
|
|
1603
781
|
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
- Workspace path MUST remain under configured workspace root.
|
|
1622
|
-
- Coding-agent cwd MUST be the per-issue workspace path for the current run.
|
|
1623
|
-
- Workspace directory names MUST use sanitized identifiers.
|
|
1624
|
-
|
|
1625
|
-
RECOMMENDED additional hardening for ports:
|
|
1626
|
-
|
|
1627
|
-
- Run under a dedicated OS user.
|
|
1628
|
-
- Restrict workspace root permissions.
|
|
1629
|
-
- Mount workspace root on a dedicated volume if possible.
|
|
1630
|
-
|
|
1631
|
-
### 15.3 Secret Handling
|
|
1632
|
-
|
|
1633
|
-
- Support `$VAR` indirection in workflow config.
|
|
1634
|
-
- Do not log API tokens or secret env values.
|
|
1635
|
-
- Validate presence of secrets without printing them.
|
|
1636
|
-
|
|
1637
|
-
### 15.4 Hook Script Safety
|
|
1638
|
-
|
|
1639
|
-
Workspace hooks are arbitrary shell scripts from `WORKFLOW.md`.
|
|
1640
|
-
|
|
1641
|
-
Implications:
|
|
1642
|
-
|
|
1643
|
-
- Hooks are fully trusted configuration.
|
|
1644
|
-
- Hooks run inside the workspace directory.
|
|
1645
|
-
- Hook output SHOULD be truncated in logs.
|
|
1646
|
-
- Hook timeouts are REQUIRED to avoid hanging the orchestrator.
|
|
1647
|
-
|
|
1648
|
-
### 15.5 Harness Hardening Guidance
|
|
1649
|
-
|
|
1650
|
-
Running Codex agents against repositories, issue trackers, and other inputs that can contain
|
|
1651
|
-
sensitive data or externally-controlled content can be dangerous. A permissive deployment can lead
|
|
1652
|
-
to data leaks, destructive mutations, or full machine compromise if the agent is induced to execute
|
|
1653
|
-
harmful commands or use overly-powerful integrations.
|
|
1654
|
-
|
|
1655
|
-
Implementations SHOULD explicitly evaluate their own risk profile and harden the execution harness
|
|
1656
|
-
where appropriate. This specification intentionally does not mandate a single hardening posture, but
|
|
1657
|
-
implementations SHOULD NOT assume that tracker data, repository contents, prompt inputs, or tool
|
|
1658
|
-
arguments are fully trustworthy just because they originate inside a normal workflow.
|
|
1659
|
-
|
|
1660
|
-
Possible hardening measures include:
|
|
1661
|
-
|
|
1662
|
-
- Tightening Codex approval and sandbox settings described elsewhere in this specification instead
|
|
1663
|
-
of running with a maximally permissive configuration.
|
|
1664
|
-
- Adding external isolation layers such as OS/container/VM sandboxing, network restrictions, or
|
|
1665
|
-
separate credentials beyond the built-in Codex policy controls.
|
|
1666
|
-
- Filtering which Linear issues, projects, teams, labels, or other tracker sources are eligible for
|
|
1667
|
-
dispatch so untrusted or out-of-scope tasks do not automatically reach the agent.
|
|
1668
|
-
- Narrowing the `linear_graphql` tool so it can only read or mutate data inside the
|
|
1669
|
-
intended project scope, rather than exposing general workspace-wide tracker access.
|
|
1670
|
-
- Reducing the set of client-side tools, credentials, filesystem paths, and network destinations
|
|
1671
|
-
available to the agent to the minimum needed for the workflow.
|
|
1672
|
-
|
|
1673
|
-
The correct controls are deployment-specific, but implementations SHOULD document them clearly and
|
|
1674
|
-
treat harness hardening as part of the core safety model rather than an optional afterthought.
|
|
1675
|
-
|
|
1676
|
-
## 16. Reference Algorithms (Language-Agnostic)
|
|
1677
|
-
|
|
1678
|
-
### 16.1 Service Startup
|
|
1679
|
-
|
|
1680
|
-
```text
|
|
1681
|
-
function start_service():
|
|
1682
|
-
configure_logging()
|
|
1683
|
-
start_observability_outputs()
|
|
1684
|
-
start_workflow_watch(on_change=reload_and_reapply_workflow)
|
|
1685
|
-
|
|
1686
|
-
state = {
|
|
1687
|
-
poll_interval_ms: get_config_poll_interval_ms(),
|
|
1688
|
-
max_concurrent_agents: get_config_max_concurrent_agents(),
|
|
1689
|
-
running: {},
|
|
1690
|
-
claimed: set(),
|
|
1691
|
-
retry_attempts: {},
|
|
1692
|
-
completed: set(),
|
|
1693
|
-
codex_totals: {input_tokens: 0, output_tokens: 0, total_tokens: 0, seconds_running: 0},
|
|
1694
|
-
codex_rate_limits: null
|
|
1695
|
-
}
|
|
1696
|
-
|
|
1697
|
-
validation = validate_dispatch_config()
|
|
1698
|
-
if validation is not ok:
|
|
1699
|
-
log_validation_error(validation)
|
|
1700
|
-
fail_startup(validation)
|
|
1701
|
-
|
|
1702
|
-
startup_terminal_workspace_cleanup()
|
|
1703
|
-
schedule_tick(delay_ms=0)
|
|
1704
|
-
|
|
1705
|
-
event_loop(state)
|
|
1706
|
-
```
|
|
1707
|
-
|
|
1708
|
-
### 16.2 Poll-and-Dispatch Tick
|
|
1709
|
-
|
|
1710
|
-
```text
|
|
1711
|
-
on_tick(state):
|
|
1712
|
-
state = reconcile_running_issues(state)
|
|
1713
|
-
|
|
1714
|
-
validation = validate_dispatch_config()
|
|
1715
|
-
if validation is not ok:
|
|
1716
|
-
log_validation_error(validation)
|
|
1717
|
-
notify_observers()
|
|
1718
|
-
schedule_tick(state.poll_interval_ms)
|
|
1719
|
-
return state
|
|
1720
|
-
|
|
1721
|
-
issues = tracker.fetch_candidate_issues()
|
|
1722
|
-
if issues failed:
|
|
1723
|
-
log_tracker_error()
|
|
1724
|
-
notify_observers()
|
|
1725
|
-
schedule_tick(state.poll_interval_ms)
|
|
1726
|
-
return state
|
|
1727
|
-
|
|
1728
|
-
for issue in sort_for_dispatch(issues):
|
|
1729
|
-
if no_available_slots(state):
|
|
1730
|
-
break
|
|
1731
|
-
|
|
1732
|
-
if should_dispatch(issue, state):
|
|
1733
|
-
state = dispatch_issue(issue, state, attempt=null)
|
|
1734
|
-
|
|
1735
|
-
notify_observers()
|
|
1736
|
-
schedule_tick(state.poll_interval_ms)
|
|
1737
|
-
return state
|
|
1738
|
-
```
|
|
1739
|
-
|
|
1740
|
-
### 16.3 Reconcile Active Runs
|
|
1741
|
-
|
|
1742
|
-
```text
|
|
1743
|
-
function reconcile_running_issues(state):
|
|
1744
|
-
state = reconcile_stalled_runs(state)
|
|
1745
|
-
|
|
1746
|
-
running_ids = keys(state.running)
|
|
1747
|
-
if running_ids is empty:
|
|
1748
|
-
return state
|
|
1749
|
-
|
|
1750
|
-
refreshed = tracker.fetch_issue_states_by_ids(running_ids)
|
|
1751
|
-
if refreshed failed:
|
|
1752
|
-
log_debug("keep workers running")
|
|
1753
|
-
return state
|
|
1754
|
-
|
|
1755
|
-
for issue in refreshed:
|
|
1756
|
-
if issue.state in terminal_states:
|
|
1757
|
-
state = terminate_running_issue(state, issue.id, cleanup_workspace=true)
|
|
1758
|
-
else if issue.state in active_states:
|
|
1759
|
-
state.running[issue.id].issue = issue
|
|
1760
|
-
else:
|
|
1761
|
-
state = terminate_running_issue(state, issue.id, cleanup_workspace=false)
|
|
1762
|
-
|
|
1763
|
-
return state
|
|
1764
|
-
```
|
|
1765
|
-
|
|
1766
|
-
### 16.4 Dispatch One Issue
|
|
1767
|
-
|
|
1768
|
-
```text
|
|
1769
|
-
function dispatch_issue(issue, state, attempt):
|
|
1770
|
-
worker = spawn_worker(
|
|
1771
|
-
fn -> run_agent_attempt(issue, attempt, parent_orchestrator_pid) end
|
|
1772
|
-
)
|
|
1773
|
-
|
|
1774
|
-
if worker spawn failed:
|
|
1775
|
-
return schedule_retry(state, issue.id, next_attempt(attempt), {
|
|
1776
|
-
identifier: issue.identifier,
|
|
1777
|
-
error: "failed to spawn agent"
|
|
1778
|
-
})
|
|
1779
|
-
|
|
1780
|
-
state.running[issue.id] = {
|
|
1781
|
-
worker_handle,
|
|
1782
|
-
monitor_handle,
|
|
1783
|
-
identifier: issue.identifier,
|
|
1784
|
-
issue,
|
|
1785
|
-
session_id: null,
|
|
1786
|
-
codex_app_server_pid: null,
|
|
1787
|
-
last_codex_message: null,
|
|
1788
|
-
last_codex_event: null,
|
|
1789
|
-
last_codex_timestamp: null,
|
|
1790
|
-
codex_input_tokens: 0,
|
|
1791
|
-
codex_output_tokens: 0,
|
|
1792
|
-
codex_total_tokens: 0,
|
|
1793
|
-
last_reported_input_tokens: 0,
|
|
1794
|
-
last_reported_output_tokens: 0,
|
|
1795
|
-
last_reported_total_tokens: 0,
|
|
1796
|
-
retry_attempt: normalize_attempt(attempt),
|
|
1797
|
-
started_at: now_utc()
|
|
1798
|
-
}
|
|
1799
|
-
|
|
1800
|
-
state.claimed.add(issue.id)
|
|
1801
|
-
state.retry_attempts.remove(issue.id)
|
|
1802
|
-
return state
|
|
1803
|
-
```
|
|
1804
|
-
|
|
1805
|
-
### 16.5 Worker Attempt (Workspace + Prompt + Agent)
|
|
1806
|
-
|
|
1807
|
-
```text
|
|
1808
|
-
function run_agent_attempt(issue, attempt, orchestrator_channel):
|
|
1809
|
-
workspace = workspace_manager.create_for_issue(issue.identifier)
|
|
1810
|
-
if workspace failed:
|
|
1811
|
-
fail_worker("workspace error")
|
|
1812
|
-
|
|
1813
|
-
if run_hook("before_run", workspace.path) failed:
|
|
1814
|
-
fail_worker("before_run hook error")
|
|
1815
|
-
|
|
1816
|
-
session = app_server.start_session(workspace=workspace.path)
|
|
1817
|
-
if session failed:
|
|
1818
|
-
run_hook_best_effort("after_run", workspace.path)
|
|
1819
|
-
fail_worker("agent session startup error")
|
|
1820
|
-
|
|
1821
|
-
max_turns = config.agent.max_turns
|
|
1822
|
-
turn_number = 1
|
|
1823
|
-
|
|
1824
|
-
while true:
|
|
1825
|
-
prompt = build_turn_prompt(workflow_template, issue, attempt, turn_number, max_turns)
|
|
1826
|
-
if prompt failed:
|
|
1827
|
-
app_server.stop_session(session)
|
|
1828
|
-
run_hook_best_effort("after_run", workspace.path)
|
|
1829
|
-
fail_worker("prompt error")
|
|
1830
|
-
|
|
1831
|
-
turn_result = app_server.run_turn(
|
|
1832
|
-
session=session,
|
|
1833
|
-
prompt=prompt,
|
|
1834
|
-
issue=issue,
|
|
1835
|
-
on_message=(msg) -> send(orchestrator_channel, {codex_update, issue.id, msg})
|
|
1836
|
-
)
|
|
1837
|
-
|
|
1838
|
-
if turn_result failed:
|
|
1839
|
-
app_server.stop_session(session)
|
|
1840
|
-
run_hook_best_effort("after_run", workspace.path)
|
|
1841
|
-
fail_worker("agent turn error")
|
|
1842
|
-
|
|
1843
|
-
refreshed_issue = tracker.fetch_issue_states_by_ids([issue.id])
|
|
1844
|
-
if refreshed_issue failed:
|
|
1845
|
-
app_server.stop_session(session)
|
|
1846
|
-
run_hook_best_effort("after_run", workspace.path)
|
|
1847
|
-
fail_worker("issue state refresh error")
|
|
1848
|
-
|
|
1849
|
-
issue = refreshed_issue[0] or issue
|
|
1850
|
-
|
|
1851
|
-
if issue.state is not active:
|
|
1852
|
-
break
|
|
1853
|
-
|
|
1854
|
-
if turn_number >= max_turns:
|
|
1855
|
-
break
|
|
1856
|
-
|
|
1857
|
-
turn_number = turn_number + 1
|
|
1858
|
-
|
|
1859
|
-
app_server.stop_session(session)
|
|
1860
|
-
run_hook_best_effort("after_run", workspace.path)
|
|
1861
|
-
|
|
1862
|
-
exit_normal()
|
|
1863
|
-
```
|
|
1864
|
-
|
|
1865
|
-
### 16.6 Worker Exit and Retry Handling
|
|
1866
|
-
|
|
1867
|
-
```text
|
|
1868
|
-
on_worker_exit(issue_id, reason, state):
|
|
1869
|
-
running_entry = state.running.remove(issue_id)
|
|
1870
|
-
state = add_runtime_seconds_to_totals(state, running_entry)
|
|
1871
|
-
|
|
1872
|
-
if reason == normal:
|
|
1873
|
-
state.completed.add(issue_id) # bookkeeping only
|
|
1874
|
-
state = schedule_retry(state, issue_id, 1, {
|
|
1875
|
-
identifier: running_entry.identifier,
|
|
1876
|
-
delay_type: continuation
|
|
1877
|
-
})
|
|
1878
|
-
else:
|
|
1879
|
-
state = schedule_retry(state, issue_id, next_attempt_from(running_entry), {
|
|
1880
|
-
identifier: running_entry.identifier,
|
|
1881
|
-
error: format("worker exited: %reason")
|
|
1882
|
-
})
|
|
1883
|
-
|
|
1884
|
-
notify_observers()
|
|
1885
|
-
return state
|
|
1886
|
-
```
|
|
1887
|
-
|
|
1888
|
-
```text
|
|
1889
|
-
on_retry_timer(issue_id, state):
|
|
1890
|
-
retry_entry = state.retry_attempts.pop(issue_id)
|
|
1891
|
-
if missing:
|
|
1892
|
-
return state
|
|
1893
|
-
|
|
1894
|
-
candidates = tracker.fetch_candidate_issues()
|
|
1895
|
-
if fetch failed:
|
|
1896
|
-
return schedule_retry(state, issue_id, retry_entry.attempt + 1, {
|
|
1897
|
-
identifier: retry_entry.identifier,
|
|
1898
|
-
error: "retry poll failed"
|
|
1899
|
-
})
|
|
1900
|
-
|
|
1901
|
-
issue = find_by_id(candidates, issue_id)
|
|
1902
|
-
if issue is null:
|
|
1903
|
-
state.claimed.remove(issue_id)
|
|
1904
|
-
return state
|
|
1905
|
-
|
|
1906
|
-
if available_slots(state) == 0:
|
|
1907
|
-
return schedule_retry(state, issue_id, retry_entry.attempt + 1, {
|
|
1908
|
-
identifier: issue.identifier,
|
|
1909
|
-
error: "no available orchestrator slots"
|
|
1910
|
-
})
|
|
1911
|
-
|
|
1912
|
-
return dispatch_issue(issue, state, attempt=retry_entry.attempt)
|
|
1913
|
-
```
|
|
1914
|
-
|
|
1915
|
-
## 17. Test and Validation Matrix
|
|
1916
|
-
|
|
1917
|
-
A conforming implementation SHOULD include tests that cover the behaviors defined in this
|
|
1918
|
-
specification.
|
|
1919
|
-
|
|
1920
|
-
Validation profiles:
|
|
1921
|
-
|
|
1922
|
-
- `Core Conformance`: deterministic tests REQUIRED for all conforming implementations.
|
|
1923
|
-
- `Extension Conformance`: REQUIRED only for OPTIONAL features that an implementation chooses to
|
|
1924
|
-
ship.
|
|
1925
|
-
- `Real Integration Profile`: environment-dependent smoke/integration checks RECOMMENDED before
|
|
1926
|
-
production use.
|
|
1927
|
-
|
|
1928
|
-
Unless otherwise noted, Sections 17.1 through 17.7 are `Core Conformance`. Bullets that begin with
|
|
1929
|
-
`If ... is implemented` are `Extension Conformance`.
|
|
1930
|
-
|
|
1931
|
-
### 17.1 Workflow and Config Parsing
|
|
1932
|
-
|
|
1933
|
-
- Workflow file path precedence:
|
|
1934
|
-
- explicit runtime path is used when provided
|
|
1935
|
-
- cwd default is `WORKFLOW.md` when no explicit runtime path is provided
|
|
1936
|
-
- Workflow file changes are detected and trigger re-read/re-apply without restart
|
|
1937
|
-
- Invalid workflow reload keeps last known good effective configuration and emits an
|
|
1938
|
-
operator-visible error
|
|
1939
|
-
- Missing `WORKFLOW.md` returns typed error
|
|
1940
|
-
- Invalid YAML front matter returns typed error
|
|
1941
|
-
- Front matter non-map returns typed error
|
|
1942
|
-
- Config defaults apply when OPTIONAL values are missing
|
|
1943
|
-
- `tracker.kind` validation enforces currently supported kind (`linear`)
|
|
1944
|
-
- `tracker.api_key` works (including `$VAR` indirection)
|
|
1945
|
-
- `$VAR` resolution works for tracker API key and path values
|
|
1946
|
-
- `~` path expansion works
|
|
1947
|
-
- `codex.command` is preserved as a shell command string
|
|
1948
|
-
- Per-state concurrency override map normalizes state names and ignores invalid values
|
|
1949
|
-
- Prompt template renders `issue` and `attempt`
|
|
1950
|
-
- Prompt rendering fails on unknown variables (strict mode)
|
|
1951
|
-
|
|
1952
|
-
### 17.2 Workspace Manager and Safety
|
|
1953
|
-
|
|
1954
|
-
- Deterministic workspace path per issue identifier
|
|
1955
|
-
- Missing workspace directory is created
|
|
1956
|
-
- Existing workspace directory is reused
|
|
1957
|
-
- Existing non-directory path at workspace location is handled safely (replace or fail per
|
|
1958
|
-
implementation policy)
|
|
1959
|
-
- OPTIONAL workspace population/synchronization errors are surfaced
|
|
1960
|
-
- `after_create` hook runs only on new workspace creation
|
|
1961
|
-
- `before_run` hook runs before each attempt and failure/timeouts abort the current attempt
|
|
1962
|
-
- `after_run` hook runs after each attempt and failure/timeouts are logged and ignored
|
|
1963
|
-
- `before_remove` hook runs on cleanup and failures/timeouts are ignored
|
|
1964
|
-
- Workspace path sanitization and root containment invariants are enforced before agent launch
|
|
1965
|
-
- Agent launch uses the per-issue workspace path as cwd and rejects out-of-root paths
|
|
1966
|
-
|
|
1967
|
-
### 17.3 Issue Tracker Client
|
|
1968
|
-
|
|
1969
|
-
- Candidate issue fetch uses active states and project slug
|
|
1970
|
-
- Linear query uses the specified project filter field (`slugId`)
|
|
1971
|
-
- Empty `fetch_issues_by_states([])` returns empty without API call
|
|
1972
|
-
- Pagination preserves order across multiple pages
|
|
1973
|
-
- Blockers are normalized from inverse relations of type `blocks`
|
|
1974
|
-
- Labels are normalized to lowercase
|
|
1975
|
-
- Issue state refresh by ID returns minimal normalized issues
|
|
1976
|
-
- Issue state refresh query uses GraphQL ID typing (`[ID!]`) as specified in Section 11.2
|
|
1977
|
-
- Error mapping for request errors, non-200, GraphQL errors, malformed payloads
|
|
1978
|
-
|
|
1979
|
-
### 17.4 Orchestrator Dispatch, Reconciliation, and Retry
|
|
1980
|
-
|
|
1981
|
-
- Dispatch sort order is priority then oldest creation time
|
|
1982
|
-
- `Todo` issue with non-terminal blockers is not eligible
|
|
1983
|
-
- `Todo` issue with terminal blockers is eligible
|
|
1984
|
-
- Active-state issue refresh updates running entry state
|
|
1985
|
-
- Non-active state stops running agent without workspace cleanup
|
|
1986
|
-
- Terminal state stops running agent and cleans workspace
|
|
1987
|
-
- Reconciliation with no running issues is a no-op
|
|
1988
|
-
- Normal worker exit schedules a short continuation retry (attempt 1)
|
|
1989
|
-
- Abnormal worker exit increments retries with 10s-based exponential backoff
|
|
1990
|
-
- Retry backoff cap uses configured `agent.max_retry_backoff_ms`
|
|
1991
|
-
- Retry queue entries include attempt, due time, identifier, and error
|
|
1992
|
-
- Stall detection kills stalled sessions and schedules retry
|
|
1993
|
-
- Slot exhaustion requeues retries with explicit error reason
|
|
1994
|
-
- If a snapshot API is implemented, it returns running rows, retry rows, token totals, and rate
|
|
1995
|
-
limits
|
|
1996
|
-
- If a snapshot API is implemented, timeout/unavailable cases are surfaced
|
|
1997
|
-
|
|
1998
|
-
### 17.5 Coding-Agent App-Server Client
|
|
1999
|
-
|
|
2000
|
-
- Launch command uses workspace cwd and invokes `bash -lc <codex.command>`
|
|
2001
|
-
- Session startup follows the targeted Codex app-server protocol.
|
|
2002
|
-
- Client identity/capability payloads are valid when the targeted Codex app-server protocol requires
|
|
2003
|
-
them.
|
|
2004
|
-
- Policy-related startup payloads use the implementation's documented approval/sandbox settings
|
|
2005
|
-
- Thread and turn identities exposed by the targeted protocol are extracted and used to emit
|
|
2006
|
-
`session_started`
|
|
2007
|
-
- Request/response read timeout is enforced
|
|
2008
|
-
- Turn timeout is enforced
|
|
2009
|
-
- Transport framing required by the targeted protocol is handled correctly
|
|
2010
|
-
- For stdio-based transports, diagnostic stderr handling is kept separate from the protocol stream
|
|
2011
|
-
- Command/file-change approvals are handled according to the implementation's documented policy
|
|
2012
|
-
- Unsupported dynamic tool calls are rejected without stalling the session
|
|
2013
|
-
- User input requests are handled according to the implementation's documented policy and do not
|
|
2014
|
-
stall indefinitely
|
|
2015
|
-
- Usage and rate-limit telemetry exposed by the targeted protocol is extracted
|
|
2016
|
-
- Approval, user-input-required, usage, and rate-limit signals are interpreted according to the
|
|
2017
|
-
targeted protocol
|
|
2018
|
-
- If client-side tools are implemented, session startup advertises the supported tool specs
|
|
2019
|
-
using the targeted app-server protocol
|
|
2020
|
-
- If the `linear_graphql` client-side tool extension is implemented:
|
|
2021
|
-
- the tool is advertised to the session
|
|
2022
|
-
- valid `query` / `variables` inputs execute against configured Linear auth
|
|
2023
|
-
- top-level GraphQL `errors` produce `success=false` while preserving the GraphQL body
|
|
2024
|
-
- invalid arguments, missing auth, and transport failures return structured failure payloads
|
|
2025
|
-
- unsupported tool names still fail without stalling the session
|
|
2026
|
-
|
|
2027
|
-
### 17.6 Observability
|
|
2028
|
-
|
|
2029
|
-
- Validation failures are operator-visible
|
|
2030
|
-
- Structured logging includes issue/session context fields
|
|
2031
|
-
- Logging sink failures do not crash orchestration
|
|
2032
|
-
- Token/rate-limit aggregation remains correct across repeated agent updates
|
|
2033
|
-
- If a human-readable status surface is implemented, it is driven from orchestrator state and does
|
|
2034
|
-
not affect correctness
|
|
2035
|
-
- If humanized event summaries are implemented, they cover key wrapper/agent event classes without
|
|
2036
|
-
changing orchestrator behavior
|
|
2037
|
-
|
|
2038
|
-
### 17.7 CLI and Host Lifecycle
|
|
2039
|
-
|
|
2040
|
-
- CLI accepts a positional workflow path argument (`path-to-WORKFLOW.md`)
|
|
2041
|
-
- CLI uses `./WORKFLOW.md` when no workflow path argument is provided
|
|
2042
|
-
- CLI errors on nonexistent explicit workflow path or missing default `./WORKFLOW.md`
|
|
2043
|
-
- CLI surfaces startup failure cleanly
|
|
2044
|
-
- CLI exits with success when application starts and shuts down normally
|
|
2045
|
-
- CLI exits nonzero when startup fails or the host process exits abnormally
|
|
2046
|
-
|
|
2047
|
-
### 17.8 Real Integration Profile (RECOMMENDED)
|
|
2048
|
-
|
|
2049
|
-
These checks are RECOMMENDED for production readiness and MAY be skipped in CI when credentials,
|
|
2050
|
-
network access, or external service permissions are unavailable.
|
|
2051
|
-
|
|
2052
|
-
- A real tracker smoke test can be run with valid credentials supplied by `LINEAR_API_KEY` or a
|
|
2053
|
-
documented local bootstrap mechanism (for example `~/.linear_api_key`).
|
|
2054
|
-
- Real integration tests SHOULD use isolated test identifiers/workspaces and clean up tracker
|
|
2055
|
-
artifacts when practical.
|
|
2056
|
-
- A skipped real-integration test SHOULD be reported as skipped, not silently treated as passed.
|
|
2057
|
-
- If a real-integration profile is explicitly enabled in CI or release validation, failures SHOULD
|
|
2058
|
-
fail that job.
|
|
2059
|
-
|
|
2060
|
-
## 18. Implementation Checklist (Definition of Done)
|
|
2061
|
-
|
|
2062
|
-
Use the same validation profiles as Section 17:
|
|
2063
|
-
|
|
2064
|
-
- Section 18.1 = `Core Conformance`
|
|
2065
|
-
- Section 18.2 = `Extension Conformance`
|
|
2066
|
-
- Section 18.3 = `Real Integration Profile`
|
|
2067
|
-
|
|
2068
|
-
### 18.1 REQUIRED for Conformance
|
|
2069
|
-
|
|
2070
|
-
- Workflow path selection supports explicit runtime path and cwd default
|
|
2071
|
-
- `WORKFLOW.md` loader with YAML front matter + prompt body split
|
|
2072
|
-
- Typed config layer with defaults and `$` resolution
|
|
2073
|
-
- Dynamic `WORKFLOW.md` watch/reload/re-apply for config and prompt
|
|
2074
|
-
- Polling orchestrator with single-authority mutable state
|
|
2075
|
-
- Issue tracker client with candidate fetch + state refresh + terminal fetch
|
|
2076
|
-
- Workspace manager with sanitized per-issue workspaces
|
|
2077
|
-
- Workspace lifecycle hooks (`after_create`, `before_run`, `after_run`, `before_remove`)
|
|
2078
|
-
- Hook timeout config (`hooks.timeout_ms`, default `60000`)
|
|
2079
|
-
- Coding-agent app-server subprocess client with JSON line protocol
|
|
2080
|
-
- Codex launch command config (`codex.command`, default `codex app-server`)
|
|
2081
|
-
- Strict prompt rendering with `issue` and `attempt` variables
|
|
2082
|
-
- Exponential retry queue with continuation retries after normal exit
|
|
2083
|
-
- Configurable retry backoff cap (`agent.max_retry_backoff_ms`, default 5m)
|
|
2084
|
-
- Reconciliation that stops runs on terminal/non-active tracker states
|
|
2085
|
-
- Workspace cleanup for terminal issues (startup sweep + active transition)
|
|
2086
|
-
- Structured logs with `issue_id`, `issue_identifier`, and `session_id`
|
|
2087
|
-
- Operator-visible observability (structured logs; OPTIONAL snapshot/status surface)
|
|
2088
|
-
|
|
2089
|
-
### 18.2 RECOMMENDED Extensions (Not REQUIRED for Conformance)
|
|
2090
|
-
|
|
2091
|
-
- HTTP server extension honors CLI `--port` over `server.port`, uses a safe default bind host, and
|
|
2092
|
-
exposes the baseline endpoints/error semantics in Section 13.7 if shipped.
|
|
2093
|
-
- `linear_graphql` client-side tool extension exposes raw Linear GraphQL access through the
|
|
2094
|
-
app-server session using configured Symphony auth.
|
|
2095
|
-
- TODO: Persist retry queue and session metadata across process restarts.
|
|
2096
|
-
- TODO: Make observability settings configurable in workflow front matter without prescribing UI
|
|
2097
|
-
implementation details.
|
|
2098
|
-
- TODO: Add first-class tracker write APIs (comments/state transitions) in the orchestrator instead
|
|
2099
|
-
of only via agent tools.
|
|
2100
|
-
- TODO: Add pluggable issue tracker adapters beyond Linear.
|
|
2101
|
-
|
|
2102
|
-
### 18.3 Operational Validation Before Production (RECOMMENDED)
|
|
2103
|
-
|
|
2104
|
-
- Run the `Real Integration Profile` from Section 17.8 with valid credentials and network access.
|
|
2105
|
-
- Verify hook execution and workflow path resolution on the target host OS/shell environment.
|
|
2106
|
-
- If the OPTIONAL HTTP server is shipped, verify the configured port behavior and loopback/default
|
|
2107
|
-
bind expectations on the target environment.
|
|
2108
|
-
|
|
2109
|
-
## Appendix A. SSH Worker Extension (OPTIONAL)
|
|
2110
|
-
|
|
2111
|
-
This appendix describes a common extension profile in which Symphony keeps one central
|
|
2112
|
-
orchestrator but executes worker runs on one or more remote hosts over SSH.
|
|
2113
|
-
|
|
2114
|
-
Extension config:
|
|
2115
|
-
|
|
2116
|
-
- `worker.ssh_hosts` (list of SSH host strings, OPTIONAL)
|
|
2117
|
-
- When omitted, work runs locally.
|
|
2118
|
-
- `worker.max_concurrent_agents_per_host` (positive integer, OPTIONAL)
|
|
2119
|
-
- Shared per-host cap applied across configured SSH hosts.
|
|
2120
|
-
|
|
2121
|
-
### A.1 Execution Model
|
|
2122
|
-
|
|
2123
|
-
- The orchestrator remains the single source of truth for polling, claims, retries, and
|
|
2124
|
-
reconciliation.
|
|
2125
|
-
- `worker.ssh_hosts` provides the candidate SSH destinations for remote execution.
|
|
2126
|
-
- Each worker run is assigned to one host at a time, and that host becomes part of the run's
|
|
2127
|
-
effective execution identity along with the issue workspace.
|
|
2128
|
-
- `workspace.root` is interpreted on the remote host, not on the orchestrator host.
|
|
2129
|
-
- The coding-agent app-server is launched over SSH stdio instead of as a local subprocess, so the
|
|
2130
|
-
orchestrator still owns the session lifecycle even though commands execute remotely.
|
|
2131
|
-
- Continuation turns inside one worker lifetime SHOULD stay on the same host and workspace.
|
|
2132
|
-
- A remote host SHOULD satisfy the same basic contract as a local worker environment: reachable
|
|
2133
|
-
shell, writable workspace root, coding-agent executable, and any required auth or repository
|
|
2134
|
-
prerequisites.
|
|
2135
|
-
|
|
2136
|
-
### A.2 Scheduling Notes
|
|
2137
|
-
|
|
2138
|
-
- SSH hosts MAY be treated as a pool for dispatch.
|
|
2139
|
-
- Implementations MAY prefer the previously used host on retries when that host is still
|
|
2140
|
-
available.
|
|
2141
|
-
- `worker.max_concurrent_agents_per_host` is an OPTIONAL shared per-host cap across configured SSH
|
|
2142
|
-
hosts.
|
|
2143
|
-
- When all SSH hosts are at capacity, dispatch SHOULD wait rather than silently falling back to a
|
|
2144
|
-
different execution mode.
|
|
2145
|
-
- Implementations MAY fail over to another host when the original host is unavailable before work
|
|
2146
|
-
has meaningfully started.
|
|
2147
|
-
- Once a run has already produced side effects, a transparent rerun on another host SHOULD be
|
|
2148
|
-
treated as a new attempt, not as invisible failover.
|
|
2149
|
-
|
|
2150
|
-
### A.3 Problems to Consider
|
|
2151
|
-
|
|
2152
|
-
- Remote environment drift:
|
|
2153
|
-
- Each host needs the expected shell environment, coding-agent executable, auth, and repository
|
|
2154
|
-
prerequisites.
|
|
2155
|
-
- Workspace locality:
|
|
2156
|
-
- Workspaces are usually host-local, so moving an issue to a different host is typically a cold
|
|
2157
|
-
restart unless shared storage exists.
|
|
2158
|
-
- Path and command safety:
|
|
2159
|
-
- Remote path resolution, shell quoting, and workspace-boundary checks matter more once execution
|
|
2160
|
-
crosses a machine boundary.
|
|
2161
|
-
- Startup and failover semantics:
|
|
2162
|
-
- Implementations SHOULD distinguish host-connectivity/startup failures from in-workspace agent
|
|
2163
|
-
failures so the same ticket is not accidentally re-executed on multiple hosts.
|
|
2164
|
-
- Host health and saturation:
|
|
2165
|
-
- A dead or overloaded host SHOULD reduce available capacity, not cause duplicate execution or an
|
|
2166
|
-
accidental fallback to local work.
|
|
2167
|
-
- Cleanup and observability:
|
|
2168
|
-
- Operators need to know which host owns a run, where its workspace lives, and whether cleanup
|
|
2169
|
-
happened on the right machine.
|
|
782
|
+
Minimum endpoints (if shipped):
|
|
783
|
+
|
|
784
|
+
- `GET /` — human-readable dashboard.
|
|
785
|
+
- `GET /api/v1/state` — summary view (running sessions, retry queue,
|
|
786
|
+
aggregate token/runtime totals, latest rate limits).
|
|
787
|
+
- `GET /api/v1/<issue_identifier>` — issue-specific runtime/debug details.
|
|
788
|
+
Returns `404` for unknown issues with `{"error":{"code":"issue_not_found",
|
|
789
|
+
"message":"..."}}`.
|
|
790
|
+
- `POST /api/v1/refresh` — queues an immediate tracker poll + reconciliation
|
|
791
|
+
cycle (best-effort; implementations MAY coalesce repeated requests).
|
|
792
|
+
Suggested response `202 Accepted`.
|
|
793
|
+
|
|
794
|
+
Endpoints SHOULD be read-only except for operational triggers like
|
|
795
|
+
`/refresh`. Unsupported methods on defined routes SHOULD return `405`. API
|
|
796
|
+
errors SHOULD use a JSON envelope such as
|
|
797
|
+
`{"error":{"code":"...","message":"..."}}`.
|