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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,8 @@ as `WORKFLOW.md`, then change these fields before starting Symphony:
- `codex.command`

If you want the dashboard, keep `server.port` in the workflow or pass `--port` on the CLI.
The web dashboard now opens with a server-rendered snapshot and continues updating live in the
browser over server-sent events.

For a complete reference covering every supported field with defaults and inline documentation, see
[docs/WORKFLOW.template.md](docs/WORKFLOW.template.md).
Expand Down
6 changes: 5 additions & 1 deletion docs/DEV_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,9 @@ issue to "In Review" and leave a comment summarizing what you did.
| `codex.read_timeout_ms` | Max time in ms to wait for the next Codex event before declaring stream stalled | `5000` |
| `codex.stall_timeout_ms` | Max silent time in ms before a running agent is declared stalled and stopped | `300000` |
| `server.port` | HTTP dashboard port; omit or `null` to disable | `null` |
| `observability.dashboard_enabled` | Enable live dashboard updates when the HTTP server is running | `true` |
| `observability.refresh_ms` | Dashboard heartbeat interval in ms for time-based refreshes | `1000` |
| `observability.render_interval_ms` | Minimum spacing in ms between pushed dashboard renders | `16` |

The prompt body uses **Liquid template syntax**. Available variables:
- `{{ issue.identifier }}`, `{{ issue.title }}`, `{{ issue.description }}`
Expand Down Expand Up @@ -324,5 +327,6 @@ These fields take effect on the next poll tick without restarting Symphony:
**How to watch runtime state**
- Structured JSON logs are the primary observability surface
- Launch with `--port 3000` to access the HTTP dashboard at `http://localhost:3000`
- The dashboard serves an initial HTML snapshot, then stays current over `/api/v1/events`

---
---
17 changes: 17 additions & 0 deletions docs/WORKFLOW.template.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,23 @@ server:
# Port to listen on. Set to a number to enable, or omit/null to disable.
# Default: null (disabled)
port: null

# ============================================================
# observability — Live dashboard refresh behavior (optional)
# ============================================================
observability:
# Enable live updates for the HTTP dashboard.
# Default: true
dashboard_enabled: true

# Heartbeat interval in milliseconds for live dashboard refreshes.
# Used to keep runtime counters current even when no orchestration state changes.
# Default: 1000 (1 s)
refresh_ms: 1000

# Minimum spacing between pushed dashboard renders in milliseconds.
# Default: 16 (~60 FPS upper bound)
render_interval_ms: 16
---

You are implementing work for Linear issue {{ issue.identifier }}.
Expand Down
33 changes: 33 additions & 0 deletions src/config/config-resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ import {
DEFAULT_MAX_CONCURRENT_AGENTS_BY_STATE,
DEFAULT_MAX_RETRY_BACKOFF_MS,
DEFAULT_MAX_TURNS,
DEFAULT_OBSERVABILITY_ENABLED,
DEFAULT_OBSERVABILITY_REFRESH_MS,
DEFAULT_OBSERVABILITY_RENDER_INTERVAL_MS,
DEFAULT_POLL_INTERVAL_MS,
DEFAULT_READ_TIMEOUT_MS,
DEFAULT_STALL_TIMEOUT_MS,
Expand Down Expand Up @@ -42,6 +45,7 @@ export function resolveWorkflowConfig(
const agent = asRecord(config.agent);
const codex = asRecord(config.codex);
const server = asRecord(config.server);
const observability = asRecord(config.observability);

return {
workflowPath: workflow.workflowPath,
Expand Down Expand Up @@ -109,6 +113,17 @@ export function resolveWorkflowConfig(
server: {
port: readNonNegativeInteger(server.port),
},
observability: {
dashboardEnabled:
readBoolean(observability.dashboard_enabled) ??
DEFAULT_OBSERVABILITY_ENABLED,
refreshMs:
readPositiveInteger(observability.refresh_ms) ??
DEFAULT_OBSERVABILITY_REFRESH_MS,
renderIntervalMs:
readPositiveInteger(observability.render_interval_ms) ??
DEFAULT_OBSERVABILITY_RENDER_INTERVAL_MS,
},
};
}

Expand Down Expand Up @@ -201,6 +216,24 @@ function readInteger(value: unknown): number | null {
return null;
}

function readBoolean(value: unknown): boolean | null {
if (typeof value === "boolean") {
return value;
}

if (typeof value === "string") {
const normalized = value.trim().toLowerCase();
if (normalized === "true") {
return true;
}
if (normalized === "false") {
return false;
}
}

return null;
}

function readPositiveInteger(value: unknown): number | null {
const parsed = readInteger(value);
if (parsed === null || parsed <= 0) {
Expand Down
8 changes: 8 additions & 0 deletions src/config/defaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ export const DEFAULT_CODEX_COMMAND = "codex app-server";
export const DEFAULT_TURN_TIMEOUT_MS = 3_600_000;
export const DEFAULT_READ_TIMEOUT_MS = 5_000;
export const DEFAULT_STALL_TIMEOUT_MS = 300_000;
export const DEFAULT_OBSERVABILITY_ENABLED = true;
export const DEFAULT_OBSERVABILITY_REFRESH_MS = 1_000;
export const DEFAULT_OBSERVABILITY_RENDER_INTERVAL_MS = 16;

export const DEFAULT_LINEAR_PAGE_SIZE = 50;
export const DEFAULT_LINEAR_NETWORK_TIMEOUT_MS = 30_000;
Expand Down Expand Up @@ -63,4 +66,9 @@ export const SPEC_DEFAULTS = Object.freeze({
readTimeoutMs: DEFAULT_READ_TIMEOUT_MS,
stallTimeoutMs: DEFAULT_STALL_TIMEOUT_MS,
},
observability: {
dashboardEnabled: DEFAULT_OBSERVABILITY_ENABLED,
refreshMs: DEFAULT_OBSERVABILITY_REFRESH_MS,
renderIntervalMs: DEFAULT_OBSERVABILITY_RENDER_INTERVAL_MS,
},
} as const);
7 changes: 7 additions & 0 deletions src/config/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,12 @@ export interface WorkflowServerConfig {
port: number | null;
}

export interface WorkflowObservabilityConfig {
dashboardEnabled: boolean;
refreshMs: number;
renderIntervalMs: number;
}

export interface ResolvedWorkflowConfig {
workflowPath: string;
promptTemplate: string;
Expand All @@ -54,6 +60,7 @@ export interface ResolvedWorkflowConfig {
agent: WorkflowAgentConfig;
codex: WorkflowCodexConfig;
server: WorkflowServerConfig;
observability: WorkflowObservabilityConfig;
}

export interface DispatchValidationFailure {
Expand Down
Loading