Showing preview only (833K chars total). Download the full file or copy to clipboard to get everything.
Repository: openai/symphony
Branch: main
Commit: a164593aacb3
Files: 98
Total size: 794.9 KB
Directory structure:
gitextract_auczxnks/
├── .codex/
│ ├── skills/
│ │ ├── commit/
│ │ │ └── SKILL.md
│ │ ├── debug/
│ │ │ └── SKILL.md
│ │ ├── land/
│ │ │ ├── SKILL.md
│ │ │ └── land_watch.py
│ │ ├── linear/
│ │ │ └── SKILL.md
│ │ ├── pull/
│ │ │ └── SKILL.md
│ │ └── push/
│ │ └── SKILL.md
│ └── worktree_init.sh
├── .github/
│ ├── pull_request_template.md
│ └── workflows/
│ ├── make-all.yml
│ └── pr-description-lint.yml
├── LICENSE
├── NOTICE
├── README.md
├── SPEC.md
└── elixir/
├── .formatter.exs
├── .gitattributes
├── .gitignore
├── AGENTS.md
├── Makefile
├── README.md
├── WORKFLOW.md
├── config/
│ └── config.exs
├── docs/
│ ├── logging.md
│ └── token_accounting.md
├── lib/
│ ├── mix/
│ │ └── tasks/
│ │ ├── pr_body.check.ex
│ │ ├── specs.check.ex
│ │ └── workspace.before_remove.ex
│ ├── symphony_elixir/
│ │ ├── agent_runner.ex
│ │ ├── cli.ex
│ │ ├── codex/
│ │ │ ├── app_server.ex
│ │ │ └── dynamic_tool.ex
│ │ ├── config/
│ │ │ └── schema.ex
│ │ ├── config.ex
│ │ ├── http_server.ex
│ │ ├── linear/
│ │ │ ├── adapter.ex
│ │ │ ├── client.ex
│ │ │ └── issue.ex
│ │ ├── log_file.ex
│ │ ├── orchestrator.ex
│ │ ├── path_safety.ex
│ │ ├── prompt_builder.ex
│ │ ├── specs_check.ex
│ │ ├── ssh.ex
│ │ ├── status_dashboard.ex
│ │ ├── tracker/
│ │ │ └── memory.ex
│ │ ├── tracker.ex
│ │ ├── workflow.ex
│ │ ├── workflow_store.ex
│ │ └── workspace.ex
│ ├── symphony_elixir.ex
│ └── symphony_elixir_web/
│ ├── components/
│ │ └── layouts.ex
│ ├── controllers/
│ │ ├── observability_api_controller.ex
│ │ └── static_asset_controller.ex
│ ├── endpoint.ex
│ ├── error_html.ex
│ ├── error_json.ex
│ ├── live/
│ │ └── dashboard_live.ex
│ ├── observability_pubsub.ex
│ ├── presenter.ex
│ ├── router.ex
│ └── static_assets.ex
├── mise.toml
├── mix.exs
├── priv/
│ └── static/
│ └── dashboard.css
└── test/
├── fixtures/
│ └── status_dashboard_snapshots/
│ ├── backoff_queue.evidence.md
│ ├── backoff_queue.snapshot.txt
│ ├── credits_unlimited.evidence.md
│ ├── credits_unlimited.snapshot.txt
│ ├── idle.evidence.md
│ ├── idle.snapshot.txt
│ ├── idle_with_dashboard_url.evidence.md
│ ├── idle_with_dashboard_url.snapshot.txt
│ ├── super_busy.evidence.md
│ └── super_busy.snapshot.txt
├── mix/
│ └── tasks/
│ ├── pr_body_check_test.exs
│ ├── specs_check_task_test.exs
│ └── workspace_before_remove_test.exs
├── support/
│ ├── live_e2e_docker/
│ │ ├── Dockerfile
│ │ ├── docker-compose.yml
│ │ ├── live_worker_entrypoint.sh
│ │ └── symphony-live-worker.conf
│ ├── snapshot_support.exs
│ └── test_support.exs
├── symphony_elixir/
│ ├── app_server_test.exs
│ ├── cli_test.exs
│ ├── core_test.exs
│ ├── dynamic_tool_test.exs
│ ├── extensions_test.exs
│ ├── live_e2e_test.exs
│ ├── log_file_test.exs
│ ├── observability_pubsub_test.exs
│ ├── orchestrator_status_test.exs
│ ├── specs_check_test.exs
│ ├── ssh_test.exs
│ ├── status_dashboard_snapshot_test.exs
│ └── workspace_and_config_test.exs
└── test_helper.exs
================================================
FILE CONTENTS
================================================
================================================
FILE: .codex/skills/commit/SKILL.md
================================================
---
name: commit
description:
Create a well-formed git commit from current changes using session history for
rationale and summary; use when asked to commit, prepare a commit message, or
finalize staged work.
---
# Commit
## Goals
- Produce a commit that reflects the actual code changes and the session
context.
- Follow common git conventions (type prefix, short subject, wrapped body).
- Include both summary and rationale in the body.
## Inputs
- Codex session history for intent and rationale.
- `git status`, `git diff`, and `git diff --staged` for actual changes.
- Repo-specific commit conventions if documented.
## Steps
1. Read session history to identify scope, intent, and rationale.
2. Inspect the working tree and staged changes (`git status`, `git diff`,
`git diff --staged`).
3. Stage intended changes, including new files (`git add -A`) after confirming
scope.
4. Sanity-check newly added files; if anything looks random or likely ignored
(build artifacts, logs, temp files), flag it to the user before committing.
5. If staging is incomplete or includes unrelated files, fix the index or ask
for confirmation.
6. Choose a conventional type and optional scope that match the change (e.g.,
`feat(scope): ...`, `fix(scope): ...`, `refactor(scope): ...`).
7. Write a subject line in imperative mood, <= 72 characters, no trailing
period.
8. Write a body that includes:
- Summary of key changes (what changed).
- Rationale and trade-offs (why it changed).
- Tests or validation run (or explicit note if not run).
9. Append a `Co-authored-by` trailer for Codex using `Codex <codex@openai.com>`
unless the user explicitly requests a different identity.
10. Wrap body lines at 72 characters.
11. Create the commit message with a here-doc or temp file and use
`git commit -F <file>` so newlines are literal (avoid `-m` with `\n`).
12. Commit only when the message matches the staged changes: if the staged diff
includes unrelated files or the message describes work that isn't staged,
fix the index or revise the message before committing.
## Output
- A single commit created with `git commit` whose message reflects the session.
## Template
Type and scope are examples only; adjust to fit the repo and changes.
```
<type>(<scope>): <short summary>
Summary:
- <what changed>
- <what changed>
Rationale:
- <why>
- <why>
Tests:
- <command or "not run (reason)">
Co-authored-by: Codex <codex@openai.com>
```
================================================
FILE: .codex/skills/debug/SKILL.md
================================================
---
name: debug
description:
Investigate stuck runs and execution failures by tracing Symphony and Codex
logs with issue/session identifiers; use when runs stall, retry repeatedly, or
fail unexpectedly.
---
# Debug
## Goals
- Find why a run is stuck, retrying, or failing.
- Correlate Linear issue identity to a Codex session quickly.
- Read the right logs in the right order to isolate root cause.
## Log Sources
- Primary runtime log: `log/symphony.log`
- Default comes from `SymphonyElixir.LogFile` (`log/symphony.log`).
- Includes orchestrator, agent runner, and Codex app-server lifecycle logs.
- Rotated runtime logs: `log/symphony.log*`
- Check these when the relevant run is older.
## Correlation Keys
- `issue_identifier`: human ticket key (example: `MT-625`)
- `issue_id`: Linear UUID (stable internal ID)
- `session_id`: Codex thread-turn pair (`<thread_id>-<turn_id>`)
`elixir/docs/logging.md` requires these fields for issue/session lifecycle logs. Use
them as your join keys during debugging.
## Quick Triage (Stuck Run)
1. Confirm scheduler/worker symptoms for the ticket.
2. Find recent lines for the ticket (`issue_identifier` first).
3. Extract `session_id` from matching lines.
4. Trace that `session_id` across start, stream, completion/failure, and stall
handling logs.
5. Decide class of failure: timeout/stall, app-server startup failure, turn
failure, or orchestrator retry loop.
## Commands
```bash
# 1) Narrow by ticket key (fastest entry point)
rg -n "issue_identifier=MT-625" log/symphony.log*
# 2) If needed, narrow by Linear UUID
rg -n "issue_id=<linear-uuid>" log/symphony.log*
# 3) Pull session IDs seen for that ticket
rg -o "session_id=[^ ;]+" log/symphony.log* | sort -u
# 4) Trace one session end-to-end
rg -n "session_id=<thread>-<turn>" log/symphony.log*
# 5) Focus on stuck/retry signals
rg -n "Issue stalled|scheduling retry|turn_timeout|turn_failed|Codex session failed|Codex session ended with error" log/symphony.log*
```
## Investigation Flow
1. Locate the ticket slice:
- Search by `issue_identifier=<KEY>`.
- If noise is high, add `issue_id=<UUID>`.
2. Establish timeline:
- Identify first `Codex session started ... session_id=...`.
- Follow with `Codex session completed`, `ended with error`, or worker exit
lines.
3. Classify the problem:
- Stall loop: `Issue stalled ... restarting with backoff`.
- App-server startup: `Codex session failed ...`.
- Turn execution failure: `turn_failed`, `turn_cancelled`, `turn_timeout`, or
`ended with error`.
- Worker crash: `Agent task exited ... reason=...`.
4. Validate scope:
- Check whether failures are isolated to one issue/session or repeating across
multiple tickets.
5. Capture evidence:
- Save key log lines with timestamps, `issue_identifier`, `issue_id`, and
`session_id`.
- Record probable root cause and the exact failing stage.
## Reading Codex Session Logs
In Symphony, Codex session diagnostics are emitted into `log/symphony.log` and
keyed by `session_id`. Read them as a lifecycle:
1. `Codex session started ... session_id=...`
2. Session stream/lifecycle events for the same `session_id`
3. Terminal event:
- `Codex session completed ...`, or
- `Codex session ended with error ...`, or
- `Issue stalled ... restarting with backoff`
For one specific session investigation, keep the trace narrow:
1. Capture one `session_id` for the ticket.
2. Build a timestamped slice for only that session:
- `rg -n "session_id=<thread>-<turn>" log/symphony.log*`
3. Mark the exact failing stage:
- Startup failure before stream events (`Codex session failed ...`).
- Turn/runtime failure after stream events (`turn_*` / `ended with error`).
- Stall recovery (`Issue stalled ... restarting with backoff`).
4. Pair findings with `issue_identifier` and `issue_id` from nearby lines to
confirm you are not mixing concurrent retries.
Always pair session findings with `issue_identifier`/`issue_id` to avoid mixing
concurrent runs.
## Notes
- Prefer `rg` over `grep` for speed on large logs.
- Check rotated logs (`log/symphony.log*`) before concluding data is missing.
- If required context fields are missing in new log statements, align with
`elixir/docs/logging.md` conventions.
================================================
FILE: .codex/skills/land/SKILL.md
================================================
---
name: land
description:
Land a PR by monitoring conflicts, resolving them, waiting for checks, and
squash-merging when green; use when asked to land, merge, or shepherd a PR to
completion.
---
# Land
## Goals
- Ensure the PR is conflict-free with main.
- Keep CI green and fix failures when they occur.
- Squash-merge the PR once checks pass.
- Do not yield to the user until the PR is merged; keep the watcher loop running
unless blocked.
- No need to delete remote branches after merge; the repo auto-deletes head
branches.
## Preconditions
- `gh` CLI is authenticated.
- You are on the PR branch with a clean working tree.
## Steps
1. Locate the PR for the current branch.
2. Confirm the full gauntlet is green locally before any push.
3. If the working tree has uncommitted changes, commit with the `commit` skill
and push with the `push` skill before proceeding.
4. Check mergeability and conflicts against main.
5. If conflicts exist, use the `pull` skill to fetch/merge `origin/main` and
resolve conflicts, then use the `push` skill to publish the updated branch.
6. Ensure Codex review comments (if present) are acknowledged and any required
fixes are handled before merging.
7. Watch checks until complete.
8. If checks fail, pull logs, fix the issue, commit with the `commit` skill,
push with the `push` skill, and re-run checks.
9. When all checks are green and review feedback is addressed, squash-merge and
delete the branch using the PR title/body for the merge subject/body.
10. **Context guard:** Before implementing review feedback, confirm it does not
conflict with the user’s stated intent or task context. If it conflicts,
respond inline with a justification and ask the user before changing code.
11. **Pushback template:** When disagreeing, reply inline with: acknowledge +
rationale + offer alternative.
12. **Ambiguity gate:** When ambiguity blocks progress, use the clarification
flow (assign PR to current GH user, mention them, wait for response). Do not
implement until ambiguity is resolved.
- If you are confident you know better than the reviewer, you may proceed
without asking the user, but reply inline with your rationale.
13. **Per-comment mode:** For each review comment, choose one of: accept,
clarify, or push back. Reply inline (or in the issue thread for Codex
reviews) stating the mode before changing code.
14. **Reply before change:** Always respond with intended action before pushing
code changes (inline for review comments, issue thread for Codex reviews).
## Commands
```
# Ensure branch and PR context
branch=$(git branch --show-current)
pr_number=$(gh pr view --json number -q .number)
pr_title=$(gh pr view --json title -q .title)
pr_body=$(gh pr view --json body -q .body)
# Check mergeability and conflicts
mergeable=$(gh pr view --json mergeable -q .mergeable)
if [ "$mergeable" = "CONFLICTING" ]; then
# Run the `pull` skill to handle fetch + merge + conflict resolution.
# Then run the `push` skill to publish the updated branch.
fi
# Preferred: use the Async Watch Helper below. The manual loop is a fallback
# when Python cannot run or the helper script is unavailable.
# Wait for review feedback: Codex reviews arrive as issue comments that start
# with "## Codex Review — <persona>". Treat them like reviewer feedback: reply
# with a `[codex]` issue comment acknowledging the findings and whether you're
# addressing or deferring them.
while true; do
gh api repos/{owner}/{repo}/issues/"$pr_number"/comments \
--jq '.[] | select(.body | startswith("## Codex Review")) | .id' | rg -q '.' \
&& break
sleep 10
done
# Watch checks
if ! gh pr checks --watch; then
gh pr checks
# Identify failing run and inspect logs
# gh run list --branch "$branch"
# gh run view <run-id> --log
exit 1
fi
# Squash-merge (remote branches auto-delete on merge in this repo)
gh pr merge --squash --subject "$pr_title" --body "$pr_body"
```
## Async Watch Helper
Preferred: use the asyncio watcher to monitor review comments, CI, and head
updates in parallel:
```
python3 .codex/skills/land/land_watch.py
```
Exit codes:
- 2: Review comments detected (address feedback)
- 3: CI checks failed
- 4: PR head updated (autofix commit detected)
## Failure Handling
- If checks fail, pull details with `gh pr checks` and `gh run view --log`, then
fix locally, commit with the `commit` skill, push with the `push` skill, and
re-run the watch.
- Use judgment to identify flaky failures. If a failure is a flake (e.g., a
timeout on only one platform), you may proceed without fixing it.
- If CI pushes an auto-fix commit (authored by GitHub Actions), it does not
trigger a fresh CI run. Detect the updated PR head, pull locally, merge
`origin/main` if needed, add a real author commit, and force-push to retrigger
CI, then restart the checks loop.
- If all jobs fail with corrupted pnpm lockfile errors on the merge commit, the
remediation is to fetch latest `origin/main`, merge, force-push, and rerun CI.
- If mergeability is `UNKNOWN`, wait and re-check.
- Do not merge while review comments (human or Codex review) are outstanding.
- Codex review jobs retry on failure and are non-blocking; use the presence of
`## Codex Review — <persona>` issue comments (not job status) as the signal
that review feedback is available.
- Do not enable auto-merge; this repo has no required checks so auto-merge can
skip tests.
- If the remote PR branch advanced due to your own prior force-push or merge,
avoid redundant merges; re-run the formatter locally if needed and
`git push --force-with-lease`.
## Review Handling
- Codex reviews now arrive as issue comments posted by GitHub Actions. They
start with `## Codex Review — <persona>` and include the reviewer’s
methodology + guardrails used. Treat these as feedback that must be
acknowledged before merge.
- Human review comments are blocking and must be addressed (responded to and
resolved) before requesting a new review or merging.
- If multiple reviewers comment in the same thread, respond to each comment
(batching is fine) before closing the thread.
- Fetch review comments via `gh api` and reply with a prefixed comment.
- Use review comment endpoints (not issue comments) to find inline feedback:
- List PR review comments:
```
gh api repos/{owner}/{repo}/pulls/<pr_number>/comments
```
- PR issue comments (top-level discussion):
```
gh api repos/{owner}/{repo}/issues/<pr_number>/comments
```
- Reply to a specific review comment:
```
gh api -X POST /repos/{owner}/{repo}/pulls/<pr_number>/comments \
-f body='[codex] <response>' -F in_reply_to=<comment_id>
```
- `in_reply_to` must be the numeric review comment id (e.g., `2710521800`), not
the GraphQL node id (e.g., `PRRC_...`), and the endpoint must include the PR
number (`/pulls/<pr_number>/comments`).
- If GraphQL review reply mutation is forbidden, use REST.
- A 404 on reply typically means the wrong endpoint (missing PR number) or
insufficient scope; verify by listing comments first.
- All GitHub comments generated by this agent must be prefixed with `[codex]`.
- For Codex review issue comments, reply in the issue thread (not a review
thread) with `[codex]` and state whether you will address the feedback now or
defer it (include rationale).
- If feedback requires changes:
- For inline review comments (human), reply with intended fixes
(`[codex] ...`) **as an inline reply to the original review comment** using
the review comment endpoint and `in_reply_to` (do not use issue comments for
this).
- Implement fixes, commit, push.
- Reply with the fix details and commit sha (`[codex] ...`) in the same place
you acknowledged the feedback (issue comment for Codex reviews, inline reply
for review comments).
- The land watcher treats Codex review issue comments as unresolved until a
newer `[codex]` issue comment is posted acknowledging the findings.
- Only request a new Codex review when you need a rerun (e.g., after new
commits). Do not request one without changes since the last review.
- Before requesting a new Codex review, re-run the land watcher and ensure
there are zero outstanding review comments (all have `[codex]` inline
replies).
- After pushing new commits, the Codex review workflow will rerun on PR
synchronization (or you can re-run the workflow manually). Post a concise
root-level summary comment so reviewers have the latest delta:
```
[codex] Changes since last review:
- <short bullets of deltas>
Commits: <sha>, <sha>
Tests: <commands run>
```
- Only request a new review if there is at least one new commit since the
previous request.
- Wait for the next Codex review comment before merging.
## Scope + PR Metadata
- The PR title and description should reflect the full scope of the change, not
just the most recent fix.
- If review feedback expands scope, decide whether to include it now or defer
it. You can accept, defer, or decline feedback. If deferring or declining,
call it out in the root-level `[codex]` update with a brief reason (e.g.,
out-of-scope, conflicts with intent, unnecessary).
- Correctness issues raised in review comments should be addressed. If you plan
to defer or decline a correctness concern, validate first and explain why the
concern does not apply.
- Classify each review comment as one of: correctness, design, style,
clarification, scope.
- For correctness feedback, provide concrete validation (test, log, or
reasoning) before closing it.
- When accepting feedback, include a one-line rationale in the root-level
update.
- When declining feedback, offer a brief alternative or follow-up trigger.
- Prefer a single consolidated "review addressed" root-level comment after a
batch of fixes instead of many small updates.
- For doc feedback, confirm the doc change matches behavior (no doc-only edits
to appease review).
================================================
FILE: .codex/skills/land/land_watch.py
================================================
#!/usr/bin/env python3
import asyncio
import json
import random
import re
from dataclasses import dataclass
from datetime import datetime
from typing import Any
POLL_SECONDS = 10
CHECKS_APPEAR_TIMEOUT_SECONDS = 120
CODEX_BOTS = {
"chatgpt-codex-connector[bot]",
"github-actions[bot]",
"codex-gc-app[bot]",
"app/codex-gc-app",
}
MAX_GH_RETRIES = 5
BASE_GH_BACKOFF_SECONDS = 2
@dataclass
class PrInfo:
number: int
url: str
head_sha: str
mergeable: str | None
merge_state: str | None
class RateLimitError(RuntimeError):
pass
def is_rate_limit_error(error: str) -> bool:
return "HTTP 429" in error or "rate limit" in error.lower()
async def run_gh(*args: str) -> str:
max_delay = BASE_GH_BACKOFF_SECONDS * (2 ** (MAX_GH_RETRIES - 1))
delay_seconds = BASE_GH_BACKOFF_SECONDS
last_error = "gh command failed"
for attempt in range(1, MAX_GH_RETRIES + 1):
proc = await asyncio.create_subprocess_exec(
"gh",
*args,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
stdout, stderr = await proc.communicate()
if proc.returncode == 0:
return stdout.decode()
error = stderr.decode().strip() or "gh command failed"
if not is_rate_limit_error(error):
raise RuntimeError(error)
last_error = error
if attempt >= MAX_GH_RETRIES:
break
jitter = random.uniform(0, delay_seconds)
await asyncio.sleep(min(delay_seconds + jitter, max_delay))
delay_seconds = min(delay_seconds * 2, max_delay)
raise RateLimitError(last_error)
async def get_pr_info() -> PrInfo:
data = await run_gh(
"pr",
"view",
"--json",
"number,url,headRefOid,mergeable,mergeStateStatus",
)
parsed = json.loads(data)
return PrInfo(
number=parsed["number"],
url=parsed["url"],
head_sha=parsed["headRefOid"],
mergeable=parsed.get("mergeable"),
merge_state=parsed.get("mergeStateStatus"),
)
async def get_paginated_list(endpoint: str) -> list[dict[str, Any]]:
page = 1
items: list[dict[str, Any]] = []
while True:
data = await run_gh(
"api",
"--method",
"GET",
endpoint,
"-f",
"per_page=100",
"-f",
f"page={page}",
)
batch = json.loads(data)
if not batch:
break
items.extend(batch)
page += 1
return items
async def get_issue_comments(pr_number: int) -> list[dict[str, Any]]:
return await get_paginated_list(
f"repos/{{owner}}/{{repo}}/issues/{pr_number}/comments",
)
async def get_review_comments(pr_number: int) -> list[dict[str, Any]]:
return await get_paginated_list(
f"repos/{{owner}}/{{repo}}/pulls/{pr_number}/comments",
)
async def get_reviews(pr_number: int) -> list[dict[str, Any]]:
page = 1
reviews: list[dict[str, Any]] = []
while True:
data = await run_gh(
"api",
"--method",
"GET",
f"repos/{{owner}}/{{repo}}/pulls/{pr_number}/reviews",
"-f",
"per_page=100",
"-f",
f"page={page}",
)
batch = json.loads(data)
if not batch:
break
reviews.extend(batch)
page += 1
return reviews
async def get_check_runs(head_sha: str) -> list[dict[str, Any]]:
page = 1
check_runs: list[dict[str, Any]] = []
while True:
data = await run_gh(
"api",
"--method",
"GET",
f"repos/{{owner}}/{{repo}}/commits/{head_sha}/check-runs",
"-f",
"per_page=100",
"-f",
f"page={page}",
)
payload = json.loads(data)
batch = payload.get("check_runs", [])
if not batch:
break
check_runs.extend(batch)
total_count = payload.get("total_count")
if total_count is not None and len(check_runs) >= total_count:
break
page += 1
return check_runs
def parse_time(value: str) -> datetime:
normalized = value.replace("Z", "+00:00")
return datetime.fromisoformat(normalized)
CONTROL_CHARS_RE = re.compile(r"[\x00-\x08\x0b-\x1f\x7f-\x9f]")
def sanitize_terminal_output(value: str) -> str:
return CONTROL_CHARS_RE.sub("", value)
def check_timestamp(check: dict[str, Any]) -> datetime | None:
for key in ("completed_at", "started_at", "run_started_at", "created_at"):
value = check.get(key)
if value:
return parse_time(value)
return None
def dedupe_check_runs(check_runs: list[dict[str, Any]]) -> list[dict[str, Any]]:
latest_by_name: dict[str, dict[str, Any]] = {}
for check in check_runs:
name = check.get("name", "unknown")
timestamp = check_timestamp(check)
if name not in latest_by_name:
latest_by_name[name] = check
continue
existing = latest_by_name[name]
existing_timestamp = check_timestamp(existing)
if timestamp is None:
continue
if existing_timestamp is None or timestamp > existing_timestamp:
latest_by_name[name] = check
return list(latest_by_name.values())
def summarize_checks(check_runs: list[dict[str, Any]]) -> tuple[bool, bool, list[str]]:
if not check_runs:
return True, False, ["no checks reported"]
check_runs = dedupe_check_runs(check_runs)
pending = False
failed = False
failures: list[str] = []
for check in check_runs:
status = check.get("status")
conclusion = check.get("conclusion")
name = check.get("name", "unknown")
if status != "completed":
pending = True
continue
if conclusion not in ("success", "skipped", "neutral"):
failed = True
failures.append(f"{name}: {conclusion}")
return pending, failed, failures
def latest_review_request_at(comments: list[dict[str, Any]]) -> datetime | None:
latest: datetime | None = None
for comment in comments:
if is_codex_bot_user(comment.get("user", {})):
continue
body = comment.get("body") or ""
if "@codex review" not in body:
continue
timestamp = comment_time(comment)
if timestamp is None:
continue
if latest is None or timestamp > latest:
latest = timestamp
return latest
def filter_codex_comments(
comments: list[dict[str, Any]],
review_requested_at: datetime | None,
) -> list[dict[str, Any]]:
latest_codex_reply = latest_codex_reply_by_thread(comments)
latest_issue_ack = latest_codex_issue_reply_time(comments)
codex_comments = [c for c in comments if is_codex_bot_user(c.get("user", {}))]
filtered: list[dict[str, Any]] = []
for comment in codex_comments:
created_time = comment_time(comment)
if created_time is None:
continue
if review_requested_at is not None and created_time <= review_requested_at:
continue
is_threaded = bool(
comment.get("in_reply_to_id") or comment.get("pull_request_review_id")
)
if not is_threaded:
if latest_issue_ack is not None and created_time <= latest_issue_ack:
continue
else:
thread_root = thread_root_id(comment)
last_reply = None
if thread_root is not None:
last_reply = latest_codex_reply.get(thread_root)
if last_reply and last_reply > created_time:
continue
filtered.append(comment)
return filtered
def is_codex_bot_user(user: dict[str, Any]) -> bool:
login = user.get("login") or ""
return login in CODEX_BOTS
def is_bot_user(user: dict[str, Any]) -> bool:
login = user.get("login") or ""
if is_codex_bot_user(user):
return True
if user.get("type") == "Bot":
return True
return login.endswith("[bot]")
def is_codex_reply_body(body: str) -> bool:
return body.startswith("[codex]")
def is_codex_review_body(body: str) -> bool:
return body.startswith("## Codex Review")
def latest_codex_issue_reply_time(
comments: list[dict[str, Any]],
) -> datetime | None:
latest: datetime | None = None
for comment in comments:
body = (comment.get("body") or "").strip()
if not is_codex_reply_body(body):
continue
created_time = comment_time(comment)
if created_time is None:
continue
if latest is None or created_time > latest:
latest = created_time
return latest
def filter_human_issue_comments(comments: list[dict[str, Any]]) -> list[dict[str, Any]]:
latest_ack = latest_codex_issue_reply_time(comments)
filtered: list[dict[str, Any]] = []
for comment in comments:
if is_bot_user(comment.get("user", {})):
continue
body = (comment.get("body") or "").strip()
if is_codex_reply_body(body):
continue
if is_codex_review_body(body):
continue
if "@codex review" in body:
continue
created_time = comment_time(comment)
if (
latest_ack is not None
and created_time is not None
and created_time <= latest_ack
):
continue
filtered.append(comment)
return filtered
def filter_codex_review_issue_comments(
comments: list[dict[str, Any]],
) -> list[dict[str, Any]]:
latest_ack = latest_codex_issue_reply_time(comments)
filtered: list[dict[str, Any]] = []
for comment in comments:
body = (comment.get("body") or "").strip()
if not is_codex_review_body(body):
continue
created_time = comment_time(comment)
if (
latest_ack is not None
and created_time is not None
and created_time <= latest_ack
):
continue
filtered.append(comment)
return filtered
def thread_root_id(comment: dict[str, Any]) -> int | None:
return comment.get("in_reply_to_id") or comment.get("id")
def comment_time(comment: dict[str, Any]) -> datetime | None:
timestamp = comment.get("updated_at") or comment.get("created_at")
if not timestamp:
return None
return parse_time(timestamp)
def latest_codex_reply_by_thread(
comments: list[dict[str, Any]],
) -> dict[int, datetime]:
latest: dict[int, datetime] = {}
for comment in comments:
body = (comment.get("body") or "").strip()
if not is_codex_reply_body(body):
continue
thread_root = thread_root_id(comment)
created_time = comment_time(comment)
if thread_root is None or created_time is None:
continue
existing = latest.get(thread_root)
if existing is None or created_time > existing:
latest[thread_root] = created_time
return latest
def filter_human_review_comments(
comments: list[dict[str, Any]],
) -> list[dict[str, Any]]:
latest_codex_reply = latest_codex_reply_by_thread(comments)
filtered: list[dict[str, Any]] = []
for comment in comments:
if is_bot_user(comment.get("user", {})):
continue
body = (comment.get("body") or "").strip()
if is_codex_reply_body(body):
continue
thread_root = thread_root_id(comment)
created_time = comment_time(comment)
last_codex_reply = None
if thread_root is not None:
last_codex_reply = latest_codex_reply.get(thread_root)
if last_codex_reply and created_time and created_time <= last_codex_reply:
continue
filtered.append(comment)
return filtered
def is_blocking_review(
review: dict[str, Any],
review_requested_at: datetime | None,
) -> bool:
created_at = review.get("submitted_at") or review.get("created_at")
if not created_at:
return False
user_login = review.get("user", {}).get("login")
created_time = parse_time(created_at)
if (
user_login in CODEX_BOTS
and review_requested_at is not None
and created_time <= review_requested_at
):
return False
body = (review.get("body") or "").strip()
state = review.get("state")
if user_login in CODEX_BOTS:
return state == "CHANGES_REQUESTED"
if body.startswith("[codex]") or state in ("APPROVED", "DISMISSED"):
return False
blocking = False
if body or state == "CHANGES_REQUESTED":
blocking = True
elif state == "COMMENTED":
blocking = False
elif state:
blocking = state not in ("APPROVED", "DISMISSED")
return blocking
def review_timestamp(review: dict[str, Any]) -> datetime | None:
created_at = review.get("submitted_at") or review.get("created_at")
if not created_at:
return None
return parse_time(created_at)
def dedupe_reviews(reviews: list[dict[str, Any]]) -> list[dict[str, Any]]:
latest_by_user: dict[str, dict[str, Any]] = {}
for review in reviews:
user_login = review.get("user", {}).get("login")
if not user_login:
continue
timestamp = review_timestamp(review)
if user_login not in latest_by_user:
latest_by_user[user_login] = review
continue
existing = latest_by_user[user_login]
existing_timestamp = review_timestamp(existing)
if timestamp is None:
continue
if existing_timestamp is None or timestamp > existing_timestamp:
latest_by_user[user_login] = review
return list(latest_by_user.values())
def filter_blocking_reviews(
reviews: list[dict[str, Any]],
review_requested_at: datetime | None,
) -> list[dict[str, Any]]:
return [
review
for review in dedupe_reviews(reviews)
if is_blocking_review(review, review_requested_at)
]
def is_merge_conflicting(pr: PrInfo) -> bool:
return pr.mergeable == "CONFLICTING" or pr.merge_state == "DIRTY"
async def fetch_review_context(
pr_number: int,
) -> tuple[
list[dict[str, Any]],
list[dict[str, Any]],
list[dict[str, Any]],
datetime | None,
]:
issue_comments = await get_issue_comments(pr_number)
review_request_at = latest_review_request_at(issue_comments)
review_comments = await get_review_comments(pr_number)
reviews = await get_reviews(pr_number)
return issue_comments, review_comments, reviews, review_request_at
def raise_on_human_feedback(
issue_comments: list[dict[str, Any]],
review_comments: list[dict[str, Any]],
reviews: list[dict[str, Any]],
review_request_at: datetime | None,
) -> None:
human_issue_comments = filter_human_issue_comments(issue_comments)
codex_review_comments = filter_codex_review_issue_comments(issue_comments)
human_review_comments = filter_human_review_comments(review_comments)
if human_issue_comments or human_review_comments or codex_review_comments:
print("Review comments detected. Address before merge.")
print(
"Reminder: decide whether feedback stays in scope; defer if needed "
"and note in your root-level update.",
)
raise SystemExit(2)
blocking_reviews = filter_blocking_reviews(reviews, review_request_at)
if blocking_reviews:
print("Review states/comments detected. Address before merge.")
print(
"Reminder: keep PR title/description aligned with the full scope "
"when changes expand.",
)
raise SystemExit(2)
async def wait_for_codex(pr_number: int, checks_done: asyncio.Event) -> None:
print("Waiting for review feedback...", flush=True)
while True:
(
issue_comments,
review_comments,
reviews,
review_request_at,
) = await fetch_review_context(pr_number)
bot_issue_comments = filter_codex_comments(issue_comments, review_request_at)
bot_review_comments = filter_codex_comments(review_comments, review_request_at)
bot_comments = bot_issue_comments + bot_review_comments
raise_on_human_feedback(
issue_comments,
review_comments,
reviews,
review_request_at,
)
if bot_comments:
latest = max(
bot_comments,
key=lambda comment: parse_time(comment["created_at"]),
)
body = sanitize_terminal_output(latest.get("body") or "").strip()
if body:
print("Codex left comments. Address feedback before merge.")
print(body)
raise SystemExit(2)
if checks_done.is_set():
return
await asyncio.sleep(POLL_SECONDS)
async def wait_for_checks(head_sha: str, checks_done: asyncio.Event) -> None:
print("Waiting for CI checks...", flush=True)
empty_seconds = 0
while True:
check_runs = await get_check_runs(head_sha)
if not check_runs:
empty_seconds += POLL_SECONDS
if empty_seconds >= CHECKS_APPEAR_TIMEOUT_SECONDS:
print(
"No checks detected after 120s; check CI configuration",
)
raise SystemExit(3)
await asyncio.sleep(POLL_SECONDS)
continue
empty_seconds = 0
pending, failed, failures = summarize_checks(check_runs)
if failed:
print("Checks failed:")
for failure in failures:
print(f"- {failure}")
raise SystemExit(3)
if not pending:
print("Checks passed")
checks_done.set()
return
await asyncio.sleep(POLL_SECONDS)
async def watch_pr() -> None:
pr = await get_pr_info()
if is_merge_conflicting(pr):
print(
"PR has merge conflicts. Resolve/rebase against main and push before "
"running land_watch again.",
)
raise SystemExit(5)
head_sha = pr.head_sha
checks_done = asyncio.Event()
codex_task = asyncio.create_task(wait_for_codex(pr.number, checks_done))
checks_task = asyncio.create_task(wait_for_checks(head_sha, checks_done))
async def head_monitor() -> None:
while True:
current = await get_pr_info()
if is_merge_conflicting(current):
print(
"PR has merge conflicts. Resolve/rebase against main and push "
"before running land_watch again.",
)
raise SystemExit(5)
if current.head_sha != head_sha:
print("PR head updated; pull/amend/force-push to retrigger CI")
raise SystemExit(4)
await asyncio.sleep(POLL_SECONDS)
monitor_task = asyncio.create_task(head_monitor())
success_task = asyncio.gather(codex_task, checks_task)
done, pending = await asyncio.wait(
[monitor_task, success_task],
return_when=asyncio.FIRST_COMPLETED,
)
for task in pending:
task.cancel()
for task in done:
exc = task.exception()
if exc:
raise exc
if __name__ == "__main__":
try:
asyncio.run(watch_pr())
except SystemExit as exc:
raise SystemExit(exc.code) from None
================================================
FILE: .codex/skills/linear/SKILL.md
================================================
---
name: linear
description: |
Use Symphony's `linear_graphql` client tool for raw Linear GraphQL
operations such as comment editing and upload flows.
---
# Linear GraphQL
Use this skill for raw Linear GraphQL work during Symphony app-server sessions.
## Primary tool
Use the `linear_graphql` client tool exposed by Symphony's app-server session.
It reuses Symphony's configured Linear auth for the session.
Tool input:
```json
{
"query": "query or mutation document",
"variables": {
"optional": "graphql variables object"
}
}
```
Tool behavior:
- Send one GraphQL operation per tool call.
- Treat a top-level `errors` array as a failed GraphQL operation even if the
tool call itself completed.
- Keep queries/mutations narrowly scoped; ask only for the fields you need.
## Discovering unfamiliar operations
When you need an unfamiliar mutation, input type, or object field, use targeted
introspection through `linear_graphql`.
List mutation names:
```graphql
query ListMutations {
__type(name: "Mutation") {
fields {
name
}
}
}
```
Inspect a specific input object:
```graphql
query CommentCreateInputShape {
__type(name: "CommentCreateInput") {
inputFields {
name
type {
kind
name
ofType {
kind
name
}
}
}
}
}
```
## Common workflows
### Query an issue by key, identifier, or id
Use these progressively:
- Start with `issue(id: $key)` when you have a ticket key such as `MT-686`.
- Fall back to `issues(filter: ...)` when you need identifier search semantics.
- Once you have the internal issue id, prefer `issue(id: $id)` for narrower reads.
Lookup by issue key:
```graphql
query IssueByKey($key: String!) {
issue(id: $key) {
id
identifier
title
state {
id
name
type
}
project {
id
name
}
branchName
url
description
updatedAt
links {
nodes {
id
url
title
}
}
}
}
```
Lookup by identifier filter:
```graphql
query IssueByIdentifier($identifier: String!) {
issues(filter: { identifier: { eq: $identifier } }, first: 1) {
nodes {
id
identifier
title
state {
id
name
type
}
project {
id
name
}
branchName
url
description
updatedAt
}
}
}
```
Resolve a key to an internal id:
```graphql
query IssueByIdOrKey($id: String!) {
issue(id: $id) {
id
identifier
title
}
}
```
Read the issue once the internal id is known:
```graphql
query IssueDetails($id: String!) {
issue(id: $id) {
id
identifier
title
url
description
state {
id
name
type
}
project {
id
name
}
attachments {
nodes {
id
title
url
sourceType
}
}
}
}
```
### Query team workflow states for an issue
Use this before changing issue state when you need the exact `stateId`:
```graphql
query IssueTeamStates($id: String!) {
issue(id: $id) {
id
team {
id
key
name
states {
nodes {
id
name
type
}
}
}
}
}
```
### Edit an existing comment
Use `commentUpdate` through `linear_graphql`:
```graphql
mutation UpdateComment($id: String!, $body: String!) {
commentUpdate(id: $id, input: { body: $body }) {
success
comment {
id
body
}
}
}
```
### Create a comment
Use `commentCreate` through `linear_graphql`:
```graphql
mutation CreateComment($issueId: String!, $body: String!) {
commentCreate(input: { issueId: $issueId, body: $body }) {
success
comment {
id
url
}
}
}
```
### Move an issue to a different state
Use `issueUpdate` with the destination `stateId`:
```graphql
mutation MoveIssueToState($id: String!, $stateId: String!) {
issueUpdate(id: $id, input: { stateId: $stateId }) {
success
issue {
id
identifier
state {
id
name
}
}
}
}
```
### Attach a GitHub PR to an issue
Use the GitHub-specific attachment mutation when linking a PR:
```graphql
mutation AttachGitHubPR($issueId: String!, $url: String!, $title: String) {
attachmentLinkGitHubPR(
issueId: $issueId
url: $url
title: $title
linkKind: links
) {
success
attachment {
id
title
url
}
}
}
```
If you only need a plain URL attachment and do not care about GitHub-specific
link metadata, use:
```graphql
mutation AttachURL($issueId: String!, $url: String!, $title: String) {
attachmentLinkURL(issueId: $issueId, url: $url, title: $title) {
success
attachment {
id
title
url
}
}
}
```
### Introspection patterns used during schema discovery
Use these when the exact field or mutation shape is unclear:
```graphql
query QueryFields {
__type(name: "Query") {
fields {
name
}
}
}
```
```graphql
query IssueFieldArgs {
__type(name: "Query") {
fields {
name
args {
name
type {
kind
name
ofType {
kind
name
ofType {
kind
name
}
}
}
}
}
}
}
```
### Upload a video to a comment
Do this in three steps:
1. Call `linear_graphql` with `fileUpload` to get `uploadUrl`, `assetUrl`, and
any required upload headers.
2. Upload the local file bytes to `uploadUrl` with `curl -X PUT` and the exact
headers returned by `fileUpload`.
3. Call `linear_graphql` again with `commentCreate` (or `commentUpdate`) and
include the resulting `assetUrl` in the comment body.
Useful mutations:
```graphql
mutation FileUpload(
$filename: String!
$contentType: String!
$size: Int!
$makePublic: Boolean
) {
fileUpload(
filename: $filename
contentType: $contentType
size: $size
makePublic: $makePublic
) {
success
uploadFile {
uploadUrl
assetUrl
headers {
key
value
}
}
}
}
```
## Usage rules
- Use `linear_graphql` for comment edits, uploads, and ad-hoc Linear API
queries.
- Prefer the narrowest issue lookup that matches what you already know:
key -> identifier search -> internal id.
- For state transitions, fetch team states first and use the exact `stateId`
instead of hardcoding names inside mutations.
- Prefer `attachmentLinkGitHubPR` over a generic URL attachment when linking a
GitHub PR to a Linear issue.
- Do not introduce new raw-token shell helpers for GraphQL access.
- If you need shell work for uploads, only use it for signed upload URLs
returned by `fileUpload`; those URLs already carry the needed authorization.
================================================
FILE: .codex/skills/pull/SKILL.md
================================================
---
name: pull
description:
Pull latest origin/main into the current local branch and resolve merge
conflicts (aka update-branch). Use when Codex needs to sync a feature branch
with origin, perform a merge-based update (not rebase), and guide conflict
resolution best practices.
---
# Pull
## Workflow
1. Verify git status is clean or commit/stash changes before merging.
2. Ensure rerere is enabled locally:
- `git config rerere.enabled true`
- `git config rerere.autoupdate true`
3. Confirm remotes and branches:
- Ensure the `origin` remote exists.
- Ensure the current branch is the one to receive the merge.
4. Fetch latest refs:
- `git fetch origin`
5. Sync the remote feature branch first:
- `git pull --ff-only origin $(git branch --show-current)`
- This pulls branch updates made remotely (for example, a GitHub auto-commit)
before merging `origin/main`.
6. Merge in order:
- Prefer `git -c merge.conflictstyle=zdiff3 merge origin/main` for clearer
conflict context.
7. If conflicts appear, resolve them (see conflict guidance below), then:
- `git add <files>`
- `git commit` (or `git merge --continue` if the merge is paused)
8. Verify with project checks (follow repo policy in `AGENTS.md`).
9. Summarize the merge:
- Call out the most challenging conflicts/files and how they were resolved.
- Note any assumptions or follow-ups.
## Conflict Resolution Guidance (Best Practices)
- Inspect context before editing:
- Use `git status` to list conflicted files.
- Use `git diff` or `git diff --merge` to see conflict hunks.
- Use `git diff :1:path/to/file :2:path/to/file` and
`git diff :1:path/to/file :3:path/to/file` to compare base vs ours/theirs
for a file-level view of intent.
- With `merge.conflictstyle=zdiff3`, conflict markers include:
- `<<<<<<<` ours, `|||||||` base, `=======` split, `>>>>>>>` theirs.
- Matching lines near the start/end are trimmed out of the conflict region,
so focus on the differing core.
- Summarize the intent of both changes, decide the semantically correct
outcome, then edit:
- State what each side is trying to achieve (bug fix, refactor, rename,
behavior change).
- Identify the shared goal, if any, and whether one side supersedes the
other.
- Decide the final behavior first; only then craft the code to match that
decision.
- Prefer preserving invariants, API contracts, and user-visible behavior
unless the conflict clearly indicates a deliberate change.
- Open files and understand intent on both sides before choosing a resolution.
- Prefer minimal, intention-preserving edits:
- Keep behavior consistent with the branch’s purpose.
- Avoid accidental deletions or silent behavior changes.
- Resolve one file at a time and rerun tests after each logical batch.
- Use `ours/theirs` only when you are certain one side should win entirely.
- For complex conflicts, search for related files or definitions to align with
the rest of the codebase.
- For generated files, resolve non-generated conflicts first, then regenerate:
- Prefer resolving source files and handwritten logic before touching
generated artifacts.
- Run the CLI/tooling command that produced the generated file to recreate it
cleanly, then stage the regenerated output.
- For import conflicts where intent is unclear, accept both sides first:
- Keep all candidate imports temporarily, finish the merge, then run lint/type
checks to remove unused or incorrect imports safely.
- After resolving, ensure no conflict markers remain:
- `git diff --check`
- When unsure, note assumptions and ask for confirmation before finalizing the
merge.
## When To Ask The User (Keep To A Minimum)
Do not ask for input unless there is no safe, reversible alternative. Prefer
making a best-effort decision, documenting the rationale, and proceeding.
Ask the user only when:
- The correct resolution depends on product intent or behavior not inferable
from code, tests, or nearby documentation.
- The conflict crosses a user-visible contract, API surface, or migration where
choosing incorrectly could break external consumers.
- A conflict requires selecting between two mutually exclusive designs with
equivalent technical merit and no clear local signal.
- The merge introduces data loss, schema changes, or irreversible side effects
without an obvious safe default.
- The branch is not the intended target, or the remote/branch names do not exist
and cannot be determined locally.
Otherwise, proceed with the merge, explain the decision briefly in notes, and
leave a clear, reviewable commit history.
================================================
FILE: .codex/skills/push/SKILL.md
================================================
---
name: push
description:
Push current branch changes to origin and create or update the corresponding
pull request; use when asked to push, publish updates, or create pull request.
---
# Push
## Prerequisites
- `gh` CLI is installed and available in `PATH`.
- `gh auth status` succeeds for GitHub operations in this repo.
## Goals
- Push current branch changes to `origin` safely.
- Create a PR if none exists for the branch, otherwise update the existing PR.
- Keep branch history clean when remote has moved.
## Related Skills
- `pull`: use this when push is rejected or sync is not clean (non-fast-forward,
merge conflict risk, or stale branch).
## Steps
1. Identify current branch and confirm remote state.
2. Run local validation (`make -C elixir all`) before pushing.
3. Push branch to `origin` with upstream tracking if needed, using whatever
remote URL is already configured.
4. If push is not clean/rejected:
- If the failure is a non-fast-forward or sync problem, run the `pull`
skill to merge `origin/main`, resolve conflicts, and rerun validation.
- Push again; use `--force-with-lease` only when history was rewritten.
- If the failure is due to auth, permissions, or workflow restrictions on
the configured remote, stop and surface the exact error instead of
rewriting remotes or switching protocols as a workaround.
5. Ensure a PR exists for the branch:
- If no PR exists, create one.
- If a PR exists and is open, update it.
- If branch is tied to a closed/merged PR, create a new branch + PR.
- Write a proper PR title that clearly describes the change outcome
- For branch updates, explicitly reconsider whether current PR title still
matches the latest scope; update it if it no longer does.
6. Write/update PR body explicitly using `.github/pull_request_template.md`:
- Fill every section with concrete content for this change.
- Replace all placeholder comments (`<!-- ... -->`).
- Keep bullets/checkboxes where template expects them.
- If PR already exists, refresh body content so it reflects the total PR
scope (all intended work on the branch), not just the newest commits,
including newly added work, removed work, or changed approach.
- Do not reuse stale description text from earlier iterations.
7. Validate PR body with `mix pr_body.check` and fix all reported issues.
8. Reply with the PR URL from `gh pr view`.
## Commands
```sh
# Identify branch
branch=$(git branch --show-current)
# Minimal validation gate
make -C elixir all
# Initial push: respect the current origin remote.
git push -u origin HEAD
# If that failed because the remote moved, use the pull skill. After
# pull-skill resolution and re-validation, retry the normal push:
git push -u origin HEAD
# If the configured remote rejects the push for auth, permissions, or workflow
# restrictions, stop and surface the exact error.
# Only if history was rewritten locally:
git push --force-with-lease origin HEAD
# Ensure a PR exists (create only if missing)
pr_state=$(gh pr view --json state -q .state 2>/dev/null || true)
if [ "$pr_state" = "MERGED" ] || [ "$pr_state" = "CLOSED" ]; then
echo "Current branch is tied to a closed PR; create a new branch + PR." >&2
exit 1
fi
# Write a clear, human-friendly title that summarizes the shipped change.
pr_title="<clear PR title written for this change>"
if [ -z "$pr_state" ]; then
gh pr create --title "$pr_title"
else
# Reconsider title on every branch update; edit if scope shifted.
gh pr edit --title "$pr_title"
fi
# Write/edit PR body to match .github/pull_request_template.md before validation.
# Example workflow:
# 1) open the template and draft body content for this PR
# 2) gh pr edit --body-file /tmp/pr_body.md
# 3) for branch updates, re-check that title/body still match current diff
tmp_pr_body=$(mktemp)
gh pr view --json body -q .body > "$tmp_pr_body"
(cd elixir && mix pr_body.check --file "$tmp_pr_body")
rm -f "$tmp_pr_body"
# Show PR URL for the reply
gh pr view --json url -q .url
```
## Notes
- Do not use `--force`; only use `--force-with-lease` as the last resort.
- Distinguish sync problems from remote auth/permission problems:
- Use the `pull` skill for non-fast-forward or stale-branch issues.
- Surface auth, permissions, or workflow restrictions directly instead of
changing remotes or protocols.
================================================
FILE: .codex/worktree_init.sh
================================================
#!/usr/bin/env bash
set -eo pipefail
script_dir="$(cd "$(dirname "$0")" && pwd)"
repo_root="$(cd "$script_dir/.." && pwd)"
project_root="$repo_root/elixir"
if ! command -v mise >/dev/null 2>&1; then
echo "mise is required. Install it from https://mise.jdx.dev/getting-started.html" >&2
exit 1
fi
cd "$project_root"
mise trust
make setup
================================================
FILE: .github/pull_request_template.md
================================================
#### Context
<!-- Why is this change needed? Length <= 240 chars -->
#### TL;DR
*<!-- A short description of what we are changing. Use simple language. Assume reader is not familiar with this code. Length <= 120 chars -->*
#### Summary
- <!-- Details of the changes in bullet points -->
- <!-- Keep them high level -->
- <!-- Each item <= 120 chars -->
#### Alternatives
- <!-- What alternatives have been considered? Why not? -->
#### Test Plan
- [ ] `make -C elixir all`
- [ ] <!-- Additional targeted checks (list below) -->
================================================
FILE: .github/workflows/make-all.yml
================================================
name: make-all
on:
pull_request:
push:
branches:
- main
jobs:
make-all:
runs-on: ubuntu-latest
defaults:
run:
working-directory: elixir
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up mise tools
uses: jdx/mise-action@v3
with:
install: true
cache: true
working_directory: elixir
- name: Cache deps and build
uses: actions/cache@v4
with:
path: |
elixir/deps
elixir/_build
key: ${{ runner.os }}-mix-${{ hashFiles('elixir/mix.lock') }}
restore-keys: |
${{ runner.os }}-mix-
- name: Verify make all
run: make all
================================================
FILE: .github/workflows/pr-description-lint.yml
================================================
name: pr-description-lint
on:
pull_request:
types: [opened, edited, reopened, synchronize, ready_for_review]
jobs:
validate-pr-description:
runs-on: ubuntu-latest
defaults:
run:
working-directory: elixir
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up mise tools
uses: jdx/mise-action@v3
with:
install: true
cache: true
working_directory: elixir
- name: Validate PR description format
env:
PR_BODY_JSON: ${{ toJson(github.event.pull_request.body) }}
run: |
mix local.hex --force
mix local.rebar --force
mix deps.get
printf '%s' "$PR_BODY_JSON" | jq -r '.' > /tmp/pr_body.md
mix pr_body.check --file /tmp/pr_body.md
================================================
FILE: LICENSE
================================================
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
================================================
FILE: NOTICE
================================================
Copyright 2025 OpenAI
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
Distributed under the License is distributed on an “AS IS” BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
Limitations under the License.
================================================
FILE: README.md
================================================
# Symphony
Symphony turns project work into isolated, autonomous implementation runs, allowing teams to manage
work instead of supervising coding agents.
[](.github/media/symphony-demo.mp4)
_In this [demo video](.github/media/symphony-demo.mp4), Symphony monitors a Linear board for work and spawns agents to handle the tasks. The agents complete the tasks and provide proof of work: CI status, PR review feedback, complexity analysis, and walkthrough videos. When accepted, the agents land the PR safely. Engineers do not need to supervise Codex; they can manage the work at a higher level._
> [!WARNING]
> Symphony is a low-key engineering preview for testing in trusted environments.
## Running Symphony
### Requirements
Symphony works best in codebases that have adopted
[harness engineering](https://openai.com/index/harness-engineering/). Symphony is the next step --
moving from managing coding agents to managing work that needs to get done.
### Option 1. Make your own
Tell your favorite coding agent to build Symphony in a programming language of your choice:
> Implement Symphony according to the following spec:
> https://github.com/openai/symphony/blob/main/SPEC.md
### Option 2. Use our experimental reference implementation
Check out [elixir/README.md](elixir/README.md) for instructions on how to set up your environment
and run the Elixir-based Symphony implementation. You can also ask your favorite coding agent to
help with the setup:
> Set up Symphony for my repository based on
> https://github.com/openai/symphony/blob/main/elixir/README.md
---
## License
This project is licensed under the [Apache License 2.0](LICENSE).
================================================
FILE: SPEC.md
================================================
# Symphony Service Specification
Status: Draft v1 (language-agnostic)
Purpose: Define a service that orchestrates coding agents to get project work done.
## 1. Problem Statement
Symphony is a long-running automation service that continuously reads work from an issue tracker
(Linear in this specification version), creates an isolated workspace for each issue, and runs a
coding agent session for that issue inside the workspace.
The service solves four operational problems:
- It turns issue execution into a repeatable daemon workflow instead of manual scripts.
- It isolates agent execution in per-issue workspaces so agent commands run only inside per-issue
workspace directories.
- It keeps the workflow policy in-repo (`WORKFLOW.md`) so teams version the agent prompt and runtime
settings with their code.
- It provides enough observability to operate and debug multiple concurrent agent runs.
Implementations are expected to document their trust and safety posture explicitly. This
specification does not require a single approval, sandbox, or operator-confirmation policy; some
implementations may target trusted environments with a high-trust configuration, while others may
require stricter approvals or sandboxing.
Important boundary:
- Symphony is a scheduler/runner and tracker reader.
- Ticket writes (state transitions, comments, PR links) are typically performed by the coding agent
using tools available in the workflow/runtime environment.
- A successful run may end at a workflow-defined handoff state (for example `Human Review`), not
necessarily `Done`.
## 2. Goals and Non-Goals
### 2.1 Goals
- Poll the issue tracker on a fixed cadence and dispatch work with bounded concurrency.
- Maintain a single authoritative orchestrator state for dispatch, retries, and reconciliation.
- Create deterministic per-issue workspaces and preserve them across runs.
- Stop active runs when issue state changes make them ineligible.
- Recover from transient failures with exponential backoff.
- Load runtime behavior from a repository-owned `WORKFLOW.md` contract.
- Expose operator-visible observability (at minimum structured logs).
- Support restart recovery without requiring a persistent database.
### 2.2 Non-Goals
- Rich web UI or multi-tenant control plane.
- Prescribing a specific dashboard or terminal UI implementation.
- General-purpose workflow engine or distributed job scheduler.
- Built-in business logic for how to edit tickets, PRs, or comments. (That logic lives in the
workflow prompt and agent tooling.)
- Mandating strong sandbox controls beyond what the coding agent and host OS provide.
- Mandating a single default approval, sandbox, or operator-confirmation posture for all
implementations.
## 3. System Overview
### 3.1 Main Components
1. `Workflow Loader`
- Reads `WORKFLOW.md`.
- Parses YAML front matter and prompt body.
- Returns `{config, prompt_template}`.
2. `Config Layer`
- Exposes typed getters for workflow config values.
- Applies defaults and environment variable indirection.
- Performs validation used by the orchestrator before dispatch.
3. `Issue Tracker Client`
- Fetches candidate issues in active states.
- Fetches current states for specific issue IDs (reconciliation).
- Fetches terminal-state issues during startup cleanup.
- Normalizes tracker payloads into a stable issue model.
4. `Orchestrator`
- Owns the poll tick.
- Owns the in-memory runtime state.
- Decides which issues to dispatch, retry, stop, or release.
- Tracks session metrics and retry queue state.
5. `Workspace Manager`
- Maps issue identifiers to workspace paths.
- Ensures per-issue workspace directories exist.
- Runs workspace lifecycle hooks.
- Cleans workspaces for terminal issues.
6. `Agent Runner`
- Creates workspace.
- Builds prompt from issue + workflow template.
- Launches the coding agent app-server client.
- Streams agent updates back to the orchestrator.
7. `Status Surface` (optional)
- Presents human-readable runtime status (for example terminal output, dashboard, or other
operator-facing view).
8. `Logging`
- Emits structured runtime logs to one or more configured sinks.
### 3.2 Abstraction Levels
Symphony is easiest to port when kept in these layers:
1. `Policy Layer` (repo-defined)
- `WORKFLOW.md` prompt body.
- Team-specific rules for ticket handling, validation, and handoff.
2. `Configuration Layer` (typed getters)
- Parses front matter into typed runtime settings.
- Handles defaults, environment tokens, and path normalization.
3. `Coordination Layer` (orchestrator)
- Polling loop, issue eligibility, concurrency, retries, reconciliation.
4. `Execution Layer` (workspace + agent subprocess)
- Filesystem lifecycle, workspace preparation, coding-agent protocol.
5. `Integration Layer` (Linear adapter)
- API calls and normalization for tracker data.
6. `Observability Layer` (logs + optional status surface)
- Operator visibility into orchestrator and agent behavior.
### 3.3 External Dependencies
- Issue tracker API (Linear for `tracker.kind: linear` in this specification version).
- Local filesystem for workspaces and logs.
- Optional workspace population tooling (for example Git CLI, if used).
- Coding-agent executable that supports JSON-RPC-like app-server mode over stdio.
- Host environment authentication for the issue tracker and coding agent.
## 4. Core Domain Model
### 4.1 Entities
#### 4.1.1 Issue
Normalized issue record used by orchestration, prompt rendering, and observability output.
Fields:
- `id` (string)
- Stable tracker-internal ID.
- `identifier` (string)
- Human-readable ticket key (example: `ABC-123`).
- `title` (string)
- `description` (string or null)
- `priority` (integer or null)
- Lower numbers are higher priority in dispatch sorting.
- `state` (string)
- Current tracker state name.
- `branch_name` (string or null)
- Tracker-provided branch metadata if available.
- `url` (string or null)
- `labels` (list of strings)
- Normalized to lowercase.
- `blocked_by` (list of blocker refs)
- Each blocker ref contains:
- `id` (string or null)
- `identifier` (string or null)
- `state` (string or null)
- `created_at` (timestamp or null)
- `updated_at` (timestamp or null)
#### 4.1.2 Workflow Definition
Parsed `WORKFLOW.md` payload:
- `config` (map)
- YAML front matter root object.
- `prompt_template` (string)
- Markdown body after front matter, trimmed.
#### 4.1.3 Service Config (Typed View)
Typed runtime values derived from `WorkflowDefinition.config` plus environment resolution.
Examples:
- poll interval
- workspace root
- active and terminal issue states
- concurrency limits
- coding-agent executable/args/timeouts
- workspace hooks
#### 4.1.4 Workspace
Filesystem workspace assigned to one issue identifier.
Fields (logical):
- `path` (workspace path; current runtime typically uses absolute paths, but relative roots are
possible if configured without path separators)
- `workspace_key` (sanitized issue identifier)
- `created_now` (boolean, used to gate `after_create` hook)
#### 4.1.5 Run Attempt
One execution attempt for one issue.
Fields (logical):
- `issue_id`
- `issue_identifier`
- `attempt` (integer or null, `null` for first run, `>=1` for retries/continuation)
- `workspace_path`
- `started_at`
- `status`
- `error` (optional)
#### 4.1.6 Live Session (Agent Session Metadata)
State tracked while a coding-agent subprocess is running.
Fields:
- `session_id` (string, `<thread_id>-<turn_id>`)
- `thread_id` (string)
- `turn_id` (string)
- `codex_app_server_pid` (string or null)
- `last_codex_event` (string/enum or null)
- `last_codex_timestamp` (timestamp or null)
- `last_codex_message` (summarized payload)
- `codex_input_tokens` (integer)
- `codex_output_tokens` (integer)
- `codex_total_tokens` (integer)
- `last_reported_input_tokens` (integer)
- `last_reported_output_tokens` (integer)
- `last_reported_total_tokens` (integer)
- `turn_count` (integer)
- Number of coding-agent turns started within the current worker lifetime.
#### 4.1.7 Retry Entry
Scheduled retry state for an issue.
Fields:
- `issue_id`
- `identifier` (best-effort human ID for status surfaces/logs)
- `attempt` (integer, 1-based for retry queue)
- `due_at_ms` (monotonic clock timestamp)
- `timer_handle` (runtime-specific timer reference)
- `error` (string or null)
#### 4.1.8 Orchestrator Runtime State
Single authoritative in-memory state owned by the orchestrator.
Fields:
- `poll_interval_ms` (current effective poll interval)
- `max_concurrent_agents` (current effective global concurrency limit)
- `running` (map `issue_id -> running entry`)
- `claimed` (set of issue IDs reserved/running/retrying)
- `retry_attempts` (map `issue_id -> RetryEntry`)
- `completed` (set of issue IDs; bookkeeping only, not dispatch gating)
- `codex_totals` (aggregate tokens + runtime seconds)
- `codex_rate_limits` (latest rate-limit snapshot from agent events)
### 4.2 Stable Identifiers and Normalization Rules
- `Issue ID`
- Use for tracker lookups and internal map keys.
- `Issue Identifier`
- Use for human-readable logs and workspace naming.
- `Workspace Key`
- Derive from `issue.identifier` by replacing any character not in `[A-Za-z0-9._-]` with `_`.
- Use the sanitized value for the workspace directory name.
- `Normalized Issue State`
- Compare states after `lowercase`.
- `Session ID`
- Compose from coding-agent `thread_id` and `turn_id` as `<thread_id>-<turn_id>`.
## 5. Workflow Specification (Repository Contract)
### 5.1 File Discovery and Path Resolution
Workflow file path precedence:
1. Explicit application/runtime setting (set by CLI startup path).
2. Default: `WORKFLOW.md` in the current process working directory.
Loader behavior:
- If the file cannot be read, return `missing_workflow_file` error.
- The workflow file is expected to be repository-owned and version-controlled.
### 5.2 File Format
`WORKFLOW.md` is a Markdown file with optional YAML front matter.
Design note:
- `WORKFLOW.md` should be self-contained enough to describe and run different workflows (prompt,
runtime settings, hooks, and tracker selection/config) without requiring out-of-band
service-specific configuration.
Parsing rules:
- If file starts with `---`, parse lines until the next `---` as YAML front matter.
- Remaining lines become the prompt body.
- If front matter is absent, treat the entire file as prompt body and use an empty config map.
- YAML front matter must decode to a map/object; non-map YAML is an error.
- Prompt body is trimmed before use.
Returned workflow object:
- `config`: front matter root object (not nested under a `config` key).
- `prompt_template`: trimmed Markdown body.
### 5.3 Front Matter Schema
Top-level keys:
- `tracker`
- `polling`
- `workspace`
- `hooks`
- `agent`
- `codex`
Unknown keys should be ignored for forward compatibility.
Note:
- The workflow front matter is extensible. Optional extensions may define additional top-level keys
(for example `server`) without changing the core schema above.
- Extensions should document their field schema, defaults, validation rules, and whether changes
apply dynamically or require restart.
- Common extension: `server.port` (integer) enables the optional HTTP server described in Section
13.7.
#### 5.3.1 `tracker` (object)
Fields:
- `kind` (string)
- Required for dispatch.
- Current supported value: `linear`
- `endpoint` (string)
- Default for `tracker.kind == "linear"`: `https://api.linear.app/graphql`
- `api_key` (string)
- May be a literal token or `$VAR_NAME`.
- Canonical environment variable for `tracker.kind == "linear"`: `LINEAR_API_KEY`.
- If `$VAR_NAME` resolves to an empty string, treat the key as missing.
- `project_slug` (string)
- Required for dispatch when `tracker.kind == "linear"`.
- `active_states` (list of strings)
- Default: `Todo`, `In Progress`
- `terminal_states` (list of strings)
- Default: `Closed`, `Cancelled`, `Canceled`, `Duplicate`, `Done`
#### 5.3.2 `polling` (object)
Fields:
- `interval_ms` (integer or string integer)
- Default: `30000`
- Changes should be re-applied at runtime and affect future tick scheduling without restart.
#### 5.3.3 `workspace` (object)
Fields:
- `root` (path string or `$VAR`)
- Default: `<system-temp>/symphony_workspaces`
- `~` and strings containing path separators are expanded.
- Bare strings without path separators are preserved as-is (relative roots are allowed but
discouraged).
#### 5.3.4 `hooks` (object)
Fields:
- `after_create` (multiline shell script string, optional)
- Runs only when a workspace directory is newly created.
- Failure aborts workspace creation.
- `before_run` (multiline shell script string, optional)
- Runs before each agent attempt after workspace preparation and before launching the coding
agent.
- Failure aborts the current attempt.
- `after_run` (multiline shell script string, optional)
- Runs after each agent attempt (success, failure, timeout, or cancellation) once the workspace
exists.
- Failure is logged but ignored.
- `before_remove` (multiline shell script string, optional)
- Runs before workspace deletion if the directory exists.
- Failure is logged but ignored; cleanup still proceeds.
- `timeout_ms` (integer, optional)
- Default: `60000`
- Applies to all workspace hooks.
- Non-positive values should be treated as invalid and fall back to the default.
- Changes should be re-applied at runtime for future hook executions.
#### 5.3.5 `agent` (object)
Fields:
- `max_concurrent_agents` (integer or string integer)
- Default: `10`
- Changes should be re-applied at runtime and affect subsequent dispatch decisions.
- `max_retry_backoff_ms` (integer or string integer)
- Default: `300000` (5 minutes)
- Changes should be re-applied at runtime and affect future retry scheduling.
- `max_concurrent_agents_by_state` (map `state_name -> positive integer`)
- Default: empty map.
- State keys are normalized (`lowercase`) for lookup.
- Invalid entries (non-positive or non-numeric) are ignored.
#### 5.3.6 `codex` (object)
Fields:
For Codex-owned config values such as `approval_policy`, `thread_sandbox`, and
`turn_sandbox_policy`, supported values are defined by the targeted Codex app-server version.
Implementors should treat them as pass-through Codex config values rather than relying on a
hand-maintained enum in this spec. To inspect the installed Codex schema, run
`codex app-server generate-json-schema --out <dir>` and inspect the relevant definitions referenced
by `v2/ThreadStartParams.json` and `v2/TurnStartParams.json`. Implementations may validate these
fields locally if they want stricter startup checks.
- `command` (string shell command)
- Default: `codex app-server`
- The runtime launches this command via `bash -lc` in the workspace directory.
- The launched process must speak a compatible app-server protocol over stdio.
- `approval_policy` (Codex `AskForApproval` value)
- Default: implementation-defined.
- `thread_sandbox` (Codex `SandboxMode` value)
- Default: implementation-defined.
- `turn_sandbox_policy` (Codex `SandboxPolicy` value)
- Default: implementation-defined.
- `turn_timeout_ms` (integer)
- Default: `3600000` (1 hour)
- `read_timeout_ms` (integer)
- Default: `5000`
- `stall_timeout_ms` (integer)
- Default: `300000` (5 minutes)
- If `<= 0`, stall detection is disabled.
### 5.4 Prompt Template Contract
The Markdown body of `WORKFLOW.md` is the per-issue prompt template.
Rendering requirements:
- Use a strict template engine (Liquid-compatible semantics are sufficient).
- Unknown variables must fail rendering.
- Unknown filters must fail rendering.
Template input variables:
- `issue` (object)
- Includes all normalized issue fields, including labels and blockers.
- `attempt` (integer or null)
- `null`/absent on first attempt.
- Integer on retry or continuation run.
Fallback prompt behavior:
- If the workflow prompt body is empty, the runtime may use a minimal default prompt
(`You are working on an issue from Linear.`).
- Workflow file read/parse failures are configuration/validation errors and should not silently fall
back to a prompt.
### 5.5 Workflow Validation and Error Surface
Error classes:
- `missing_workflow_file`
- `workflow_parse_error`
- `workflow_front_matter_not_a_map`
- `template_parse_error` (during prompt rendering)
- `template_render_error` (unknown variable/filter, invalid interpolation)
Dispatch gating behavior:
- Workflow file read/YAML errors block new dispatches until fixed.
- Template errors fail only the affected run attempt.
## 6. Configuration Specification
### 6.1 Source Precedence and Resolution Semantics
Configuration precedence:
1. Workflow file path selection (runtime setting -> cwd default).
2. YAML front matter values.
3. Environment indirection via `$VAR_NAME` inside selected YAML values.
4. Built-in defaults.
Value coercion semantics:
- Path/command fields support:
- `~` home expansion
- `$VAR` expansion for env-backed path values
- Apply expansion only to values intended to be local filesystem paths; do not rewrite URIs or
arbitrary shell command strings.
### 6.2 Dynamic Reload Semantics
Dynamic reload is required:
- The software should watch `WORKFLOW.md` for changes.
- On change, it should re-read and re-apply workflow config and prompt template without restart.
- The software should attempt to adjust live behavior to the new config (for example polling
cadence, concurrency limits, active/terminal states, codex settings, workspace paths/hooks, and
prompt content for future runs).
- Reloaded config applies to future dispatch, retry scheduling, reconciliation decisions, hook
execution, and agent launches.
- Implementations are not required to restart in-flight agent sessions automatically when config
changes.
- Extensions that manage their own listeners/resources (for example an HTTP server port change) may
require restart unless the implementation explicitly supports live rebind.
- Implementations should also re-validate/reload defensively during runtime operations (for example
before dispatch) in case filesystem watch events are missed.
- Invalid reloads should not crash the service; keep operating with the last known good effective
configuration and emit an operator-visible error.
### 6.3 Dispatch Preflight Validation
This validation is a scheduler preflight run before attempting to dispatch new work. It validates
the workflow/config needed to poll and launch workers, not a full audit of all possible workflow
behavior.
Startup validation:
- Validate configuration before starting the scheduling loop.
- If startup validation fails, fail startup and emit an operator-visible error.
Per-tick dispatch validation:
- Re-validate before each dispatch cycle.
- If validation fails, skip dispatch for that tick, keep reconciliation active, and emit an
operator-visible error.
Validation checks:
- Workflow file can be loaded and parsed.
- `tracker.kind` is present and supported.
- `tracker.api_key` is present after `$` resolution.
- `tracker.project_slug` is present when required by the selected tracker kind.
- `codex.command` is present and non-empty.
### 6.4 Config Fields Summary (Cheat Sheet)
This section is intentionally redundant so a coding agent can implement the config layer quickly.
- `tracker.kind`: string, required, currently `linear`
- `tracker.endpoint`: string, default `https://api.linear.app/graphql` when `tracker.kind=linear`
- `tracker.api_key`: string or `$VAR`, canonical env `LINEAR_API_KEY` when `tracker.kind=linear`
- `tracker.project_slug`: string, required when `tracker.kind=linear`
- `tracker.active_states`: list of strings, default `["Todo", "In Progress"]`
- `tracker.terminal_states`: list of strings, default `["Closed", "Cancelled", "Canceled", "Duplicate", "Done"]`
- `polling.interval_ms`: integer, default `30000`
- `workspace.root`: path, default `<system-temp>/symphony_workspaces`
- `worker.ssh_hosts` (extension): list of SSH host strings, optional; when omitted, work runs
locally
- `worker.max_concurrent_agents_per_host` (extension): positive integer, optional; shared per-host
cap applied across configured SSH hosts
- `hooks.after_create`: shell script or null
- `hooks.before_run`: shell script or null
- `hooks.after_run`: shell script or null
- `hooks.before_remove`: shell script or null
- `hooks.timeout_ms`: integer, default `60000`
- `agent.max_concurrent_agents`: integer, default `10`
- `agent.max_turns`: integer, default `20`
- `agent.max_retry_backoff_ms`: integer, default `300000` (5m)
- `agent.max_concurrent_agents_by_state`: map of positive integers, default `{}`
- `codex.command`: shell command string, default `codex app-server`
- `codex.approval_policy`: Codex `AskForApproval` value, default implementation-defined
- `codex.thread_sandbox`: Codex `SandboxMode` value, default implementation-defined
- `codex.turn_sandbox_policy`: Codex `SandboxPolicy` value, default implementation-defined
- `codex.turn_timeout_ms`: integer, default `3600000`
- `codex.read_timeout_ms`: integer, default `5000`
- `codex.stall_timeout_ms`: integer, default `300000`
- `server.port` (extension): integer, optional; enables the optional HTTP server, `0` may be used
for ephemeral local bind, and CLI `--port` overrides it
## 7. Orchestration State Machine
The orchestrator is the only component that mutates scheduling state. All worker outcomes are
reported back to it and converted into explicit state transitions.
### 7.1 Issue Orchestration States
This is not the same as tracker states (`Todo`, `In Progress`, etc.). This is the service's internal
claim state.
1. `Unclaimed`
- Issue is not running and has no retry scheduled.
2. `Claimed`
- Orchestrator has reserved the issue to prevent duplicate dispatch.
- In practice, claimed issues are either `Running` or `RetryQueued`.
3. `Running`
- Worker task exists and the issue is tracked in `running` map.
4. `RetryQueued`
- Worker is not running, but a retry timer exists in `retry_attempts`.
5. `Released`
- Claim removed because issue is terminal, non-active, missing, or retry path completed without
re-dispatch.
Important nuance:
- A successful worker exit does not mean the issue is done forever.
- The worker may continue through multiple back-to-back coding-agent turns before it exits.
- After each normal turn completion, the worker re-checks the tracker issue state.
- If the issue is still in an active state, the worker should start another turn on the same live
coding-agent thread in the same workspace, up to `agent.max_turns`.
- The first turn should use the full rendered task prompt.
- Continuation turns should send only continuation guidance to the existing thread, not resend the
original task prompt that is already present in thread history.
- Once the worker exits normally, the orchestrator still schedules a short continuation retry
(about 1 second) so it can re-check whether the issue remains active and needs another worker
session.
### 7.2 Run Attempt Lifecycle
A run attempt transitions through these phases:
1. `PreparingWorkspace`
2. `BuildingPrompt`
3. `LaunchingAgentProcess`
4. `InitializingSession`
5. `StreamingTurn`
6. `Finishing`
7. `Succeeded`
8. `Failed`
9. `TimedOut`
10. `Stalled`
11. `CanceledByReconciliation`
Distinct terminal reasons are important because retry logic and logs differ.
### 7.3 Transition Triggers
- `Poll Tick`
- Reconcile active runs.
- Validate config.
- Fetch candidate issues.
- Dispatch until slots are exhausted.
- `Worker Exit (normal)`
- Remove running entry.
- Update aggregate runtime totals.
- Schedule continuation retry (attempt `1`) after the worker exhausts or finishes its in-process
turn loop.
- `Worker Exit (abnormal)`
- Remove running entry.
- Update aggregate runtime totals.
- Schedule exponential-backoff retry.
- `Codex Update Event`
- Update live session fields, token counters, and rate limits.
- `Retry Timer Fired`
- Re-fetch active candidates and attempt re-dispatch, or release claim if no longer eligible.
- `Reconciliation State Refresh`
- Stop runs whose issue states are terminal or no longer active.
- `Stall Timeout`
- Kill worker and schedule retry.
### 7.4 Idempotency and Recovery Rules
- The orchestrator serializes state mutations through one authority to avoid duplicate dispatch.
- `claimed` and `running` checks are required before launching any worker.
- Reconciliation runs before dispatch on every tick.
- Restart recovery is tracker-driven and filesystem-driven (no durable orchestrator DB required).
- Startup terminal cleanup removes stale workspaces for issues already in terminal states.
## 8. Polling, Scheduling, and Reconciliation
### 8.1 Poll Loop
At startup, the service validates config, performs startup cleanup, schedules an immediate tick, and
then repeats every `polling.interval_ms`.
The effective poll interval should be updated when workflow config changes are re-applied.
Tick sequence:
1. Reconcile running issues.
2. Run dispatch preflight validation.
3. Fetch candidate issues from tracker using active states.
4. Sort issues by dispatch priority.
5. Dispatch eligible issues while slots remain.
6. Notify observability/status consumers of state changes.
If per-tick validation fails, dispatch is skipped for that tick, but reconciliation still happens
first.
### 8.2 Candidate Selection Rules
An issue is dispatch-eligible only if all are true:
- It has `id`, `identifier`, `title`, and `state`.
- Its state is in `active_states` and not in `terminal_states`.
- It is not already in `running`.
- It is not already in `claimed`.
- Global concurrency slots are available.
- Per-state concurrency slots are available.
- Blocker rule for `Todo` state passes:
- If the issue state is `Todo`, do not dispatch when any blocker is non-terminal.
Sorting order (stable intent):
1. `priority` ascending (1..4 are preferred; null/unknown sorts last)
2. `created_at` oldest first
3. `identifier` lexicographic tie-breaker
### 8.3 Concurrency Control
Global limit:
- `available_slots = max(max_concurrent_agents - running_count, 0)`
Per-state limit:
- `max_concurrent_agents_by_state[state]` if present (state key normalized)
- otherwise fallback to global limit
The runtime counts issues by their current tracked state in the `running` map.
Optional SSH host limit:
- When `worker.max_concurrent_agents_per_host` is set, each configured SSH host may run at most
that many concurrent agents at once.
- Hosts at that cap are skipped for new dispatch until capacity frees up.
### 8.4 Retry and Backoff
Retry entry creation:
- Cancel any existing retry timer for the same issue.
- Store `attempt`, `identifier`, `error`, `due_at_ms`, and new timer handle.
Backoff formula:
- Normal continuation retries after a clean worker exit use a short fixed delay of `1000` ms.
- Failure-driven retries use `delay = min(10000 * 2^(attempt - 1), agent.max_retry_backoff_ms)`.
- Power is capped by the configured max retry backoff (default `300000` / 5m).
Retry handling behavior:
1. Fetch active candidate issues (not all issues).
2. Find the specific issue by `issue_id`.
3. If not found, release claim.
4. If found and still candidate-eligible:
- Dispatch if slots are available.
- Otherwise requeue with error `no available orchestrator slots`.
5. If found but no longer active, release claim.
Note:
- Terminal-state workspace cleanup is handled by startup cleanup and active-run reconciliation
(including terminal transitions for currently running issues).
- Retry handling mainly operates on active candidates and releases claims when the issue is absent,
rather than performing terminal cleanup itself.
### 8.5 Active Run Reconciliation
Reconciliation runs every tick and has two parts.
Part A: Stall detection
- For each running issue, compute `elapsed_ms` since:
- `last_codex_timestamp` if any event has been seen, else
- `started_at`
- If `elapsed_ms > codex.stall_timeout_ms`, terminate the worker and queue a retry.
- If `stall_timeout_ms <= 0`, skip stall detection entirely.
Part B: Tracker state refresh
- Fetch current issue states for all running issue IDs.
- For each running issue:
- If tracker state is terminal: terminate worker and clean workspace.
- If tracker state is still active: update the in-memory issue snapshot.
- If tracker state is neither active nor terminal: terminate worker without workspace cleanup.
- If state refresh fails, keep workers running and try again on the next tick.
### 8.6 Startup Terminal Workspace Cleanup
When the service starts:
1. Query tracker for issues in terminal states.
2. For each returned issue identifier, remove the corresponding workspace directory.
3. If the terminal-issues fetch fails, log a warning and continue startup.
This prevents stale terminal workspaces from accumulating after restarts.
## 9. Workspace Management and Safety
### 9.1 Workspace Layout
Workspace root:
- `workspace.root` (normalized path; the current config layer expands path-like values and preserves
bare relative names)
Per-issue workspace path:
- `<workspace.root>/<sanitized_issue_identifier>`
Workspace persistence:
- Workspaces are reused across runs for the same issue.
- Successful runs do not auto-delete workspaces.
### 9.2 Workspace Creation and Reuse
Input: `issue.identifier`
Algorithm summary:
1. Sanitize identifier to `workspace_key`.
2. Compute workspace path under workspace root.
3. Ensure the workspace path exists as a directory.
4. Mark `created_now=true` only if the directory was created during this call; otherwise
`created_now=false`.
5. If `created_now=true`, run `after_create` hook if configured.
Notes:
- This section does not assume any specific repository/VCS workflow.
- Workspace preparation beyond directory creation (for example dependency bootstrap, checkout/sync,
code generation) is implementation-defined and is typically handled via hooks.
### 9.3 Optional Workspace Population (Implementation-Defined)
The spec does not require any built-in VCS or repository bootstrap behavior.
Implementations may populate or synchronize the workspace using implementation-defined logic and/or
hooks (for example `after_create` and/or `before_run`).
Failure handling:
- Workspace population/synchronization failures return an error for the current attempt.
- If failure happens while creating a brand-new workspace, implementations may remove the partially
prepared directory.
- Reused workspaces should not be destructively reset on population failure unless that policy is
explicitly chosen and documented.
### 9.4 Workspace Hooks
Supported hooks:
- `hooks.after_create`
- `hooks.before_run`
- `hooks.after_run`
- `hooks.before_remove`
Execution contract:
- Execute in a local shell context appropriate to the host OS, with the workspace directory as
`cwd`.
- On POSIX systems, `sh -lc <script>` (or a stricter equivalent such as `bash -lc <script>`) is a
conforming default.
- Hook timeout uses `hooks.timeout_ms`; default: `60000 ms`.
- Log hook start, failures, and timeouts.
Failure semantics:
- `after_create` failure or timeout is fatal to workspace creation.
- `before_run` failure or timeout is fatal to the current run attempt.
- `after_run` failure or timeout is logged and ignored.
- `before_remove` failure or timeout is logged and ignored.
### 9.5 Safety Invariants
This is the most important portability constraint.
Invariant 1: Run the coding agent only in the per-issue workspace path.
- Before launching the coding-agent subprocess, validate:
- `cwd == workspace_path`
Invariant 2: Workspace path must stay inside workspace root.
- Normalize both paths to absolute.
- Require `workspace_path` to have `workspace_root` as a prefix directory.
- Reject any path outside the workspace root.
Invariant 3: Workspace key is sanitized.
- Only `[A-Za-z0-9._-]` allowed in workspace directory names.
- Replace all other characters with `_`.
## 10. Agent Runner Protocol (Coding Agent Integration)
This section defines the language-neutral contract for integrating a coding agent app-server.
Compatibility profile:
- The normative contract is message ordering, required behaviors, and the logical fields that must
be extracted (for example session IDs, completion state, approval handling, and usage/rate-limit
telemetry).
- Exact JSON field names may vary slightly across compatible app-server versions.
- Implementations should tolerate equivalent payload shapes when they carry the same logical
meaning, especially for nested IDs, approval requests, user-input-required signals, and
token/rate-limit metadata.
### 10.1 Launch Contract
Subprocess launch parameters:
- Command: `codex.command`
- Invocation: `bash -lc <codex.command>`
- Working directory: workspace path
- Stdout/stderr: separate streams
- Framing: line-delimited protocol messages on stdout (JSON-RPC-like JSON per line)
Notes:
- The default command is `codex app-server`.
- Approval policy, cwd, and prompt are expressed in the protocol messages in Section 10.2.
Recommended additional process settings:
- Max line size: 10 MB (for safe buffering)
### 10.2 Session Startup Handshake
Reference: https://developers.openai.com/codex/app-server/
The client must send these protocol messages in order:
Illustrative startup transcript (equivalent payload shapes are acceptable if they preserve the same
semantics):
```json
{"id":1,"method":"initialize","params":{"clientInfo":{"name":"symphony","version":"1.0"},"capabilities":{}}}
{"method":"initialized","params":{}}
{"id":2,"method":"thread/start","params":{"approvalPolicy":"<implementation-defined>","sandbox":"<implementation-defined>","cwd":"/abs/workspace"}}
{"id":3,"method":"turn/start","params":{"threadId":"<thread-id>","input":[{"type":"text","text":"<rendered prompt-or-continuation-guidance>"}],"cwd":"/abs/workspace","title":"ABC-123: Example","approvalPolicy":"<implementation-defined>","sandboxPolicy":{"type":"<implementation-defined>"}}}
```
1. `initialize` request
- Params include:
- `clientInfo` object (for example `{name, version}`)
- `capabilities` object (may be empty)
- If the targeted Codex app-server requires capability negotiation for dynamic tools, include the
necessary capability flag(s) here.
- Wait for response (`read_timeout_ms`)
2. `initialized` notification
3. `thread/start` request
- Params include:
- `approvalPolicy` = implementation-defined session approval policy value
- `sandbox` = implementation-defined session sandbox value
- `cwd` = absolute workspace path
- If optional client-side tools are implemented, include their advertised tool specs using the
protocol mechanism supported by the targeted Codex app-server version.
4. `turn/start` request
- Params include:
- `threadId`
- `input` = single text item containing rendered prompt for the first turn, or continuation
guidance for later turns on the same thread
- `cwd`
- `title` = `<issue.identifier>: <issue.title>`
- `approvalPolicy` = implementation-defined turn approval policy value
- `sandboxPolicy` = implementation-defined object-form sandbox policy payload when required by
the targeted app-server version
Session identifiers:
- Read `thread_id` from `thread/start` result `result.thread.id`
- Read `turn_id` from each `turn/start` result `result.turn.id`
- Emit `session_id = "<thread_id>-<turn_id>"`
- Reuse the same `thread_id` for all continuation turns inside one worker run
### 10.3 Streaming Turn Processing
The client reads line-delimited messages until the turn terminates.
Completion conditions:
- `turn/completed` -> success
- `turn/failed` -> failure
- `turn/cancelled` -> failure
- turn timeout (`turn_timeout_ms`) -> failure
- subprocess exit -> failure
Continuation processing:
- If the worker decides to continue after a successful turn, it should issue another `turn/start`
on the same live `threadId`.
- The app-server subprocess should remain alive across those continuation turns and be stopped only
when the worker run is ending.
Line handling requirements:
- Read protocol messages from stdout only.
- Buffer partial stdout lines until newline arrives.
- Attempt JSON parse on complete stdout lines.
- Stderr is not part of the protocol stream:
- ignore it or log it as diagnostics
- do not attempt protocol JSON parsing on stderr
### 10.4 Emitted Runtime Events (Upstream to Orchestrator)
The app-server client emits structured events to the orchestrator callback. Each event should
include:
- `event` (enum/string)
- `timestamp` (UTC timestamp)
- `codex_app_server_pid` (if available)
- optional `usage` map (token counts)
- payload fields as needed
Important emitted events may include:
- `session_started`
- `startup_failed`
- `turn_completed`
- `turn_failed`
- `turn_cancelled`
- `turn_ended_with_error`
- `turn_input_required`
- `approval_auto_approved`
- `unsupported_tool_call`
- `notification`
- `other_message`
- `malformed`
### 10.5 Approval, Tool Calls, and User Input Policy
Approval, sandbox, and user-input behavior is implementation-defined.
Policy requirements:
- Each implementation should document its chosen approval, sandbox, and operator-confirmation
posture.
- Approval requests and user-input-required events must not leave a run stalled indefinitely. An
implementation should either satisfy them, surface them to an operator, auto-resolve them, or
fail the run according to its documented policy.
Example high-trust behavior:
- Auto-approve command execution approvals for the session.
- Auto-approve file-change approvals for the session.
- Treat user-input-required turns as hard failure.
Unsupported dynamic tool calls:
- Supported dynamic tool calls that are explicitly implemented and advertised by the runtime should
be handled according to their extension contract.
- If the agent requests a dynamic tool call (`item/tool/call`) that is not supported, return a tool
failure response and continue the session.
- This prevents the session from stalling on unsupported tool execution paths.
Optional client-side tool extension:
- An implementation may expose a limited set of client-side tools to the app-server session.
- Current optional standardized tool: `linear_graphql`.
- If implemented, supported tools should be advertised to the app-server session during startup
using the protocol mechanism supported by the targeted Codex app-server version.
- Unsupported tool names should still return a failure result and continue the session.
`linear_graphql` extension contract:
- Purpose: execute a raw GraphQL query or mutation against Linear using Symphony's configured
tracker auth for the current session.
- Availability: only meaningful when `tracker.kind == "linear"` and valid Linear auth is configured.
- Preferred input shape:
```json
{
"query": "single GraphQL query or mutation document",
"variables": {
"optional": "graphql variables object"
}
}
```
- `query` must be a non-empty string.
- `query` must contain exactly one GraphQL operation.
- `variables` is optional and, when present, must be a JSON object.
- Implementations may additionally accept a raw GraphQL query string as shorthand input.
- Execute one GraphQL operation per tool call.
- If the provided document contains multiple operations, reject the tool call as invalid input.
- `operationName` selection is intentionally out of scope for this extension.
- Reuse the configured Linear endpoint and auth from the active Symphony workflow/runtime config; do
not require the coding agent to read raw tokens from disk.
- Tool result semantics:
- transport success + no top-level GraphQL `errors` -> `success=true`
- top-level GraphQL `errors` present -> `success=false`, but preserve the GraphQL response body
for debugging
- invalid input, missing auth, or transport failure -> `success=false` with an error payload
- Return the GraphQL response or error payload as structured tool output that the model can inspect
in-session.
Illustrative responses (equivalent payload shapes are acceptable if they preserve the same outcome):
```json
{"id":"<approval-id>","result":{"approved":true}}
{"id":"<tool-call-id>","result":{"success":false,"error":"unsupported_tool_call"}}
```
Hard failure on user input requirement:
- If the agent requests user input, fail the run attempt immediately.
- The client detects this via:
- explicit method (`item/tool/requestUserInput`), or
- turn methods/flags indicating input is required.
### 10.6 Timeouts and Error Mapping
Timeouts:
- `codex.read_timeout_ms`: request/response timeout during startup and sync requests
- `codex.turn_timeout_ms`: total turn stream timeout
- `codex.stall_timeout_ms`: enforced by orchestrator based on event inactivity
Error mapping (recommended normalized categories):
- `codex_not_found`
- `invalid_workspace_cwd`
- `response_timeout`
- `turn_timeout`
- `port_exit`
- `response_error`
- `turn_failed`
- `turn_cancelled`
- `turn_input_required`
### 10.7 Agent Runner Contract
The `Agent Runner` wraps workspace + prompt + app-server client.
Behavior:
1. Create/reuse workspace for issue.
2. Build prompt from workflow template.
3. Start app-server session.
4. Forward app-server events to orchestrator.
5. On any error, fail the worker attempt (the orchestrator will retry).
Note:
- Workspaces are intentionally preserved after successful runs.
## 11. Issue Tracker Integration Contract (Linear-Compatible)
### 11.1 Required Operations
An implementation must support these tracker adapter operations:
1. `fetch_candidate_issues()`
- Return issues in configured active states for a configured project.
2. `fetch_issues_by_states(state_names)`
- Used for startup terminal cleanup.
3. `fetch_issue_states_by_ids(issue_ids)`
- Used for active-run reconciliation.
### 11.2 Query Semantics (Linear)
Linear-specific requirements for `tracker.kind == "linear"`:
- `tracker.kind == "linear"`
- GraphQL endpoint (default `https://api.linear.app/graphql`)
- Auth token sent in `Authorization` header
- `tracker.project_slug` maps to Linear project `slugId`
- Candidate issue query filters project using `project: { slugId: { eq: $projectSlug } }`
- Issue-state refresh query uses GraphQL issue IDs with variable type `[ID!]`
- Pagination required for candidate issues
- Page size default: `50`
- Network timeout: `30000 ms`
Important:
- Linear GraphQL schema details can drift. Keep query construction isolated and test the exact query
fields/types required by this specification.
A non-Linear implementation may change transport details, but the normalized outputs must match the
domain model in Section 4.
### 11.3 Normalization Rules
Candidate issue normalization should produce fields listed in Section 4.1.1.
Additional normalization details:
- `labels` -> lowercase strings
- `blocked_by` -> derived from inverse relations where relation type is `blocks`
- `priority` -> integer only (non-integers become null)
- `created_at` and `updated_at` -> parse ISO-8601 timestamps
### 11.4 Error Handling Contract
Recommended error categories:
- `unsupported_tracker_kind`
- `missing_tracker_api_key`
- `missing_tracker_project_slug`
- `linear_api_request` (transport failures)
- `linear_api_status` (non-200 HTTP)
- `linear_graphql_errors`
- `linear_unknown_payload`
- `linear_missing_end_cursor` (pagination integrity error)
Orchestrator behavior on tracker errors:
- Candidate fetch failure: log and skip dispatch for this tick.
- Running-state refresh failure: log and keep active workers running.
- Startup terminal cleanup failure: log warning and continue startup.
### 11.5 Tracker Writes (Important Boundary)
Symphony does not require first-class tracker write APIs in the orchestrator.
- Ticket mutations (state transitions, comments, PR metadata) are typically handled by the coding
agent using tools defined by the workflow prompt.
- The service remains a scheduler/runner and tracker reader.
- Workflow-specific success often means "reached the next handoff state" (for example
`Human Review`) rather than tracker terminal state `Done`.
- If the optional `linear_graphql` client-side tool extension is implemented, it is still part of
the agent toolchain rather than orchestrator business logic.
## 12. Prompt Construction and Context Assembly
### 12.1 Inputs
Inputs to prompt rendering:
- `workflow.prompt_template`
- normalized `issue` object
- optional `attempt` integer (retry/continuation metadata)
### 12.2 Rendering Rules
- Render with strict variable checking.
- Render with strict filter checking.
- Convert issue object keys to strings for template compatibility.
- Preserve nested arrays/maps (labels, blockers) so templates can iterate.
### 12.3 Retry/Continuation Semantics
`attempt` should be passed to the template because the workflow prompt may provide different
instructions for:
- first run (`attempt` null or absent)
- continuation run after a successful prior session
- retry after error/timeout/stall
### 12.4 Failure Semantics
If prompt rendering fails:
- Fail the run attempt immediately.
- Let the orchestrator treat it like any other worker failure and decide retry behavior.
## 13. Logging, Status, and Observability
### 13.1 Logging Conventions
Required context fields for issue-related logs:
- `issue_id`
- `issue_identifier`
Required context for coding-agent session lifecycle logs:
- `session_id`
Message formatting requirements:
- Use stable `key=value` phrasing.
- Include action outcome (`completed`, `failed`, `retrying`, etc.).
- Include concise failure reason when present.
- Avoid logging large raw payloads unless necessary.
### 13.2 Logging Outputs and Sinks
The spec does not prescribe where logs must go (stderr, file, remote sink, etc.).
Requirements:
- Operators must be able to see startup/validation/dispatch failures without attaching a debugger.
- Implementations may write to one or more sinks.
- If a configured log sink fails, the service should continue running when possible and emit an
operator-visible warning through any remaining sink.
### 13.3 Runtime Snapshot / Monitoring Interface (Optional but Recommended)
If the implementation exposes a synchronous runtime snapshot (for dashboards or monitoring), it
should return:
- `running` (list of running session rows)
- each running row should include `turn_count`
- `retrying` (list of retry queue rows)
- `codex_totals`
- `input_tokens`
- `output_tokens`
- `total_tokens`
- `seconds_running` (aggregate runtime seconds as of snapshot time, including active sessions)
- `rate_limits` (latest coding-agent rate limit payload, if available)
Recommended snapshot error modes:
- `timeout`
- `unavailable`
### 13.4 Optional Human-Readable Status Surface
A human-readable status surface (terminal output, dashboard, etc.) is optional and
implementation-defined.
If present, it should draw from orchestrator state/metrics only and must not be required for
correctness.
### 13.5 Session Metrics and Token Accounting
Token accounting rules:
- Agent events may include token counts in multiple payload shapes.
- Prefer absolute thread totals when available, such as:
- `thread/tokenUsage/updated` payloads
- `total_token_usage` within token-count wrapper events
- Ignore delta-style payloads such as `last_token_usage` for dashboard/API totals.
- Extract input/output/total token counts leniently from common field names within the selected
payload.
- For absolute totals, track deltas relative to last reported totals to avoid double-counting.
- Do not treat generic `usage` maps as cumulative totals unless the event type defines them that
way.
- Accumulate aggregate totals in orchestrator state.
Runtime accounting:
- Runtime should be reported as a live aggregate at snapshot/render time.
- Implementations may maintain a cumulative counter for ended sessions and add active-session
elapsed time derived from `running` entries (for example `started_at`) when producing a
snapshot/status view.
- Add run duration seconds to the cumulative ended-session runtime when a session ends (normal exit
or cancellation/termination).
- Continuous background ticking of runtime totals is not required.
Rate-limit tracking:
- Track the latest rate-limit payload seen in any agent update.
- Any human-readable presentation of rate-limit data is implementation-defined.
### 13.6 Humanized Agent Event Summaries (Optional)
Humanized summaries of raw agent protocol events are optional.
If implemented:
- Treat them as observability-only output.
- Do not make orchestrator logic depend on humanized strings.
### 13.7 Optional HTTP Server Extension
This section defines an optional HTTP interface for observability and operational control.
If implemented:
- The HTTP server is an extension and is not required for conformance.
- The implementation may serve server-rendered HTML or a client-side application for the dashboard.
- The dashboard/API must be observability/control surfaces only and must not become required for
orchestrator correctness.
Enablement (extension):
- Start the HTTP server when a CLI `--port` argument is provided.
- Start the HTTP server when `server.port` is present in `WORKFLOW.md` front matter.
- `server.port` is extension configuration and is intentionally not part of the core front-matter
schema in Section 5.3.
- Precedence: CLI `--port` overrides `server.port` when both are present.
- `server.port` must be an integer. Positive values bind that port. `0` may be used to request an
ephemeral port for local development and tests.
- Implementations should bind loopback by default (`127.0.0.1` or host equivalent) unless explicitly
configured otherwise.
- Changes to HTTP listener settings (for example `server.port`) do not need to hot-rebind;
restart-required behavior is conformant.
#### 13.7.1 Human-Readable Dashboard (`/`)
- Host a human-readable dashboard at `/`.
- The returned document should depict the current state of the system (for example active sessions,
retry delays, token consumption, runtime totals, recent events, and health/error indicators).
- It is up to the implementation whether this is server-generated HTML or a client-side app that
consumes the JSON API below.
#### 13.7.2 JSON REST API (`/api/v1/*`)
Provide a JSON REST API under `/api/v1/*` for current runtime state and operational debugging.
Minimum endpoints:
- `GET /api/v1/state`
- Returns a summary view of the current system state (running sessions, retry queue/delays,
aggregate token/runtime totals, latest rate limits, and any additional tracked summary fields).
- Suggested response shape:
```json
{
"generated_at": "2026-02-24T20:15:30Z",
"counts": {
"running": 2,
"retrying": 1
},
"running": [
{
"issue_id": "abc123",
"issue_identifier": "MT-649",
"state": "In Progress",
"session_id": "thread-1-turn-1",
"turn_count": 7,
"last_event": "turn_completed",
"last_message": "",
"started_at": "2026-02-24T20:10:12Z",
"last_event_at": "2026-02-24T20:14:59Z",
"tokens": {
"input_tokens": 1200,
"output_tokens": 800,
"total_tokens": 2000
}
}
],
"retrying": [
{
"issue_id": "def456",
"issue_identifier": "MT-650",
"attempt": 3,
"due_at": "2026-02-24T20:16:00Z",
"error": "no available orchestrator slots"
}
],
"codex_totals": {
"input_tokens": 5000,
"output_tokens": 2400,
"total_tokens": 7400,
"seconds_running": 1834.2
},
"rate_limits": null
}
```
- `GET /api/v1/<issue_identifier>`
- Returns issue-specific runtime/debug details for the identified issue, including any information
the implementation tracks that is useful for debugging.
- Suggested response shape:
```json
{
"issue_identifier": "MT-649",
"issue_id": "abc123",
"status": "running",
"workspace": {
"path": "/tmp/symphony_workspaces/MT-649"
},
"attempts": {
"restart_count": 1,
"current_retry_attempt": 2
},
"running": {
"session_id": "thread-1-turn-1",
"turn_count": 7,
"state": "In Progress",
"started_at": "2026-02-24T20:10:12Z",
"last_event": "notification",
"last_message": "Working on tests",
"last_event_at": "2026-02-24T20:14:59Z",
"tokens": {
"input_tokens": 1200,
"output_tokens": 800,
"total_tokens": 2000
}
},
"retry": null,
"logs": {
"codex_session_logs": [
{
"label": "latest",
"path": "/var/log/symphony/codex/MT-649/latest.log",
"url": null
}
]
},
"recent_events": [
{
"at": "2026-02-24T20:14:59Z",
"event": "notification",
"message": "Working on tests"
}
],
"last_error": null,
"tracked": {}
}
```
- If the issue is unknown to the current in-memory state, return `404` with an error response (for
example `{\"error\":{\"code\":\"issue_not_found\",\"message\":\"...\"}}`).
- `POST /api/v1/refresh`
- Queues an immediate tracker poll + reconciliation cycle (best-effort trigger; implementations
may coalesce repeated requests).
- Suggested request body: empty body or `{}`.
- Suggested response (`202 Accepted`) shape:
```json
{
"queued": true,
"coalesced": false,
"requested_at": "2026-02-24T20:15:30Z",
"operations": ["poll", "reconcile"]
}
```
API design notes:
- The JSON shapes above are the recommended baseline for interoperability and debugging ergonomics.
- Implementations may add fields, but should avoid breaking existing fields within a version.
- Endpoints should be read-only except for operational triggers like `/refresh`.
- Unsupported methods on defined routes should return `405 Method Not Allowed`.
- API errors should use a JSON envelope such as `{"error":{"code":"...","message":"..."}}`.
- If the dashboard is a client-side app, it should consume this API rather than duplicating state
logic.
## 14. Failure Model and Recovery Strategy
### 14.1 Failure Classes
1. `Workflow/Config Failures`
- Missing `WORKFLOW.md`
- Invalid YAML front matter
- Unsupported tracker kind or missing tracker credentials/project slug
- Missing coding-agent executable
2. `Workspace Failures`
- Workspace directory creation failure
- Workspace population/synchronization failure (implementation-defined; may come from hooks)
- Invalid workspace path configuration
- Hook timeout/failure
3. `Agent Session Failures`
- Startup handshake failure
- Turn failed/cancelled
- Turn timeout
- User input requested (hard fail)
- Subprocess exit
- Stalled session (no activity)
4. `Tracker Failures`
- API transport errors
- Non-200 status
- GraphQL errors
- malformed payloads
5. `Observability Failures`
- Snapshot timeout
- Dashboard render errors
- Log sink configuration failure
### 14.2 Recovery Behavior
- Dispatch validation failures:
- Skip new dispatches.
- Keep service alive.
- Continue reconciliation where possible.
- Worker failures:
- Convert to retries with exponential backoff.
- Tracker candidate-fetch failures:
- Skip this tick.
- Try again on next tick.
- Reconciliation state-refresh failures:
- Keep current workers.
- Retry on next tick.
- Dashboard/log failures:
- Do not crash the orchestrator.
### 14.3 Partial State Recovery (Restart)
Current design is intentionally in-memory for scheduler state.
After restart:
- No retry timers are restored from prior process memory.
- No running sessions are assumed recoverable.
- Service recovers by:
- startup terminal workspace cleanup
- fresh polling of active issues
- re-dispatching eligible work
### 14.4 Operator Intervention Points
Operators can control behavior by:
- Editing `WORKFLOW.md` (prompt and most runtime settings).
- `WORKFLOW.md` changes should be detected and re-applied automatically without restart.
- Changing issue states in the tracker:
- terminal state -> running session is stopped and workspace cleaned when reconciled
- non-active state -> running session is stopped without cleanup
- Restarting the service for process recovery or deployment (not as the normal path for applying
workflow config changes).
## 15. Security and Operational Safety
### 15.1 Trust Boundary Assumption
Each implementation defines its own trust boundary.
Operational safety requirements:
- Implementations should state clearly whether they are intended for trusted environments, more
restrictive environments, or both.
- Implementations should state clearly whether they rely on auto-approved actions, operator
approvals, stricter sandboxing, or some combination of those controls.
- Workspace isolation and path validation are important baseline controls, but they are not a
substitute for whatever approval and sandbox policy an implementation chooses.
### 15.2 Filesystem Safety Requirements
Mandatory:
- Workspace path must remain under configured workspace root.
- Coding-agent cwd must be the per-issue workspace path for the current run.
- Workspace directory names must use sanitized identifiers.
Recommended additional hardening for ports:
- Run under a dedicated OS user.
- Restrict workspace root permissions.
- Mount workspace root on a dedicated volume if possible.
### 15.3 Secret Handling
- Support `$VAR` indirection in workflow config.
- Do not log API tokens or secret env values.
- Validate presence of secrets without printing them.
### 15.4 Hook Script Safety
Workspace hooks are arbitrary shell scripts from `WORKFLOW.md`.
Implications:
- Hooks are fully trusted configuration.
- Hooks run inside the workspace directory.
- Hook output should be truncated in logs.
- Hook timeouts are required to avoid hanging the orchestrator.
### 15.5 Harness Hardening Guidance
Running Codex agents against repositories, issue trackers, and other inputs that may contain
sensitive data or externally-controlled content can be dangerous. A permissive deployment can lead
to data leaks, destructive mutations, or full machine compromise if the agent is induced to execute
harmful commands or use overly-powerful integrations.
Implementations should explicitly evaluate their own risk profile and harden the execution harness
where appropriate. This specification intentionally does not mandate a single hardening posture, but
ports should not assume that tracker data, repository contents, prompt inputs, or tool arguments are
fully trustworthy just because they originate inside a normal workflow.
Possible hardening measures include:
- Tightening Codex approval and sandbox settings described elsewhere in this specification instead
of running with a maximally permissive configuration.
- Adding external isolation layers such as OS/container/VM sandboxing, network restrictions, or
separate credentials beyond the built-in Codex policy controls.
- Filtering which Linear issues, projects, teams, labels, or other tracker sources are eligible for
dispatch so untrusted or out-of-scope tasks do not automatically reach the agent.
- Narrowing the optional `linear_graphql` tool so it can only read or mutate data inside the
intended project scope, rather than exposing general workspace-wide tracker access.
- Reducing the set of client-side tools, credentials, filesystem paths, and network destinations
available to the agent to the minimum needed for the workflow.
The correct controls are deployment-specific, but implementations should document them clearly and
treat harness hardening as part of the core safety model rather than an optional afterthought.
## 16. Reference Algorithms (Language-Agnostic)
### 16.1 Service Startup
```text
function start_service():
configure_logging()
start_observability_outputs()
start_workflow_watch(on_change=reload_and_reapply_workflow)
state = {
poll_interval_ms: get_config_poll_interval_ms(),
max_concurrent_agents: get_config_max_concurrent_agents(),
running: {},
claimed: set(),
retry_attempts: {},
completed: set(),
codex_totals: {input_tokens: 0, output_tokens: 0, total_tokens: 0, seconds_running: 0},
codex_rate_limits: null
}
validation = validate_dispatch_config()
if validation is not ok:
log_validation_error(validation)
fail_startup(validation)
startup_terminal_workspace_cleanup()
schedule_tick(delay_ms=0)
event_loop(state)
```
### 16.2 Poll-and-Dispatch Tick
```text
on_tick(state):
state = reconcile_running_issues(state)
validation = validate_dispatch_config()
if validation is not ok:
log_validation_error(validation)
notify_observers()
schedule_tick(state.poll_interval_ms)
return state
issues = tracker.fetch_candidate_issues()
if issues failed:
log_tracker_error()
notify_observers()
schedule_tick(state.poll_interval_ms)
return state
for issue in sort_for_dispatch(issues):
if no_available_slots(state):
break
if should_dispatch(issue, state):
state = dispatch_issue(issue, state, attempt=null)
notify_observers()
schedule_tick(state.poll_interval_ms)
return state
```
### 16.3 Reconcile Active Runs
```text
function reconcile_running_issues(state):
state = reconcile_stalled_runs(state)
running_ids = keys(state.running)
if running_ids is empty:
return state
refreshed = tracker.fetch_issue_states_by_ids(running_ids)
if refreshed failed:
log_debug("keep workers running")
return state
for issue in refreshed:
if issue.state in terminal_states:
state = terminate_running_issue(state, issue.id, cleanup_workspace=true)
else if issue.state in active_states:
state.running[issue.id].issue = issue
else:
state = terminate_running_issue(state, issue.id, cleanup_workspace=false)
return state
```
### 16.4 Dispatch One Issue
```text
function dispatch_issue(issue, state, attempt):
worker = spawn_worker(
fn -> run_agent_attempt(issue, attempt, parent_orchestrator_pid) end
)
if worker spawn failed:
return schedule_retry(state, issue.id, next_attempt(attempt), {
identifier: issue.identifier,
error: "failed to spawn agent"
})
state.running[issue.id] = {
worker_handle,
monitor_handle,
identifier: issue.identifier,
issue,
session_id: null,
codex_app_server_pid: null,
last_codex_message: null,
last_codex_event: null,
last_codex_timestamp: null,
codex_input_tokens: 0,
codex_output_tokens: 0,
codex_total_tokens: 0,
last_reported_input_tokens: 0,
last_reported_output_tokens: 0,
last_reported_total_tokens: 0,
retry_attempt: normalize_attempt(attempt),
started_at: now_utc()
}
state.claimed.add(issue.id)
state.retry_attempts.remove(issue.id)
return state
```
### 16.5 Worker Attempt (Workspace + Prompt + Agent)
```text
function run_agent_attempt(issue, attempt, orchestrator_channel):
workspace = workspace_manager.create_for_issue(issue.identifier)
if workspace failed:
fail_worker("workspace error")
if run_hook("before_run", workspace.path) failed:
fail_worker("before_run hook error")
session = app_server.start_session(workspace=workspace.path)
if session failed:
run_hook_best_effort("after_run", workspace.path)
fail_worker("agent session startup error")
max_turns = config.agent.max_turns
turn_number = 1
while true:
prompt = build_turn_prompt(workflow_template, issue, attempt, turn_number, max_turns)
if prompt failed:
app_server.stop_session(session)
run_hook_best_effort("after_run", workspace.path)
fail_worker("prompt error")
turn_result = app_server.run_turn(
session=session,
prompt=prompt,
issue=issue,
on_message=(msg) -> send(orchestrator_channel, {codex_update, issue.id, msg})
)
if turn_result failed:
app_server.stop_session(session)
run_hook_best_effort("after_run", workspace.path)
fail_worker("agent turn error")
refreshed_issue = tracker.fetch_issue_states_by_ids([issue.id])
if refreshed_issue failed:
app_server.stop_session(session)
run_hook_best_effort("after_run", workspace.path)
fail_worker("issue state refresh error")
issue = refreshed_issue[0] or issue
if issue.state is not active:
break
if turn_number >= max_turns:
break
turn_number = turn_number + 1
app_server.stop_session(session)
run_hook_best_effort("after_run", workspace.path)
exit_normal()
```
### 16.6 Worker Exit and Retry Handling
```text
on_worker_exit(issue_id, reason, state):
running_entry = state.running.remove(issue_id)
state = add_runtime_seconds_to_totals(state, running_entry)
if reason == normal:
state.completed.add(issue_id) # bookkeeping only
state = schedule_retry(state, issue_id, 1, {
identifier: running_entry.identifier,
delay_type: continuation
})
else:
state = schedule_retry(state, issue_id, next_attempt_from(running_entry), {
identifier: running_entry.identifier,
error: format("worker exited: %reason")
})
notify_observers()
return state
```
```text
on_retry_timer(issue_id, state):
retry_entry = state.retry_attempts.pop(issue_id)
if missing:
return state
candidates = tracker.fetch_candidate_issues()
if fetch failed:
return schedule_retry(state, issue_id, retry_entry.attempt + 1, {
identifier: retry_entry.identifier,
error: "retry poll failed"
})
issue = find_by_id(candidates, issue_id)
if issue is null:
state.claimed.remove(issue_id)
return state
if available_slots(state) == 0:
return schedule_retry(state, issue_id, retry_entry.attempt + 1, {
identifier: issue.identifier,
error: "no available orchestrator slots"
})
return dispatch_issue(issue, state, attempt=retry_entry.attempt)
```
## 17. Test and Validation Matrix
A conforming implementation should include tests that cover the behaviors defined in this
specification.
Validation profiles:
- `Core Conformance`: deterministic tests required for all conforming implementations.
- `Extension Conformance`: required only for optional features that an implementation chooses to
ship.
- `Real Integration Profile`: environment-dependent smoke/integration checks recommended before
production use.
Unless otherwise noted, Sections 17.1 through 17.7 are `Core Conformance`. Bullets that begin with
`If ... is implemented` are `Extension Conformance`.
### 17.1 Workflow and Config Parsing
- Workflow file path precedence:
- explicit runtime path is used when provided
- cwd default is `WORKFLOW.md` when no explicit runtime path is provided
- Workflow file changes are detected and trigger re-read/re-apply without restart
- Invalid workflow reload keeps last known good effective configuration and emits an
operator-visible error
- Missing `WORKFLOW.md` returns typed error
- Invalid YAML front matter returns typed error
- Front matter non-map returns typed error
- Config defaults apply when optional values are missing
- `tracker.kind` validation enforces currently supported kind (`linear`)
- `tracker.api_key` works (including `$VAR` indirection)
- `$VAR` resolution works for tracker API key and path values
- `~` path expansion works
- `codex.command` is preserved as a shell command string
- Per-state concurrency override map normalizes state names and ignores invalid values
- Prompt template renders `issue` and `attempt`
- Prompt rendering fails on unknown variables (strict mode)
### 17.2 Workspace Manager and Safety
- Deterministic workspace path per issue identifier
- Missing workspace directory is created
- Existing workspace directory is reused
- Existing non-directory path at workspace location is handled safely (replace or fail per
implementation policy)
- Optional workspace population/synchronization errors are surfaced
- Temporary artifacts (`tmp`, `.elixir_ls`) are removed during prep
- `after_create` hook runs only on new workspace creation
- `before_run` hook runs before each attempt and failure/timeouts abort the current attempt
- `after_run` hook runs after each attempt and failure/timeouts are logged and ignored
- `before_remove` hook runs on cleanup and failures/timeouts are ignored
- Workspace path sanitization and root containment invariants are enforced before agent launch
- Agent launch uses the per-issue workspace path as cwd and rejects out-of-root paths
### 17.3 Issue Tracker Client
- Candidate issue fetch uses active states and project slug
- Linear query uses the specified project filter field (`slugId`)
- Empty `fetch_issues_by_states([])` returns empty without API call
- Pagination preserves order across multiple pages
- Blockers are normalized from inverse relations of type `blocks`
- Labels are normalized to lowercase
- Issue state refresh by ID returns minimal normalized issues
- Issue state refresh query uses GraphQL ID typing (`[ID!]`) as specified in Section 11.2
- Error mapping for request errors, non-200, GraphQL errors, malformed payloads
### 17.4 Orchestrator Dispatch, Reconciliation, and Retry
- Dispatch sort order is priority then oldest creation time
- `Todo` issue with non-terminal blockers is not eligible
- `Todo` issue with terminal blockers is eligible
- Active-state issue refresh updates running entry state
- Non-active state stops running agent without workspace cleanup
- Terminal state stops running agent and cleans workspace
- Reconciliation with no running issues is a no-op
- Normal worker exit schedules a short continuation retry (attempt 1)
- Abnormal worker exit increments retries with 10s-based exponential backoff
- Retry backoff cap uses configured `agent.max_retry_backoff_ms`
- Retry queue entries include attempt, due time, identifier, and error
- Stall detection kills stalled sessions and schedules retry
- Slot exhaustion requeues retries with explicit error reason
- If a snapshot API is implemented, it returns running rows, retry rows, token totals, and rate
limits
- If a snapshot API is implemented, timeout/unavailable cases are surfaced
### 17.5 Coding-Agent App-Server Client
- Launch command uses workspace cwd and invokes `bash -lc <codex.command>`
- Startup handshake sends `initialize`, `initialized`, `thread/start`, `turn/start`
- `initialize` includes client identity/capabilities payload required by the targeted Codex
app-server protocol
- Policy-related startup payloads use the implementation's documented approval/sandbox settings
- `thread/start` and `turn/start` parse nested IDs and emit `session_started`
- Request/response read timeout is enforced
- Turn timeout is enforced
- Partial JSON lines are buffered until newline
- Stdout and stderr are handled separately; protocol JSON is parsed from stdout only
- Non-JSON stderr lines are logged but do not crash parsing
- Command/file-change approvals are handled according to the implementation's documented policy
- Unsupported dynamic tool calls are rejected without stalling the session
- User input requests are handled according to the implementation's documented policy and do not
stall indefinitely
- Usage and rate-limit payloads are extracted from nested payload shapes
- Compatible payload variants for approvals, user-input-required signals, and usage/rate-limit
telemetry are accepted when they preserve the same logical meaning
- If optional client-side tools are implemented, the startup handshake advertises the supported tool
specs required for discovery by the targeted app-server version
- If the optional `linear_graphql` client-side tool extension is implemented:
- the tool is advertised to the session
- valid `query` / `variables` inputs execute against configured Linear auth
- top-level GraphQL `errors` produce `success=false` while preserving the GraphQL body
- invalid arguments, missing auth, and transport failures return structured failure payloads
- unsupported tool names still fail without stalling the session
### 17.6 Observability
- Validation failures are operator-visible
- Structured logging includes issue/session context fields
- Logging sink failures do not crash orchestration
- Token/rate-limit aggregation remains correct across repeated agent updates
- If a human-readable status surface is implemented, it is driven from orchestrator state and does
not affect correctness
- If humanized event summaries are implemented, they cover key wrapper/agent event classes without
changing orchestrator behavior
### 17.7 CLI and Host Lifecycle
- CLI accepts an optional positional workflow path argument (`path-to-WORKFLOW.md`)
- CLI uses `./WORKFLOW.md` when no workflow path argument is provided
- CLI errors on nonexistent explicit workflow path or missing default `./WORKFLOW.md`
- CLI surfaces startup failure cleanly
- CLI exits with success when application starts and shuts down normally
- CLI exits nonzero when startup fails or the host process exits abnormally
### 17.8 Real Integration Profile (Recommended)
These checks are recommended for production readiness and may be skipped in CI when credentials,
network access, or external service permissions are unavailable.
- A real tracker smoke test can be run with valid credentials supplied by `LINEAR_API_KEY` or a
documented local bootstrap mechanism (for example `~/.linear_api_key`).
- Real integration tests should use isolated test identifiers/workspaces and clean up tracker
artifacts when practical.
- A skipped real-integration test should be reported as skipped, not silently treated as passed.
- If a real-integration profile is explicitly enabled in CI or release validation, failures should
fail that job.
## 18. Implementation Checklist (Definition of Done)
Use the same validation profiles as Section 17:
- Section 18.1 = `Core Conformance`
- Section 18.2 = `Extension Conformance`
- Section 18.3 = `Real Integration Profile`
### 18.1 Required for Conformance
- Workflow path selection supports explicit runtime path and cwd default
- `WORKFLOW.md` loader with YAML front matter + prompt body split
- Typed config layer with defaults and `$` resolution
- Dynamic `WORKFLOW.md` watch/reload/re-apply for config and prompt
- Polling orchestrator with single-authority mutable state
- Issue tracker client with candidate fetch + state refresh + terminal fetch
- Workspace manager with sanitized per-issue workspaces
- Workspace lifecycle hooks (`after_create`, `before_run`, `after_run`, `before_remove`)
- Hook timeout config (`hooks.timeout_ms`, default `60000`)
- Coding-agent app-server subprocess client with JSON line protocol
- Codex launch command config (`codex.command`, default `codex app-server`)
- Strict prompt rendering with `issue` and `attempt` variables
- Exponential retry queue with continuation retries after normal exit
- Configurable retry backoff cap (`agent.max_retry_backoff_ms`, default 5m)
- Reconciliation that stops runs on terminal/non-active tracker states
- Workspace cleanup for terminal issues (startup sweep + active transition)
- Structured logs with `issue_id`, `issue_identifier`, and `session_id`
- Operator-visible observability (structured logs; optional snapshot/status surface)
### 18.2 Recommended Extensions (Not Required for Conformance)
- Optional HTTP server honors CLI `--port` over `server.port`, uses a safe default bind host, and
exposes the baseline endpoints/error semantics in Section 13.7 if shipped.
- Optional `linear_graphql` client-side tool extension exposes raw Linear GraphQL access through the
app-server session using configured Symphony auth.
- TODO: Persist retry queue and session metadata across process restarts.
- TODO: Make observability settings configurable in workflow front matter without prescribing UI
implementation details.
- TODO: Add first-class tracker write APIs (comments/state transitions) in the orchestrator instead
of only via agent tools.
- TODO: Add pluggable issue tracker adapters beyond Linear.
### 18.3 Operational Validation Before Production (Recommended)
- Run the `Real Integration Profile` from Section 17.8 with valid credentials and network access.
- Verify hook execution and workflow path resolution on the target host OS/shell environment.
- If the optional HTTP server is shipped, verify the configured port behavior and loopback/default
bind expectations on the target environment.
## Appendix A. SSH Worker Extension (Optional)
This appendix describes a common extension profile in which Symphony keeps one central
orchestrator but executes worker runs on one or more remote hosts over SSH.
### A.1 Execution Model
- The orchestrator remains the single source of truth for polling, claims, retries, and
reconciliation.
- `worker.ssh_hosts` provides the candidate SSH destinations for remote execution.
- Each worker run is assigned to one host at a time, and that host becomes part of the run's
effective execution identity along with the issue workspace.
- `workspace.root` is interpreted on the remote host, not on the orchestrator host.
- The coding-agent app-server is launched over SSH stdio instead of as a local subprocess, so the
orchestrator still owns the session lifecycle even though commands execute remotely.
- Continuation turns inside one worker lifetime should stay on the same host and workspace.
- A remote host should satisfy the same basic contract as a local worker environment: reachable
shell, writable workspace root, coding-agent executable, and any required auth or repository
prerequisites.
### A.2 Scheduling Notes
- SSH hosts may be treated as a pool for dispatch.
- Implementations may prefer the previously used host on retries when that host is still
available.
- `worker.max_concurrent_agents_per_host` is an optional shared per-host cap across configured SSH
hosts.
- When all SSH hosts are at capacity, dispatch should wait rather than silently falling back to a
different execution mode.
- Implementations may fail over to another host when the original host is unavailable before work
has meaningfully started.
- Once a run has already produced side effects, a transparent rerun on another host should be
treated as a new attempt, not as invisible failover.
### A.3 Problems to Consider
- Remote environment drift:
- Each host needs the expected shell environment, coding-agent executable, auth, and repository
prerequisites.
- Workspace locality:
- Workspaces are usually host-local, so moving an issue to a different host is typically a cold
restart unless shared storage exists.
- Path and command safety:
- Remote path resolution, shell quoting, and workspace-boundary checks matter more once execution
crosses a machine boundary.
- Startup and failover semantics:
- Implementations should distinguish host-connectivity/startup failures from in-workspace agent
failures so the same ticket is not accidentally re-executed on multiple hosts.
- Host health and saturation:
- A dead or overloaded host should reduce available capacity, not cause duplicate execution or an
accidental fallback to local work.
- Cleanup and observability:
- Operators need to know which host owns a run, where its workspace lives, and whether cleanup
happened on the right machine.
================================================
FILE: elixir/.formatter.exs
================================================
# Used by "mix format"
[
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"],
line_length: 200
]
================================================
FILE: elixir/.gitattributes
================================================
test/fixtures/status_dashboard_snapshots/* linguist-generated=true
================================================
FILE: elixir/.gitignore
================================================
# The directory Mix will write compiled artifacts to.
/_build/
# If you run "mix test --cover", coverage assets end up here.
/cover/
# The directory Mix downloads your dependencies sources to.
/deps/
# Where third-party dependencies like ExDoc output generated docs.
/doc/
# Temporary files, for example, from tests.
/tmp/
# Generated browser assets.
/priv/static/assets/
# Local runtime logs.
/log/
/logs/
# If the VM crashes, it generates a dump, let's ignore it too.
erl_crash.dump
# Elixir language server and tooling.
/.elixir_ls/
/.fetch/
# Editor / OS temporary files.
.DS_Store
*.swp
*.swo
*~
# IDE folders.
.idea/
.vscode/
/bin/
# Local environment and auth artifacts.
.env
.env.*
.secrets
.credentials
status.txt
.codex/original-user-prompt.txt
# Also ignore archive artifacts (built via "mix archive.build").
*.ez
# Ignore package tarball (built via "mix hex.build").
symphony_elixir-*.tar
================================================
FILE: elixir/AGENTS.md
================================================
# Symphony Elixir
This directory contains the Elixir agent orchestration service that polls Linear, creates per-issue workspaces, and runs Codex in app-server mode.
## Environment
- Elixir: `1.19.x` (OTP 28) via `mise`.
- Install deps: `mix setup`.
- Main quality gate: `make all` (format check, lint, coverage, dialyzer).
## Codebase-Specific Conventions
- Runtime config is loaded from `WORKFLOW.md` front matter via `SymphonyElixir.Workflow` and `SymphonyElixir.Config`.
- Keep the implementation aligned with [`../SPEC.md`](../SPEC.md) where practical.
- The implementation may be a superset of the spec.
- The implementation must not conflict with the spec.
- If implementation changes meaningfully alter the intended behavior, update the spec in the same
change where practical so the spec stays current.
- Prefer adding config access through `SymphonyElixir.Config` instead of ad-hoc env reads.
- Workspace safety is critical:
- Never run Codex turn cwd in source repo.
- Workspaces must stay under configured workspace root.
- Orchestrator behavior is stateful and concurrency-sensitive; preserve retry, reconciliation, and cleanup semantics.
- Follow `docs/logging.md` for logging conventions and required issue/session context fields.
## Tests and Validation
Run targeted tests while iterating, then run full gates before handoff.
```bash
make all
```
## Required Rules
- Public functions (`def`) in `lib/` must have an adjacent `@spec`.
- `defp` specs are optional.
- `@impl` callback implementations are exempt from local `@spec` requirement.
- Keep changes narrowly scoped; avoid unrelated refactors.
- Follow existing module/style patterns in `lib/symphony_elixir/*`.
Validation command:
```bash
mix specs.check
```
## PR Requirements
- PR body must follow `../.github/pull_request_template.md` exactly.
- Validate PR body locally when needed:
```bash
mix pr_body.check --file /path/to/pr_body.md
```
## Docs Update Policy
If behavior/config changes, update docs in the same PR:
- `../README.md` for project concept and goals.
- `README.md` for Elixir implementation and run instructions.
- `WORKFLOW.md` for workflow/config contract changes.
================================================
FILE: elixir/Makefile
================================================
.PHONY: help all setup deps build fmt fmt-check lint test coverage ci dialyzer e2e
MIX ?= mix
help:
@echo "Targets: setup, deps, fmt, fmt-check, lint, test, coverage, dialyzer, e2e, ci"
setup:
$(MIX) setup
deps:
$(MIX) deps.get
build:
$(MIX) build
fmt:
$(MIX) format
fmt-check:
$(MIX) format --check-formatted
lint:
$(MIX) lint
coverage:
$(MIX) test --cover
test:
$(MIX) test
dialyzer:
$(MIX) deps.get
$(MIX) dialyzer --format short
e2e:
SYMPHONY_RUN_LIVE_E2E=1 $(MIX) test test/symphony_elixir/live_e2e_test.exs
ci:
$(MAKE) setup
$(MAKE) build
$(MAKE) fmt-check
$(MAKE) lint
$(MAKE) coverage
$(MAKE) dialyzer
all: ci
================================================
FILE: elixir/README.md
================================================
# Symphony Elixir
This directory contains the current Elixir/OTP implementation of Symphony, based on
[`SPEC.md`](../SPEC.md) at the repository root.
> [!WARNING]
> Symphony Elixir is prototype software intended for evaluation only and is presented as-is.
> We recommend implementing your own hardened version based on `SPEC.md`.
## Screenshot

## How it works
1. Polls Linear for candidate work
2. Creates a workspace per issue
3. Launches Codex in [App Server mode](https://developers.openai.com/codex/app-server/) inside the
workspace
4. Sends a workflow prompt to Codex
5. Keeps Codex working on the issue until the work is done
During app-server sessions, Symphony also serves a client-side `linear_graphql` tool so that repo
skills can make raw Linear GraphQL calls.
If a claimed issue moves to a terminal state (`Done`, `Closed`, `Cancelled`, or `Duplicate`),
Symphony stops the active agent for that issue and cleans up matching workspaces.
## How to use it
1. Make sure your codebase is set up to work well with agents: see
[Harness engineering](https://openai.com/index/harness-engineering/).
2. Get a new personal token in Linear via Settings → Security & access → Personal API keys, and
set it as the `LINEAR_API_KEY` environment variable.
3. Copy this directory's `WORKFLOW.md` to your repo.
4. Optionally copy the `commit`, `push`, `pull`, `land`, and `linear` skills to your repo.
- The `linear` skill expects Symphony's `linear_graphql` app-server tool for raw Linear GraphQL
operations such as comment editing or upload flows.
5. Customize the copied `WORKFLOW.md` file for your project.
- To get your project's slug, right-click the project and copy its URL. The slug is part of the
URL.
- When creating a workflow based on this repo, note that it depends on non-standard Linear
issue statuses: "Rework", "Human Review", and "Merging". You can customize them in
Team Settings → Workflow in Linear.
6. Follow the instructions below to install the required runtime dependencies and start the service.
## Prerequisites
We recommend using [mise](https://mise.jdx.dev/) to manage Elixir/Erlang versions.
```bash
mise install
mise exec -- elixir --version
```
## Run
```bash
git clone https://github.com/openai/symphony
cd symphony/elixir
mise trust
mise install
mise exec -- mix setup
mise exec -- mix build
mise exec -- ./bin/symphony ./WORKFLOW.md
```
## Configuration
Pass a custom workflow file path to `./bin/symphony` when starting the service:
```bash
./bin/symphony /path/to/custom/WORKFLOW.md
```
If no path is passed, Symphony defaults to `./WORKFLOW.md`.
Optional flags:
- `--logs-root` tells Symphony to write logs under a different directory (default: `./log`)
- `--port` also starts the Phoenix observability service (default: disabled)
The `WORKFLOW.md` file uses YAML front matter for configuration, plus a Markdown body used as the
Codex session prompt.
Minimal example:
```md
---
tracker:
kind: linear
project_slug: "..."
workspace:
root: ~/code/workspaces
hooks:
after_create: |
git clone git@github.com:your-org/your-repo.git .
agent:
max_concurrent_agents: 10
max_turns: 20
codex:
command: codex app-server
---
You are working on a Linear issue {{ issue.identifier }}.
Title: {{ issue.title }} Body: {{ issue.description }}
```
Notes:
- If a value is missing, defaults are used.
- Safer Codex defaults are used when policy fields are omitted:
- `codex.approval_policy` defaults to `{"reject":{"sandbox_approval":true,"rules":true,"mcp_elicitations":true}}`
- `codex.thread_sandbox` defaults to `workspace-write`
- `codex.turn_sandbox_policy` defaults to a `workspaceWrite` policy rooted at the current issue workspace
- Supported `codex.approval_policy` values depend on the targeted Codex app-server version. In the current local Codex schema, string values include `untrusted`, `on-failure`, `on-request`, and `never`, and object-form `reject` is also supported.
- Supported `codex.thread_sandbox` values: `read-only`, `workspace-write`, `danger-full-access`.
- When `codex.turn_sandbox_policy` is set explicitly, Symphony passes the map through to Codex
unchanged. Compatibility then depends on the targeted Codex app-server version rather than local
Symphony validation.
- `agent.max_turns` caps how many back-to-back Codex turns Symphony will run in a single agent
invocation when a turn completes normally but the issue is still in an active state. Default: `20`.
- If the Markdown body is blank, Symphony uses a default prompt template that includes the issue
identifier, title, and body.
- Use `hooks.after_create` to bootstrap a fresh workspace. For a Git-backed repo, you can run
`git clone ... .` there, along with any other setup commands you need.
- If a hook needs `mise exec` inside a freshly cloned workspace, trust the repo config and fetch
the project dependencies in `hooks.after_create` before invoking `mise` later from other hooks.
- `tracker.api_key` reads from `LINEAR_API_KEY` when unset or when value is `$LINEAR_API_KEY`.
- For path values, `~` is expanded to the home directory.
- For env-backed path values, use `$VAR`. `workspace.root` resolves `$VAR` before path handling,
while `codex.command` stays a shell command string and any `$VAR` expansion there happens in the
launched shell.
```yaml
tracker:
api_key: $LINEAR_API_KEY
workspace:
root: $SYMPHONY_WORKSPACE_ROOT
hooks:
after_create: |
git clone --depth 1 "$SOURCE_REPO_URL" .
codex:
command: "$CODEX_BIN app-server --model gpt-5.3-codex"
```
- If `WORKFLOW.md` is missing or has invalid YAML at startup, Symphony does not boot.
- If a later reload fails, Symphony keeps running with the last known good workflow and logs the
reload error until the file is fixed.
- `server.port` or CLI `--port` enables the optional Phoenix LiveView dashboard and JSON API at
`/`, `/api/v1/state`, `/api/v1/<issue_identifier>`, and `/api/v1/refresh`.
## Web dashboard
The observability UI now runs on a minimal Phoenix stack:
- LiveView for the dashboard at `/`
- JSON API for operational debugging under `/api/v1/*`
- Bandit as the HTTP server
- Phoenix dependency static assets for the LiveView client bootstrap
## Project Layout
- `lib/`: application code and Mix tasks
- `test/`: ExUnit coverage for runtime behavior
- `WORKFLOW.md`: in-repo workflow contract used by local runs
- `../.codex/`: repository-local Codex skills and setup helpers
## Testing
```bash
make all
```
Run the real external end-to-end test only when you want Symphony to create disposable Linear
resources and launch a real `codex app-server` session:
```bash
cd elixir
export LINEAR_API_KEY=...
make e2e
```
Optional environment variables:
- `SYMPHONY_LIVE_LINEAR_TEAM_KEY` defaults to `SYME2E`
- `SYMPHONY_LIVE_SSH_WORKER_HOSTS` uses those SSH hosts when set, as a comma-separated list
`make e2e` runs two live scenarios:
- one with a local worker
- one with SSH workers
If `SYMPHONY_LIVE_SSH_WORKER_HOSTS` is unset, the SSH scenario uses `docker compose` to start two
disposable SSH workers on `localhost:<port>`. The live test generates a temporary SSH keypair,
mounts the host `~/.codex/auth.json` into each worker, verifies that Symphony can talk to them
over real SSH, then runs the same orchestration flow against those worker addresses. This keeps
the transport representative without depending on long-lived external machines.
Set `SYMPHONY_LIVE_SSH_WORKER_HOSTS` if you want `make e2e` to target real SSH hosts instead.
The live test creates a temporary Linear project and issue, writes a temporary `WORKFLOW.md`, runs
a real agent turn, verifies the workspace side effect, requires Codex to comment on and close the
Linear issue, then marks the project completed so the run remains visible in Linear.
## FAQ
### Why Elixir?
Elixir is built on Erlang/BEAM/OTP, which is great for supervising long-running processes. It has an
active ecosystem of tools and libraries. It also supports hot code reloading without stopping
actively running subagents, which is very useful during development.
### What's the easiest way to set this up for my own codebase?
Launch `codex` in your repo, give it the URL to the Symphony repo, and ask it to set things up for
you.
## License
This project is licensed under the [Apache License 2.0](../LICENSE).
================================================
FILE: elixir/WORKFLOW.md
================================================
---
tracker:
kind: linear
project_slug: "symphony-0c79b11b75ea"
active_states:
- Todo
- In Progress
- Merging
- Rework
terminal_states:
- Closed
- Cancelled
- Canceled
- Duplicate
- Done
polling:
interval_ms: 5000
workspace:
root: ~/code/symphony-workspaces
hooks:
after_create: |
git clone --depth 1 https://github.com/openai/symphony .
if command -v mise >/dev/null 2>&1; then
cd elixir && mise trust && mise exec -- mix deps.get
fi
before_remove: |
cd elixir && mise exec -- mix workspace.before_remove
agent:
max_concurrent_agents: 10
max_turns: 20
codex:
command: codex --config shell_environment_policy.inherit=all --config model_reasoning_effort=xhigh --model gpt-5.3-codex app-server
approval_policy: never
thread_sandbox: workspace-write
turn_sandbox_policy:
type: workspaceWrite
---
You are working on a Linear ticket `{{ issue.identifier }}`
{% if attempt %}
Continuation context:
- This is retry attempt #{{ attempt }} because the ticket is still in an active state.
- Resume from the current workspace state instead of restarting from scratch.
- Do not repeat already-completed investigation or validation unless needed for new code changes.
- Do not end the turn while the issue remains in an active state unless you are blocked by missing required permissions/secrets.
{% endif %}
Issue context:
Identifier: {{ issue.identifier }}
Title: {{ issue.title }}
Current status: {{ issue.state }}
Labels: {{ issue.labels }}
URL: {{ issue.url }}
Description:
{% if issue.description %}
{{ issue.description }}
{% else %}
No description provided.
{% endif %}
Instructions:
1. This is an unattended orchestration session. Never ask a human to perform follow-up actions.
2. Only stop early for a true blocker (missing required auth/permissions/secrets). If blocked, record it in the workpad and move the issue according to workflow.
3. Final message must report completed actions and blockers only. Do not include "next steps for user".
Work only in the provided repository copy. Do not touch any other path.
## Prerequisite: Linear MCP or `linear_graphql` tool is available
The agent should be able to talk to Linear, either via a configured Linear MCP server or injected `linear_graphql` tool. If none are present, stop and ask the user to configure Linear.
## Default posture
- Start by determining the ticket's current status, then follow the matching flow for that status.
- Start every task by opening the tracking workpad comment and bringing it up to date before doing new implementation work.
- Spend extra effort up front on planning and verification design before implementation.
- Reproduce first: always confirm the current behavior/issue signal before changing code so the fix target is explicit.
- Keep ticket metadata current (state, checklist, acceptance criteria, links).
- Treat a single persistent Linear comment as the source of truth for progress.
- Use that single workpad comment for all progress and handoff notes; do not post separate "done"/summary comments.
- Treat any ticket-authored `Validation`, `Test Plan`, or `Testing` section as non-negotiable acceptance input: mirror it in the workpad and execute it before considering the work complete.
- When meaningful out-of-scope improvements are discovered during execution,
file a separate Linear issue instead of expanding scope. The follow-up issue
must include a clear title, description, and acceptance criteria, be placed in
`Backlog`, be assigned to the same project as the current issue, link the
current issue as `related`, and use `blockedBy` when the follow-up depends on
the current issue.
- Move status only when the matching quality bar is met.
- Operate autonomously end-to-end unless blocked by missing requirements, secrets, or permissions.
- Use the blocked-access escape hatch only for true external blockers (missing required tools/auth) after exhausting documented fallbacks.
## Related skills
- `linear`: interact with Linear.
- `commit`: produce clean, logical commits during implementation.
- `push`: keep remote branch current and publish updates.
- `pull`: keep branch updated with latest `origin/main` before handoff.
- `land`: when ticket reaches `Merging`, explicitly open and follow `.codex/skills/land/SKILL.md`, which includes the `land` loop.
## Status map
- `Backlog` -> out of scope for this workflow; do not modify.
- `Todo` -> queued; immediately transition to `In Progress` before active work.
- Special case: if a PR is already attached, treat as feedback/rework loop (run full PR feedback sweep, address or explicitly push back, revalidate, return to `Human Review`).
- `In Progress` -> implementation actively underway.
- `Human Review` -> PR is attached and validated; waiting on human approval.
- `Merging` -> approved by human; execute the `land` skill flow (do not call `gh pr merge` directly).
- `Rework` -> reviewer requested changes; planning + implementation required.
- `Done` -> terminal state; no further action required.
## Step 0: Determine current ticket state and route
1. Fetch the issue by explicit ticket ID.
2. Read the current state.
3. Route to the matching flow:
- `Backlog` -> do not modify issue content/state; stop and wait for human to move it to `Todo`.
- `Todo` -> immediately move to `In Progress`, then ensure bootstrap workpad comment exists (create if missing), then start execution flow.
- If PR is already attached, start by reviewing all open PR comments and deciding required changes vs explicit pushback responses.
- `In Progress` -> continue execution flow from current scratchpad comment.
- `Human Review` -> wait and poll for decision/review updates.
- `Merging` -> on entry, open and follow `.codex/skills/land/SKILL.md`; do not call `gh pr merge` directly.
- `Rework` -> run rework flow.
- `Done` -> do nothing and shut down.
4. Check whether a PR already exists for the current branch and whether it is closed.
- If a branch PR exists and is `CLOSED` or `MERGED`, treat prior branch work as non-reusable for this run.
- Create a fresh branch from `origin/main` and restart execution flow as a new attempt.
5. For `Todo` tickets, do startup sequencing in this exact order:
- `update_issue(..., state: "In Progress")`
- find/create `## Codex Workpad` bootstrap comment
- only then begin analysis/planning/implementation work.
6. Add a short comment if state and issue content are inconsistent, then proceed with the safest flow.
## Step 1: Start/continue execution (Todo or In Progress)
1. Find or create a single persistent scratchpad comment for the issue:
- Search existing comments for a marker header: `## Codex Workpad`.
- Ignore resolved comments while searching; only active/unresolved comments are eligible to be reused as the live workpad.
- If found, reuse that comment; do not create a new workpad comment.
- If not found, create one workpad comment and use it for all updates.
- Persist the workpad comment ID and only write progress updates to that ID.
2. If arriving from `Todo`, do not delay on additional status transitions: the issue should already be `In Progress` before this step begins.
3. Immediately reconcile the workpad before new edits:
- Check off items that are already done.
- Expand/fix the plan so it is comprehensive for current scope.
- Ensure `Acceptance Criteria` and `Validation` are current and still make sense for the task.
4. Start work by writing/updating a hierarchical plan in the workpad comment.
5. Ensure the workpad includes a compact environment stamp at the top as a code fence line:
- Format: `<host>:<abs-workdir>@<short-sha>`
- Example: `devbox-01:/home/dev-user/code/symphony-workspaces/MT-32@7bdde33bc`
- Do not include metadata already inferable from Linear issue fields (`issue ID`, `status`, `branch`, `PR link`).
6. Add explicit acceptance criteria and TODOs in checklist form in the same comment.
- If changes are user-facing, include a UI walkthrough acceptance criterion that describes the end-to-end user path to validate.
- If changes touch app files or app behavior, add explicit app-specific flow checks to `Acceptance Criteria` in the workpad (for example: launch path, changed interaction path, and expected result path).
- If the ticket description/comment context includes `Validation`, `Test Plan`, or `Testing` sections, copy those requirements into the workpad `Acceptance Criteria` and `Validation` sections as required checkboxes (no optional downgrade).
7. Run a principal-style self-review of the plan and refine it in the comment.
8. Before implementing, capture a concrete reproduction signal and record it in the workpad `Notes` section (command/output, screenshot, or deterministic UI behavior).
9. Run the `pull` skill to sync with latest `origin/main` before any code edits, then record the pull/sync result in the workpad `Notes`.
- Include a `pull skill evidence` note with:
- merge source(s),
- result (`clean` or `conflicts resolved`),
- resulting `HEAD` short SHA.
10. Compact context and proceed to execution.
## PR feedback sweep protocol (required)
When a ticket has an attached PR, run this protocol before moving to `Human Review`:
1. Identify the PR number from issue links/attachments.
2. Gather feedback from all channels:
- Top-level PR comments (`gh pr view --comments`).
- Inline review comments (`gh api repos/<owner>/<repo>/pulls/<pr>/comments`).
- Review summaries/states (`gh pr view --json reviews`).
3. Treat every actionable reviewer comment (human or bot), including inline review comments, as blocking until one of these is true:
- code/test/docs updated to address it, or
- explicit, justified pushback reply is posted on that thread.
4. Update the workpad plan/checklist to include each feedback item and its resolution status.
5. Re-run validation after feedback-driven changes and push updates.
6. Repeat this sweep until there are no outstanding actionable comments.
## Blocked-access escape hatch (required behavior)
Use this only when completion is blocked by missing required tools or missing auth/permissions that cannot be resolved in-session.
- GitHub is **not** a valid blocker by default. Always try fallback strategies first (alternate remote/auth mode, then continue publish/review flow).
- Do not move to `Human Review` for GitHub access/auth until all fallback strategies have been attempted and documented in the workpad.
- If a non-GitHub required tool is missing, or required non-GitHub auth is unavailable, move the ticket to `Human Review` with a short blocker brief in the workpad that includes:
- what is missing,
- why it blocks required acceptance/validation,
- exact human action needed to unblock.
- Keep the brief concise and action-oriented; do not add extra top-level comments outside the workpad.
## Step 2: Execution phase (Todo -> In Progress -> Human Review)
1. Determine current repo state (`branch`, `git status`, `HEAD`) and verify the kickoff `pull` sync result is already recorded in the workpad before implementation continues.
2. If current issue state is `Todo`, move it to `In Progress`; otherwise leave the current state unchanged.
3. Load the existing workpad comment and treat it as the active execution checklist.
- Edit it liberally whenever reality changes (scope, risks, validation approach, discovered tasks).
4. Implement against the hierarchical TODOs and keep the comment current:
- Check off completed items.
- Add newly discovered items in the appropriate section.
- Keep parent/child structure intact as scope evolves.
- Update the workpad immediately after each meaningful milestone (for example: reproduction complete, code change landed, validation run, review feedback addressed).
- Never leave completed work unchecked in the plan.
- For tickets that started as `Todo` with an attached PR, run the full PR feedback sweep protocol immediately after kickoff and before new feature work.
5. Run validation/tests required for the scope.
- Mandatory gate: execute all ticket-provided `Validation`/`Test Plan`/ `Testing` requirements when present; treat unmet items as incomplete work.
- Prefer a targeted proof that directly demonstrates the behavior you changed.
- You may make temporary local proof edits to validate assumptions (for example: tweak a local build input for `make`, or hardcode a UI account / response path) when this increases confidence.
- Revert every temporary proof edit before commit/push.
- Document these temporary proof steps and outcomes in the workpad `Validation`/`Notes` sections so reviewers can follow the evidence.
- If app-touching, run `launch-app` validation and capture/upload media via `github-pr-media` before handoff.
6. Re-check all acceptance criteria and close any gaps.
7. Before every `git push` attempt, run the required validation for your scope and confirm it passes; if it fails, address issues and rerun until green, then commit and push changes.
8. Attach PR URL to the issue (prefer attachment; use the workpad comment only if attachment is unavailable).
- Ensure the GitHub PR has label `symphony` (add it if missing).
9. Merge latest `origin/main` into branch, resolve conflicts, and rerun checks.
10. Update the workpad comment with final checklist status and validation notes.
- Mark completed plan/acceptance/validation checklist items as checked.
- Add final handoff notes (commit + validation summary) in the same workpad comment.
- Do not include PR URL in the workpad comment; keep PR linkage on the issue via attachment/link fields.
- Add a short `### Confusions` section at the bottom when any part of task execution was unclear/confusing, with concise bullets.
- Do not post any additional completion summary comment.
11. Before moving to `Human Review`, poll PR feedback and checks:
- Read the PR `Manual QA Plan` comment (when present) and use it to sharpen UI/runtime test coverage for the current change.
- Run the full PR feedback sweep protocol.
- Confirm PR checks are passing (green) after the latest changes.
- Confirm every required ticket-provided validation/test-plan item is explicitly marked complete in the workpad.
- Repeat this check-address-verify loop until no outstanding comments remain and checks are fully passing.
- Re-open and refresh the workpad before state transition so `Plan`, `Acceptance Criteria`, and `Validation` exactly match completed work.
12. Only then move issue to `Human Review`.
- Exception: if blocked by missing required non-GitHub tools/auth per the blocked-access escape hatch, move to `Human Review` with the blocker brief and explicit unblock actions.
13. For `Todo` tickets that already had a PR attached at kickoff:
- Ensure all existing PR feedback was reviewed and resolved, including inline review comments (code changes or explicit, justified pushback response).
- Ensure branch was pushed with any required updates.
- Then move to `Human Review`.
## Step 3: Human Review and merge handling
1. When the issue is in `Human Review`, do not code or change ticket content.
2. Poll for updates as needed, including GitHub PR review comments from humans and bots.
3. If review feedback requires changes, move the issue to `Rework` and follow the rework flow.
4. If approved, human moves the issue to `Merging`.
5. When the issue is in `Merging`, open and follow `.codex/skills/land/SKILL.md`, then run the `land` skill in a loop until the PR is merged. Do not call `gh pr merge` directly.
6. After merge is complete, move the issue to `Done`.
## Step 4: Rework handling
1. Treat `Rework` as a full approach reset, not incremental patching.
2. Re-read the full issue body and all human comments; explicitly identify what will be done differently this attempt.
3. Close the existing PR tied to the issue.
4. Remove the existing `## Codex Workpad` comment from the issue.
5. Create a fresh branch from `origin/main`.
6. Start over from the normal kickoff flow:
- If current issue state is `Todo`, move it to `In Progress`; otherwise keep the current state.
- Create a new bootstrap `## Codex Workpad` comment.
- Build a fresh plan/checklist and execute end-to-end.
## Completion bar before Human Review
- Step 1/2 checklist is fully complete and accurately reflected in the single workpad comment.
- Acceptance criteria and required ticket-provided validation items are complete.
- Validation/tests are green for the latest commit.
- PR feedback sweep is complete and no actionable comments remain.
- PR checks are green, branch is pushed, and PR is linked on the issue.
- Required PR metadata is present (`symphony` label).
- If app-touching, runtime validation/media requirements from `App runtime validation (required)` are complete.
## Guardrails
- If the branch PR is already closed/merged, do not reuse that branch or prior implementation state for continuation.
- For closed/merged branch PRs, create a new branch from `origin/main` and restart from reproduction/planning as if starting fresh.
- If issue state is `Backlog`, do not modify it; wait for human to move to `Todo`.
- Do not edit the issue body/description for planning or progress tracking.
- Use exactly one persistent workpad comment (`## Codex Workpad`) per issue.
- If comment editing is unavailable in-session, use the update script. Only report blocked if both MCP editing and script-based editing are unavailable.
- Temporary proof edits are allowed only for local verification and must be reverted before commit.
- If out-of-scope improvements are found, create a separate Backlog issue rather
than expanding current scope, and include a clear
title/description/acceptance criteria, same-project assignment, a `related`
link to the current issue, and `blockedBy` when the follow-up depends on the
current issue.
- Do not move to `Human Review` unless the `Completion bar before Human Review` is satisfied.
- In `Human Review`, do not make changes; wait and poll.
- If state is terminal (`Done`), do nothing and shut down.
- Keep issue text concise, specific, and reviewer-oriented.
- If blocked and no workpad exists yet, add one blocker comment describing blocker, impact, and next unblock action.
## Workpad template
Use this exact structure for the persistent workpad comment and keep it updated in place throughout execution:
````md
## Codex Workpad
```text
<hostname>:<abs-path>@<short-sha>
```
### Plan
- [ ] 1\. Parent task
- [ ] 1.1 Child task
- [ ] 1.2 Child task
- [ ] 2\. Parent task
### Acceptance Criteria
- [ ] Criterion 1
- [ ] Criterion 2
### Validation
- [ ] targeted tests: `<command>`
### Notes
- <short progress note with timestamp>
### Confusions
- <only include when something was confusing during execution>
````
================================================
FILE: elixir/config/config.exs
================================================
import Config
config :phoenix, :json_library, Jason
config :symphony_elixir, SymphonyElixirWeb.Endpoint,
adapter: Bandit.PhoenixAdapter,
url: [host: "localhost"],
render_errors: [
formats: [html: SymphonyElixirWeb.ErrorHTML, json: SymphonyElixirWeb.ErrorJSON],
layout: false
],
pubsub_server: SymphonyElixir.PubSub,
live_view: [signing_salt: "symphony-live-view"],
secret_key_base: String.duplicate("s", 64),
check_origin: false,
server: false
================================================
FILE: elixir/docs/logging.md
================================================
# Logging Best Practices
This guide defines logging conventions for Symphony so Codex can diagnose failures quickly.
## Goals
- Make logs searchable by issue and session.
- Capture enough execution context to identify root cause without reruns.
- Keep messages stable so dashboards/alerts are reliable.
## Required Context Fields
When logging issue-related work, include both identifiers:
- `issue_id`: Linear internal UUID (stable foreign key).
- `issue_identifier`: human ticket key (for example `MT-620`).
When logging Codex execution lifecycle events, include:
- `session_id`: combined Codex thread/turn identifier.
## Message Design
- Use explicit `key=value` pairs in message text for high-signal fields.
- Prefer deterministic wording for recurring lifecycle events.
- Include the action outcome (`completed`, `failed`, `retrying`) and the reason/error when available.
- Avoid logging large payloads unless required for debugging.
## Scope Guidance
- `AgentRunner`: log start/completion/failure with issue context, plus `session_id` when known.
- `Orchestrator`: log dispatch, retry, terminal/non-active transitions, and worker exits with issue context. Include `session_id` whenever running-entry data has it.
- `Codex.AppServer`: log session start/completion/error with issue context and `session_id`.
## Checklist For New Logs
- Is this event tied to a Linear issue? Include `issue_id` and `issue_identifier`.
- Is this event tied to a Codex session? Include `session_id`.
- Is the failure reason present and concise?
- Is the message format consistent with existing lifecycle logs?
================================================
FILE: elixir/docs/token_accounting.md
================================================
# Codex Token Accounting
This document explains how Codex reports token usage through the app-server protocol and how Symphony should account for it.
It is based on the current Codex source in `codex-rs`, especially:
- `app-server/README.md`
- `protocol/src/protocol.rs`
- `app-server/src/bespoke_event_handling.rs`
- `app-server-protocol/src/protocol/v2.rs`
- `exec/src/event_processor_with_jsonl_output.rs`
- `state/src/extract.rs`
## Short Version
- `last_token_usage` means "the latest increment".
- `total_token_usage` means "the cumulative total so far".
- `thread/tokenUsage/updated` is the live streaming notification for token usage.
- `turn/completed` carries final turn state, and turn-level usage is exposed separately from the live thread token stream.
- Generic `usage` fields are event-specific. Do not assume every `usage` payload is a cumulative thread total.
## Primary Source Semantics
Codex defines `TokenUsageInfo` like this:
```rust
pub struct TokenUsageInfo {
pub total_token_usage: TokenUsage,
pub last_token_usage: TokenUsage,
pub model_context_window: Option<i64>,
}
```
The important behavior is in `append_last_usage`:
```rust
pub fn append_last_usage(&mut self, last: &TokenUsage) {
self.total_token_usage.add_assign(last);
self.last_token_usage = last.clone();
}
```
That gives the core semantics:
- `last_token_usage`: the newest chunk of usage that was just added
- `total_token_usage`: the accumulated total after adding that chunk
This is the most important accounting rule in the Codex source.
## Event Types
### `codex/event/token_count`
Codex core emits token count events containing `TokenUsageInfo`.
These events can carry:
- `info.total_token_usage`
- `info.last_token_usage`
- `info.model_context_window`
Symphony sees these events wrapped inside the app-server message stream.
Meaning:
- `total_token_usage` is an absolute cumulative snapshot
- `last_token_usage` is the delta that produced that snapshot
### `thread/tokenUsage/updated`
The app-server converts token count events into a dedicated thread-scoped notification:
```rust
let notification = ThreadTokenUsageUpdatedNotification {
thread_id: conversation_id.to_string(),
turn_id,
token_usage,
};
```
`ThreadTokenUsage` is defined as:
```rust
pub struct ThreadTokenUsage {
pub total: TokenUsageBreakdown,
pub last: TokenUsageBreakdown,
pub model_context_window: Option<i64>,
}
```
And it is populated directly from `TokenUsageInfo`:
```rust
impl From<CoreTokenUsageInfo> for ThreadTokenUsage {
fn from(value: CoreTokenUsageInfo) -> Self {
Self {
total: value.total_token_usage.into(),
last: value.last_token_usage.into(),
model_context_window: value.model_context_window,
}
}
}
```
Meaning:
- `thread/tokenUsage/updated` is the canonical live notification for token usage
- `tokenUsage.total` is an absolute thread total
- `tokenUsage.last` is the latest increment that produced that total
The app-server README is explicit: token usage streams separately via `thread/tokenUsage/updated`.
### `turn/completed`
The app-server README says `turn/completed` carries final turn state and token usage.
There are two important details:
1. The app-server protocol `turn/completed` notification contains a final `turn` object.
2. The `exec` event processor also emits a turn-completed event that includes a `usage` struct.
In the `exec` event processor, the turn-completed usage is built from the most recent captured `total_token_usage`:
```rust
if let Some(info) = &ev.info {
self.last_total_token_usage = Some(info.total_token_usage.clone());
}
```
Then on turn completion:
```rust
let usage = if let Some(u) = &self.last_total_token_usage {
Usage {
input_tokens: u.input_tokens,
cached_input_tokens: u.cached_input_tokens,
output_tokens: u.output_tokens,
}
}
```
Important consequence:
- a turn-completed `usage` payload is not the same schema as `ThreadTokenUsage`
- it should be interpreted in the context of the specific event that emitted it
- it must not be blindly mixed with `thread/tokenUsage/updated` accounting
### Generic `usage`
Codex uses the word `usage` in multiple places.
That does not mean all `usage` maps have the same semantics.
Examples:
- `thread/tokenUsage/updated.tokenUsage.total`: absolute cumulative thread total
- `thread/tokenUsage/updated.tokenUsage.last`: latest delta
- turn-completed `usage`: event-specific completion usage payload
Rule:
- never classify a `usage` map by name alone
- classify it by event type and payload path
## What The Metrics Mean
### Absolute totals
These are safe high-water-mark style counters:
- `info.total_token_usage`
- `tokenUsage.total` on `thread/tokenUsage/updated`
Use these when you want:
- live dashboard totals
- stable per-thread accumulation
- recovery after missed intermediate events
### Deltas
These are incremental additions:
- `info.last_token_usage`
- `tokenUsage.last` on `thread/tokenUsage/updated`
Use these only when:
- no absolute total is available
- you are explicitly handling additive updates
### Context window
`model_context_window` is not spend. It is the model's context limit.
Codex also has logic that can "fill to context window", which sets:
- `total_token_usage.total_tokens = context_window`
- `last_token_usage.total_tokens = delta`
So `total_tokens` can reflect context-window normalization behavior, not just a raw upstream token report.
For Symphony, `model_context_window` should be displayed or logged separately from spend.
## Recommended Accounting Strategy For Symphony
Track usage per active Codex thread.
For each thread, keep:
- `absolute_total`: latest accepted absolute total snapshot
- `accumulated_total`: the total you expose in UI/API
- `last_seen_turn_id`
### Preferred source order
When a token-related event arrives, use this precedence:
1. `thread/tokenUsage/updated.tokenUsage.total`
2. `TokenCountEvent.info.total_token_usage`
Ignore these for accounting:
- `thread/tokenUsage/updated.tokenUsage.last`
- `TokenCountEvent.info.last_token_usage`
- generic `usage` maps
- turn-completed `usage`
Do not treat generic `params.usage` as equivalent to a cumulative thread total unless the event type makes that meaning explicit.
### Algorithm
#### If an absolute total is present
- Treat it as a thread-level snapshot.
- If it is greater than or equal to the stored `absolute_total`, replace the stored absolute total.
- Set exposed totals from that absolute snapshot.
- Do not add the corresponding delta again.
#### If no absolute total is present
- Ignore the event for accounting.
- Keep the last accepted absolute high-water mark unchanged.
### Why this matters
If you misclassify a per-turn `usage` payload as an absolute thread total, later turns can appear to stall because a smaller per-turn number is compared against a larger cumulative baseline.
## What Symphony Should And Should Not Do
### Do
- Prefer `thread/tokenUsage/updated` for live reporting.
- Treat `tokenUsage.total` as authoritative for thread totals.
- Key accounting by `thread_id`, not just issue id.
- Expect one thread to span multiple turns when Symphony reuses a live Codex thread.
### Do not
- Do not treat every `usage` map as absolute.
- Do not count `tokenUsage.last` or `last_token_usage` into dashboard totals.
- Do not add turn-completed `usage` on top of already-counted live thread totals unless you can prove it represents missing spend.
- Do not reset accounting just because a new turn starts on the same thread.
## Practical Interpretation For Symphony Logs
When reading raw app-server events:
- `codex/event/token_count`
- useful if you are inspecting nested `info.total_token_usage`
- `thread/tokenUsage/updated`
- best source for live dashboard and API totals
- `turn/completed`
- best used as end-of-turn state, not as an unconditional additive token event
## Why `total_token_usage` Is The Durable Choice
Codex itself consistently prefers cumulative totals when it needs durable state:
- the state extractor stores `info.total_token_usage.total_tokens`
- the exec event processor caches the last `total_token_usage` and uses that on turn completion
That is a strong signal for Symphony:
- use absolute totals as the main accounting surface
- ignore last/delta values for totals
## Recommended Symphony Documentation Contract
If Symphony documents token reporting externally, the contract should be:
- Live token totals come from Codex thread-scoped cumulative usage.
- Incremental usage may also be emitted, but Symphony does not use it for totals.
- Turn-completed usage is event-specific and should not be assumed to be a fresh additive increment.
- Reporting is thread-based, and multiple turns can occur on one thread.
## Implementation Checklist
- Prefer `thread/tokenUsage/updated.tokenUsage.total`
- Fallback to `info.total_token_usage`
- Ignore `last` for totals
- Key totals by `thread_id`
- Do not classify generic `usage` by field name alone
- Do not double-count turn-completed usage after live updates
================================================
FILE: elixir/lib/mix/tasks/pr_body.check.ex
================================================
defmodule Mix.Tasks.PrBody.Check do
use Mix.Task
@shortdoc "Validate PR body format against the repository PR template"
@moduledoc """
Validates a PR description markdown file against the structure and expectations
implied by the repository pull request template.
Usage:
mix pr_body.check --file /path/to/pr_body.md
"""
@template_paths [
".github/pull_request_template.md",
"../.github/pull_request_template.md"
]
@impl Mix.Task
def run(args) do
{opts, _argv, invalid} = OptionParser.parse(args, strict: [file: :string, help: :boolean], aliases: [h: :help])
cond do
opts[:help] ->
Mix.shell().info(@moduledoc)
invalid != [] ->
Mix.raise("Invalid option(s): #{inspect(invalid)}")
true ->
file_path = required_opt(opts, :file)
with {:ok, template_path, template} <- read_template(),
{:ok, body} <- read_file(file_path),
{:ok, headings} <- extract_template_headings(template, template_path),
:ok <- lint_and_print(template_path, template, body, headings) do
Mix.shell().info("PR body format OK")
else
{:error, message} -> Mix.raise(message)
end
end
end
defp read_template do
case Enum.find_value(@template_paths, &read_template_candidate/1) do
{:ok, _path, _template} = result ->
result
nil ->
joined_paths = Enum.join(@template_paths, ", ")
{:error, "Unable to read PR template from any of: #{joined_paths}"}
end
end
defp read_template_candidate(path) do
case File.read(path) do
{:ok, content} -> {:ok, path, content}
{:error, _reason} -> nil
end
end
defp required_opt(opts, key) do
case opts[key] do
nil -> Mix.raise("Missing required option --#{key}")
value -> value
end
end
defp read_file(path) do
case File.read(path) do
{:ok, content} -> {:ok, content}
{:error, reason} -> {:error, "Unable to read #{path}: #{inspect(reason)}"}
end
end
defp extract_template_headings(template, template_path) do
headings =
Regex.scan(~r/^\#{4,6}\s+.+$/m, template)
|> Enum.map(&hd/1)
if headings == [] do
{:error, "No markdown headings found in #{template_path}"}
else
{:ok, headings}
end
end
defp lint_and_print(template_path, template, body, headings) do
errors = lint(template, body, headings)
if errors == [] do
:ok
else
Enum.each(errors, fn err -> Mix.shell().error("ERROR: #{err}") end)
{:error, "PR body format invalid. Read `#{template_path}` and follow it precisely."}
end
end
defp lint(template, body, headings) do
[]
|> check_required_headings(body, headings)
|> check_order(body, headings)
|> check_no_placeholders(body)
|> check_sections_from_template(template, body, headings)
end
defp check_required_headings(errors, body, headings) do
missing = Enum.filter(headings, fn heading -> heading_position(body, heading) == :nomatch end)
errors ++ Enum.map(missing, fn heading -> "Missing required heading: #{heading}" end)
end
defp check_order(errors, body, headings) do
positions =
headings
|> Enum.map(&heading_position(body, &1))
|> Enum.reject(&(&1 == :nomatch))
if positions == Enum.sort(positions), do: errors, else: errors ++ ["Required headings are out of order."]
end
defp check_no_placeholders(errors, body) do
if String.contains?(body, "<!--") do
errors ++ ["PR description still contains template placeholder comments (<!-- ... -->)."]
else
errors
end
end
defp check_sections_from_template(errors, template, body, headings) do
Enum.reduce(headings, errors, fn heading, acc ->
template_section = capture_heading_section(template, heading, headings)
body_section = capture_heading_section(body, heading, headings)
cond do
is_nil(body_section) ->
acc
String.trim(body_section) == "" ->
acc ++ ["Section cannot be empty: #{heading}"]
true ->
acc
|> maybe_require_bullets(heading, template_section, body_section)
|> maybe_require_checkboxes(heading, template_section, body_section)
end
end)
end
defp maybe_require_bullets(errors, heading, template_section, body_section) do
requires_bullets = Regex.match?(~r/^- /m, template_section || "")
if requires_bullets and not Regex.match?(~r/^- /m, body_section) do
errors ++ ["Section must include at least one bullet item: #{heading}"]
else
errors
end
end
defp maybe_require_checkboxes(errors, heading, template_section, body_section) do
requires_checkboxes = Regex.match?(~r/^- \[ \] /m, template_section || "")
if requires_checkboxes and not Regex.match?(~r/^- \[[ xX]\] /m, body_section) do
errors ++ ["Section must include at least one checkbox item: #{heading}"]
else
errors
end
end
defp heading_position(body, heading) do
case :binary.match(body, heading) do
{idx, _len} -> idx
:nomatch -> :nomatch
end
end
defp capture_heading_section(doc, heading, headings) do
with {heading_idx, _} <- :binary.match(doc, heading),
section_start <- heading_idx + byte_size(heading),
t
gitextract_auczxnks/
├── .codex/
│ ├── skills/
│ │ ├── commit/
│ │ │ └── SKILL.md
│ │ ├── debug/
│ │ │ └── SKILL.md
│ │ ├── land/
│ │ │ ├── SKILL.md
│ │ │ └── land_watch.py
│ │ ├── linear/
│ │ │ └── SKILL.md
│ │ ├── pull/
│ │ │ └── SKILL.md
│ │ └── push/
│ │ └── SKILL.md
│ └── worktree_init.sh
├── .github/
│ ├── pull_request_template.md
│ └── workflows/
│ ├── make-all.yml
│ └── pr-description-lint.yml
├── LICENSE
├── NOTICE
├── README.md
├── SPEC.md
└── elixir/
├── .formatter.exs
├── .gitattributes
├── .gitignore
├── AGENTS.md
├── Makefile
├── README.md
├── WORKFLOW.md
├── config/
│ └── config.exs
├── docs/
│ ├── logging.md
│ └── token_accounting.md
├── lib/
│ ├── mix/
│ │ └── tasks/
│ │ ├── pr_body.check.ex
│ │ ├── specs.check.ex
│ │ └── workspace.before_remove.ex
│ ├── symphony_elixir/
│ │ ├── agent_runner.ex
│ │ ├── cli.ex
│ │ ├── codex/
│ │ │ ├── app_server.ex
│ │ │ └── dynamic_tool.ex
│ │ ├── config/
│ │ │ └── schema.ex
│ │ ├── config.ex
│ │ ├── http_server.ex
│ │ ├── linear/
│ │ │ ├── adapter.ex
│ │ │ ├── client.ex
│ │ │ └── issue.ex
│ │ ├── log_file.ex
│ │ ├── orchestrator.ex
│ │ ├── path_safety.ex
│ │ ├── prompt_builder.ex
│ │ ├── specs_check.ex
│ │ ├── ssh.ex
│ │ ├── status_dashboard.ex
│ │ ├── tracker/
│ │ │ └── memory.ex
│ │ ├── tracker.ex
│ │ ├── workflow.ex
│ │ ├── workflow_store.ex
│ │ └── workspace.ex
│ ├── symphony_elixir.ex
│ └── symphony_elixir_web/
│ ├── components/
│ │ └── layouts.ex
│ ├── controllers/
│ │ ├── observability_api_controller.ex
│ │ └── static_asset_controller.ex
│ ├── endpoint.ex
│ ├── error_html.ex
│ ├── error_json.ex
│ ├── live/
│ │ └── dashboard_live.ex
│ ├── observability_pubsub.ex
│ ├── presenter.ex
│ ├── router.ex
│ └── static_assets.ex
├── mise.toml
├── mix.exs
├── priv/
│ └── static/
│ └── dashboard.css
└── test/
├── fixtures/
│ └── status_dashboard_snapshots/
│ ├── backoff_queue.evidence.md
│ ├── backoff_queue.snapshot.txt
│ ├── credits_unlimited.evidence.md
│ ├── credits_unlimited.snapshot.txt
│ ├── idle.evidence.md
│ ├── idle.snapshot.txt
│ ├── idle_with_dashboard_url.evidence.md
│ ├── idle_with_dashboard_url.snapshot.txt
│ ├── super_busy.evidence.md
│ └── super_busy.snapshot.txt
├── mix/
│ └── tasks/
│ ├── pr_body_check_test.exs
│ ├── specs_check_task_test.exs
│ └── workspace_before_remove_test.exs
├── support/
│ ├── live_e2e_docker/
│ │ ├── Dockerfile
│ │ ├── docker-compose.yml
│ │ ├── live_worker_entrypoint.sh
│ │ └── symphony-live-worker.conf
│ ├── snapshot_support.exs
│ └── test_support.exs
├── symphony_elixir/
│ ├── app_server_test.exs
│ ├── cli_test.exs
│ ├── core_test.exs
│ ├── dynamic_tool_test.exs
│ ├── extensions_test.exs
│ ├── live_e2e_test.exs
│ ├── log_file_test.exs
│ ├── observability_pubsub_test.exs
│ ├── orchestrator_status_test.exs
│ ├── specs_check_test.exs
│ ├── ssh_test.exs
│ ├── status_dashboard_snapshot_test.exs
│ └── workspace_and_config_test.exs
└── test_helper.exs
SYMBOL INDEX (898 symbols across 57 files)
FILE: .codex/skills/land/land_watch.py
class PrInfo (line 23) | class PrInfo:
class RateLimitError (line 31) | class RateLimitError(RuntimeError):
function is_rate_limit_error (line 35) | def is_rate_limit_error(error: str) -> bool:
function run_gh (line 39) | async def run_gh(*args: str) -> str:
function get_pr_info (line 65) | async def get_pr_info() -> PrInfo:
function get_paginated_list (line 82) | async def get_paginated_list(endpoint: str) -> list[dict[str, Any]]:
function get_issue_comments (line 104) | async def get_issue_comments(pr_number: int) -> list[dict[str, Any]]:
function get_review_comments (line 110) | async def get_review_comments(pr_number: int) -> list[dict[str, Any]]:
function get_reviews (line 116) | async def get_reviews(pr_number: int) -> list[dict[str, Any]]:
function get_check_runs (line 138) | async def get_check_runs(head_sha: str) -> list[dict[str, Any]]:
function parse_time (line 164) | def parse_time(value: str) -> datetime:
function sanitize_terminal_output (line 172) | def sanitize_terminal_output(value: str) -> str:
function check_timestamp (line 176) | def check_timestamp(check: dict[str, Any]) -> datetime | None:
function dedupe_check_runs (line 184) | def dedupe_check_runs(check_runs: list[dict[str, Any]]) -> list[dict[str...
function summarize_checks (line 201) | def summarize_checks(check_runs: list[dict[str, Any]]) -> tuple[bool, bo...
function latest_review_request_at (line 221) | def latest_review_request_at(comments: list[dict[str, Any]]) -> datetime...
function filter_codex_comments (line 237) | def filter_codex_comments(
function is_codex_bot_user (line 268) | def is_codex_bot_user(user: dict[str, Any]) -> bool:
function is_bot_user (line 273) | def is_bot_user(user: dict[str, Any]) -> bool:
function is_codex_reply_body (line 282) | def is_codex_reply_body(body: str) -> bool:
function is_codex_review_body (line 286) | def is_codex_review_body(body: str) -> bool:
function latest_codex_issue_reply_time (line 290) | def latest_codex_issue_reply_time(
function filter_human_issue_comments (line 306) | def filter_human_issue_comments(comments: list[dict[str, Any]]) -> list[...
function filter_codex_review_issue_comments (line 330) | def filter_codex_review_issue_comments(
function thread_root_id (line 350) | def thread_root_id(comment: dict[str, Any]) -> int | None:
function comment_time (line 354) | def comment_time(comment: dict[str, Any]) -> datetime | None:
function latest_codex_reply_by_thread (line 361) | def latest_codex_reply_by_thread(
function filter_human_review_comments (line 379) | def filter_human_review_comments(
function is_blocking_review (line 401) | def is_blocking_review(
function review_timestamp (line 432) | def review_timestamp(review: dict[str, Any]) -> datetime | None:
function dedupe_reviews (line 439) | def dedupe_reviews(reviews: list[dict[str, Any]]) -> list[dict[str, Any]]:
function filter_blocking_reviews (line 458) | def filter_blocking_reviews(
function is_merge_conflicting (line 469) | def is_merge_conflicting(pr: PrInfo) -> bool:
function fetch_review_context (line 473) | async def fetch_review_context(
function raise_on_human_feedback (line 488) | def raise_on_human_feedback(
function wait_for_codex (line 514) | async def wait_for_codex(pr_number: int, checks_done: asyncio.Event) -> ...
function wait_for_checks (line 547) | async def wait_for_checks(head_sha: str, checks_done: asyncio.Event) -> ...
function watch_pr (line 575) | async def watch_pr() -> None:
FILE: elixir/lib/mix/tasks/pr_body.check.ex
class Mix.Tasks.PrBody.Check (line 1) | defmodule Mix.Tasks.PrBody.Check
method run (line 21) | def run(args) do
method read_template (line 45) | defp read_template do
method read_template_candidate (line 56) | defp read_template_candidate(path) do
method required_opt (line 63) | defp required_opt(opts, key) do
method read_file (line 70) | defp read_file(path) do
method extract_template_headings (line 77) | defp extract_template_headings(template, template_path) do
method lint_and_print (line 89) | defp lint_and_print(template_path, template, body, headings) do
method lint (line 101) | defp lint(template, body, headings) do
method check_required_headings (line 109) | defp check_required_headings(errors, body, headings) do
method check_order (line 114) | defp check_order(errors, body, headings) do
method check_no_placeholders (line 123) | defp check_no_placeholders(errors, body) do
method check_sections_from_template (line 131) | defp check_sections_from_template(errors, template, body, headings) do
method maybe_require_bullets (line 151) | defp maybe_require_bullets(errors, heading, template_section, body_sec...
method maybe_require_checkboxes (line 161) | defp maybe_require_checkboxes(errors, heading, template_section, body_...
method heading_position (line 171) | defp heading_position(body, heading) do
method capture_heading_section (line 178) | defp capture_heading_section(doc, heading, headings) do
method extract_section_content (line 191) | defp extract_section_content(doc, content_start, heading, headings) do
method next_heading_offset (line 200) | defp next_heading_offset(content, heading, headings) do
method headings_after (line 211) | defp headings_after(current_heading, headings) do
FILE: elixir/lib/mix/tasks/specs.check.ex
class Mix.Tasks.Specs.Check (line 1) | defmodule Mix.Tasks.Specs.Check
method run (line 15) | def run(args) do
method load_exemptions (line 41) | defp load_exemptions(path) do
FILE: elixir/lib/mix/tasks/workspace.before_remove.ex
class Mix.Tasks.Workspace.BeforeRemove (line 1) | defmodule Mix.Tasks.Workspace.BeforeRemove
method run (line 21) | def run(args) do
method maybe_close_open_pull_requests (line 43) | defp maybe_close_open_pull_requests(_repo, nil), do: :ok
method maybe_close_open_pull_requests (line 45) | defp maybe_close_open_pull_requests(repo, branch) do
method gh_available? (line 55) | defp gh_available? do
method gh_authenticated? (line 59) | defp gh_authenticated? do
method list_open_pull_request_numbers (line 63) | defp list_open_pull_request_numbers(repo, branch) do
method close_pull_request (line 88) | defp close_pull_request(repo, branch, pr_number) do
method closing_comment (line 108) | defp closing_comment(branch) do
method format_output (line 112) | defp format_output(""), do: ""
method format_output (line 113) | defp format_output(output), do: " output=#{inspect(output)}"
method current_branch (line 115) | defp current_branch do
method run_command (line 128) | defp run_command(command, args) do
FILE: elixir/lib/symphony_elixir.ex
class SymphonyElixir (line 1) | defmodule SymphonyElixir
method start_link (line 10) | def start_link(opts \\ []) do
class SymphonyElixir.Application (line 15) | defmodule SymphonyElixir.Application
method start (line 23) | def start(_type, _args) do
method stop (line 43) | def stop(_state) do
FILE: elixir/lib/symphony_elixir/agent_runner.ex
class SymphonyElixir.AgentRunner (line 1) | defmodule SymphonyElixir.AgentRunner
method run (line 13) | def run(issue, codex_update_recipient \\ nil, opts \\ []) do
method run_on_worker_host (line 29) | defp run_on_worker_host(issue, codex_update_recipient, opts, worker_ho...
method codex_message_handler (line 49) | defp codex_message_handler(recipient, issue) do
method send_codex_update (line 61) | defp send_codex_update(_recipient, _issue, _message), do: :ok
method send_worker_runtime_info (line 77) | defp send_worker_runtime_info(_recipient, _issue, _worker_host, _works...
method run_codex_turns (line 79) | defp run_codex_turns(workspace, issue, codex_update_recipient, opts, w...
method do_run_codex_turns (line 92) | defp do_run_codex_turns(app_session, workspace, issue, codex_update_re...
method build_turn_prompt (line 133) | defp build_turn_prompt(issue, opts, 1, _max_turns), do: PromptBuilder....
method build_turn_prompt (line 135) | defp build_turn_prompt(_issue, _opts, turn_number, max_turns) do
method continue_with_issue? (line 164) | defp continue_with_issue?(issue, _issue_state_fetcher), do: {:done, is...
method active_issue_state? (line 173) | defp active_issue_state?(_state_name), do: false
method selected_worker_host (line 175) | defp selected_worker_host(nil, []), do: nil
method worker_host_for_log (line 191) | defp worker_host_for_log(nil), do: "local"
method worker_host_for_log (line 192) | defp worker_host_for_log(worker_host), do: worker_host
method issue_context (line 200) | defp issue_context(%Issue{id: issue_id, identifier: identifier}) do
FILE: elixir/lib/symphony_elixir/cli.ex
class SymphonyElixir.CLI (line 1) | defmodule SymphonyElixir.CLI
method main (line 21) | def main(args) do
method evaluate (line 33) | def evaluate(args, deps \\ runtime_deps()) do
method run (line 55) | def run(workflow_path, deps) do
method usage_message (line 74) | defp usage_message do
method runtime_deps (line 79) | defp runtime_deps do
method maybe_set_logs_root (line 89) | defp maybe_set_logs_root(opts, deps) do
method require_guardrails_acknowledgement (line 105) | defp require_guardrails_acknowledgement(opts) do
method acknowledgement_banner (line 114) | defp acknowledgement_banner do
method set_logs_root (line 146) | defp set_logs_root(logs_root) do
method maybe_set_server_port (line 151) | defp maybe_set_server_port(opts, deps) do
method wait_for_shutdown (line 173) | defp wait_for_shutdown do
FILE: elixir/lib/symphony_elixir/codex/app_server.ex
class SymphonyElixir.Codex.AppServer (line 1) | defmodule SymphonyElixir.Codex.AppServer
method run (line 29) | def run(workspace, prompt, issue, opts \\ []) do
method start_session (line 40) | def start_session(workspace, opts \\ []) do
method run_turn (line 70) | def run_turn(
method start_port (line 189) | defp start_port(workspace, nil) do
method send_initialize (line 241) | defp send_initialize(port) do
method session_policies (line 265) | defp session_policies(workspace, nil) do
method do_start_session (line 273) | defp do_start_session(port, workspace, session_policies) do
method start_thread (line 280) | defp start_thread(port, workspace, %{approval_policy: approval_policy,...
method start_turn (line 304) | defp start_turn(port, thread_id, prompt, issue, workspace, approval_po...
method await_turn_completion (line 329) | defp await_turn_completion(port, on_message, tool_executor, auto_appro...
method receive_loop (line 340) | defp receive_loop(port, on_message, timeout_ms, pending_line, tool_exe...
method handle_incoming (line 364) | defp handle_incoming(port, on_message, data, timeout_ms, tool_executor...
method emit_turn_event (line 441) | defp emit_turn_event(on_message, event, payload, payload_string, port,...
method handle_turn_method (line 454) | defp handle_turn_method(
method maybe_handle_approval_request (line 526) | defp maybe_handle_approval_request(
method maybe_handle_approval_request (line 548) | defp maybe_handle_approval_request(
method maybe_handle_approval_request (line 583) | defp maybe_handle_approval_request(
method maybe_handle_approval_request (line 605) | defp maybe_handle_approval_request(
method maybe_handle_approval_request (line 627) | defp maybe_handle_approval_request(
method maybe_handle_approval_request (line 649) | defp maybe_handle_approval_request(
method maybe_handle_approval_request (line 671) | defp maybe_handle_approval_request(
method normalize_dynamic_tool_result (line 702) | defp normalize_dynamic_tool_result(result) do
method dynamic_tool_output (line 711) | defp dynamic_tool_output(result), do: Jason.encode!(result, pretty: true)
method approve_or_require (line 722) | defp approve_or_require(
method approve_or_require (line 744) | defp approve_or_require(
method maybe_auto_answer_tool_request_user_input (line 757) | defp maybe_auto_answer_tool_request_user_input(
method maybe_auto_answer_tool_request_user_input (line 793) | defp maybe_auto_answer_tool_request_user_input(
method tool_request_user_input_approval_answers (line 833) | defp tool_request_user_input_approval_answers(_params), do: :error
method reply_with_non_interactive_tool_input_answer (line 835) | defp reply_with_non_interactive_tool_input_answer(
method tool_request_user_input_unavailable_answers (line 881) | defp tool_request_user_input_unavailable_answers(_params), do: :error
method tool_request_user_input_question_id (line 886) | defp tool_request_user_input_question_id(_question), do: :error
method tool_request_user_input_approval_answer (line 896) | defp tool_request_user_input_approval_answer(_question), do: :error
method tool_request_user_input_approval_option_label (line 898) | defp tool_request_user_input_approval_option_label(options) do
method tool_request_user_input_option_label (line 911) | defp tool_request_user_input_option_label(_option), do: nil
method await_response (line 922) | defp await_response(port, request_id) do
method with_timeout_response (line 926) | defp with_timeout_response(port, request_id, timeout_ms, pending_line) do
method handle_response (line 943) | defp handle_response(port, request_id, data, timeout_ms) do
method log_non_json_stream_line (line 966) | defp log_non_json_stream_line(data, stream_label) do
method protocol_message_candidate? (line 982) | defp protocol_message_candidate?(data) do
method issue_context (line 989) | defp issue_context(%{id: issue_id, identifier: identifier}) do
method metadata_from_message (line 1014) | defp metadata_from_message(port, payload) do
method maybe_set_usage (line 1028) | defp maybe_set_usage(metadata, _payload), do: metadata
method default_on_message (line 1034) | defp default_on_message(_message), do: :ok
method tool_call_name (line 1049) | defp tool_call_name(_params), do: nil
method tool_call_arguments (line 1055) | defp tool_call_arguments(_params), do: %{}
method send_message (line 1057) | defp send_message(port, message) do
method needs_input? (line 1067) | defp needs_input?(_method, _payload), do: false
method request_payload_requires_input? (line 1081) | defp request_payload_requires_input?(payload) do
method needs_input_field? (line 1095) | defp needs_input_field?(_payload), do: false
FILE: elixir/lib/symphony_elixir/codex/dynamic_tool.ex
class SymphonyElixir.Codex.DynamicTool (line 1) | defmodule SymphonyElixir.Codex.DynamicTool
method execute (line 30) | def execute(tool, arguments, opts \\ []) do
method tool_specs (line 46) | def tool_specs do
method execute_linear_graphql (line 56) | defp execute_linear_graphql(arguments, opts) do
method normalize_linear_graphql_arguments (line 91) | defp normalize_linear_graphql_arguments(_arguments), do: {:error, :inv...
method normalize_query (line 93) | defp normalize_query(arguments) do
method normalize_variables (line 106) | defp normalize_variables(arguments) do
method graphql_response (line 113) | defp graphql_response(response) do
method failure_response (line 124) | defp failure_response(payload) do
method encode_payload (line 145) | defp encode_payload(payload), do: inspect(payload)
method tool_error_payload (line 147) | defp tool_error_payload(:missing_query) do
method tool_error_payload (line 155) | defp tool_error_payload(:invalid_arguments) do
method tool_error_payload (line 163) | defp tool_error_payload(:invalid_variables) do
method tool_error_payload (line 171) | defp tool_error_payload(:missing_linear_api_token) do
method tool_error_payload (line 179) | defp tool_error_payload({:linear_api_status, status}) do
method tool_error_payload (line 188) | defp tool_error_payload({:linear_api_request, reason}) do
method tool_error_payload (line 197) | defp tool_error_payload(reason) do
method supported_tool_names (line 206) | defp supported_tool_names do
FILE: elixir/lib/symphony_elixir/config.ex
class SymphonyElixir.Config (line 1) | defmodule SymphonyElixir.Config
method settings (line 30) | def settings do
method settings! (line 41) | def settings! do
method max_concurrent_agents_for_state (line 62) | def max_concurrent_agents_for_state(_state_name), do: settings!().agen...
method codex_turn_sandbox_policy (line 65) | def codex_turn_sandbox_policy(workspace \\ nil) do
method workflow_prompt (line 76) | def workflow_prompt do
method server_port (line 87) | def server_port do
method validate! (line 95) | def validate! do
method codex_runtime_settings (line 103) | def codex_runtime_settings(workspace \\ nil, opts \\ []) do
method validate_semantics (line 117) | defp validate_semantics(settings) do
method format_config_error (line 136) | defp format_config_error(reason) do
FILE: elixir/lib/symphony_elixir/config/schema.ex
class SymphonyElixir.Config.Schema (line 1) | defmodule SymphonyElixir.Config.Schema
method resolve_turn_sandbox_policy (line 293) | def resolve_turn_sandbox_policy(settings, workspace \\ nil) do
method resolve_runtime_turn_sandbox_policy (line 308) | def resolve_runtime_turn_sandbox_policy(settings, workspace \\ nil, op...
method normalize_state_limits (line 327) | def normalize_state_limits(nil), do: %{}
method validate_state_limits (line 337) | def validate_state_limits(changeset, field) do
method changeset (line 354) | defp changeset(attrs) do
method finalize_settings (line 368) | defp finalize_settings(settings) do
method normalize_keys (line 396) | defp normalize_keys(value), do: value
method normalize_optional_map (line 398) | defp normalize_optional_map(nil), do: nil
method normalize_key (line 402) | defp normalize_key(value), do: to_string(value)
method drop_nil_values (line 414) | defp drop_nil_values(value), do: value
method resolve_secret_setting (line 416) | defp resolve_secret_setting(nil, fallback), do: normalize_secret_value...
method env_reference_name (line 459) | defp env_reference_name("$" <> env_name) do
method env_reference_name (line 467) | defp env_reference_name(_value), do: :error
method resolve_env_token (line 469) | defp resolve_env_token(env_name) do
method normalize_secret_value (line 480) | defp normalize_secret_value(_value), do: nil
method default_turn_sandbox_policy (line 482) | defp default_turn_sandbox_policy(workspace) do
method default_runtime_turn_sandbox_policy (line 504) | defp default_runtime_turn_sandbox_policy(workspace_root, _opts) do
method default_workspace_root (line 511) | defp default_workspace_root(nil, fallback), do: fallback
method default_workspace_root (line 512) | defp default_workspace_root("", fallback), do: fallback
method default_workspace_root (line 513) | defp default_workspace_root(workspace, _fallback), do: workspace
method expand_local_workspace_root (line 520) | defp expand_local_workspace_root(_workspace_root) do
method format_errors (line 524) | defp format_errors(changeset) do
method flatten_errors (line 531) | defp flatten_errors(errors, prefix \\ nil)
method translate_error (line 549) | defp translate_error({message, options}) do
method error_value_to_string (line 556) | defp error_value_to_string(value), do: inspect(value)
class StringOrMap (line 14) | defmodule StringOrMap
method type (line 19) | def type, do: :map
method embed_as (line 22) | def embed_as(_format), do: :self
method equal? (line 25) | def equal?(left, right), do: left == right
method cast (line 29) | def cast(_value), do: :error
method load (line 33) | def load(_value), do: :error
method dump (line 37) | def dump(_value), do: :error
class Tracker (line 40) | defmodule Tracker
method changeset (line 58) | def changeset(schema, attrs) do
class Polling (line 68) | defmodule Polling
method changeset (line 79) | def changeset(schema, attrs) do
class Workspace (line 86) | defmodule Workspace
method changeset (line 97) | def changeset(schema, attrs) do
class Worker (line 103) | defmodule Worker
method changeset (line 115) | def changeset(schema, attrs) do
class Agent (line 122) | defmodule Agent
method changeset (line 138) | def changeset(schema, attrs) do
class Codex (line 153) | defmodule Codex
method changeset (line 180) | def changeset(schema, attrs) do
class Hooks (line 202) | defmodule Hooks
method changeset (line 217) | def changeset(schema, attrs) do
class Observability (line 224) | defmodule Observability
method changeset (line 237) | def changeset(schema, attrs) do
class Server (line 245) | defmodule Server
method changeset (line 257) | def changeset(schema, attrs) do
FILE: elixir/lib/symphony_elixir/http_server.ex
class SymphonyElixir.HttpServer (line 1) | defmodule SymphonyElixir.HttpServer
method child_spec (line 12) | def child_spec(opts) do
method start_link (line 20) | def start_link(opts \\ []) do
method bound_port (line 52) | def bound_port(_server \\ __MODULE__) do
method parse_host (line 63) | defp parse_host({_, _, _, _} = ip), do: {:ok, ip}
method parse_host (line 64) | defp parse_host({_, _, _, _, _, _, _, _} = ip), do: {:ok, ip}
method normalize_host (line 83) | defp normalize_host(host), do: to_string(host)
method secret_key_base (line 85) | defp secret_key_base do
FILE: elixir/lib/symphony_elixir/linear/adapter.ex
class SymphonyElixir.Linear.Adapter (line 1) | defmodule SymphonyElixir.Linear.Adapter
method fetch_candidate_issues (line 41) | def fetch_candidate_issues, do: client_module().fetch_candidate_issues()
method fetch_issues_by_states (line 44) | def fetch_issues_by_states(states), do: client_module().fetch_issues_b...
method fetch_issue_states_by_ids (line 47) | def fetch_issue_states_by_ids(issue_ids), do: client_module().fetch_is...
method client_module (line 76) | defp client_module do
method resolve_state_id (line 80) | defp resolve_state_id(issue_id, state_name) do
FILE: elixir/lib/symphony_elixir/linear/client.ex
class SymphonyElixir.Linear.Client (line 1) | defmodule SymphonyElixir.Linear.Client
method fetch_candidate_issues (line 107) | def fetch_candidate_issues do
method do_fetch_by_states (line 239) | defp do_fetch_by_states(project_slug, state_names, assignee_filter) do
method do_fetch_by_states_page (line 243) | defp do_fetch_by_states_page(project_slug, state_names, assignee_filte...
method do_fetch_issue_states (line 274) | defp do_fetch_issue_states(ids, assignee_filter) do
method do_fetch_issue_states_page (line 284) | defp do_fetch_issue_states_page([], _assignee_filter, _graphql_fun, ac...
method do_fetch_issue_states_page (line 291) | defp do_fetch_issue_states_page(ids, assignee_filter, graphql_fun, acc...
method build_graphql_payload (line 326) | defp build_graphql_payload(query, variables, operation_name) do
method maybe_put_operation_name (line 344) | defp maybe_put_operation_name(payload, _operation_name), do: payload
method summarize_error_body (line 369) | defp summarize_error_body(body) do
method graphql_headers (line 383) | defp graphql_headers do
method post_graphql_request (line 397) | defp post_graphql_request(payload, headers) do
method decode_linear_response (line 405) | defp decode_linear_response(%{"data" => %{"issues" => %{"nodes" => nod...
method decode_linear_response (line 414) | defp decode_linear_response(%{"errors" => errors}, _assignee_filter) do
method decode_linear_response (line 418) | defp decode_linear_response(_unknown, _assignee_filter) do
method decode_linear_page_response (line 422) | defp decode_linear_page_response(
method decode_linear_page_response (line 438) | defp decode_linear_page_response(response, assignee_filter), do: decod...
method next_page_cursor (line 445) | defp next_page_cursor(%{has_next_page: true}), do: {:error, :linear_mi...
method next_page_cursor (line 446) | defp next_page_cursor(_), do: :done
method normalize_issue (line 469) | defp normalize_issue(_issue, _assignee_filter), do: nil
method assignee_field (line 472) | defp assignee_field(_assignee, _field), do: nil
method assigned_to_worker? (line 474) | defp assigned_to_worker?(_assignee, nil), do: true
method assigned_to_worker? (line 486) | defp assigned_to_worker?(_assignee, _assignee_filter), do: false
method assignee_id (line 488) | defp assignee_id(%{} = assignee), do: normalize_assignee_match_value(a...
method routing_assignee_filter (line 490) | defp routing_assignee_filter do
method resolve_viewer_assignee_filter (line 513) | defp resolve_viewer_assignee_filter do
method normalize_assignee_match_value (line 539) | defp normalize_assignee_match_value(_value), do: nil
method extract_labels (line 548) | defp extract_labels(_), do: []
method extract_blockers (line 573) | defp extract_blockers(_), do: []
method parse_datetime (line 575) | defp parse_datetime(nil), do: nil
method parse_datetime (line 577) | defp parse_datetime(raw) do
method parse_priority (line 585) | defp parse_priority(_priority), do: nil
FILE: elixir/lib/symphony_elixir/linear/issue.ex
class SymphonyElixir.Linear.Issue (line 1) | defmodule SymphonyElixir.Linear.Issue
method label_names (line 40) | def label_names(%__MODULE__{labels: labels}) do
FILE: elixir/lib/symphony_elixir/log_file.ex
class SymphonyElixir.LogFile (line 1) | defmodule SymphonyElixir.LogFile
method default_log_file (line 14) | def default_log_file do
method configure (line 24) | def configure do
method setup_disk_handler (line 32) | defp setup_disk_handler(log_file, max_bytes, max_files) do
method remove_existing_handler (line 52) | defp remove_existing_handler do
method remove_default_console_handler (line 60) | defp remove_default_console_handler do
method disk_log_handler_config (line 68) | defp disk_log_handler_config(path, max_bytes, max_files) do
FILE: elixir/lib/symphony_elixir/orchestrator.ex
class SymphonyElixir.Orchestrator (line 1) | defmodule SymphonyElixir.Orchestrator
method start_link (line 46) | def start_link(opts \\ []) do
method init (line 52) | def init(_opts) do
method handle_info (line 91) | def handle_info({:tick, _tick_token}, state), do: {:noreply, state}
method handle_info (line 93) | def handle_info(:tick, state) do
method handle_info (line 109) | def handle_info(:run_poll_cycle, state) do
method handle_info (line 119) | def handle_info(
method handle_info (line 183) | def handle_info(
method handle_info (line 204) | def handle_info({:codex_worker_update, _issue_id, _update}, state), do...
method handle_info (line 206) | def handle_info({:retry_issue, issue_id, retry_token}, state) do
method handle_info (line 217) | def handle_info({:retry_issue, _issue_id}, state), do: {:noreply, state}
method handle_info (line 219) | def handle_info(msg, state) do
method maybe_dispatch (line 224) | defp maybe_dispatch(%State{} = state) do
method reconcile_running_issues (line 275) | defp reconcile_running_issues(%State{} = state) do
method should_dispatch_issue_for_test (line 312) | def should_dispatch_issue_for_test(%Issue{} = issue, %State{} = state) do
method select_worker_host_for_test (line 332) | def select_worker_host_for_test(%State{} = state, preferred_worker_hos...
method reconcile_running_issue_states (line 336) | defp reconcile_running_issue_states([], state, _active_states, _termin...
method reconcile_running_issue_states (line 338) | defp reconcile_running_issue_states([issue | rest], state, active_stat...
method reconcile_issue_state (line 347) | defp reconcile_issue_state(%Issue{} = issue, state, active_states, ter...
method reconcile_issue_state (line 369) | defp reconcile_issue_state(_issue, state, _active_states, _terminal_st...
method reconcile_missing_running_issue_ids (line 391) | defp reconcile_missing_running_issue_ids(state, _requested_issue_ids, ...
method log_missing_running_issue (line 403) | defp log_missing_running_issue(_state, _issue_id), do: :ok
method refresh_running_issue_state (line 405) | defp refresh_running_issue_state(%State{} = state, %Issue{} = issue) do
method terminate_running_issue (line 415) | defp terminate_running_issue(%State{} = state, issue_id, cleanup_works...
method reconcile_stalled_running_issues (line 448) | defp reconcile_stalled_running_issues(%State{} = state) do
method restart_stalled_issue (line 467) | defp restart_stalled_issue(state, issue_id, running_entry, now, timeou...
method stall_elapsed_ms (line 489) | defp stall_elapsed_ms(running_entry, now) do
method last_activity_timestamp (line 505) | defp last_activity_timestamp(_running_entry), do: nil
method terminate_task (line 517) | defp terminate_task(_pid), do: :ok
method choose_issues (line 519) | defp choose_issues(issues, state) do
method priority_rank (line 545) | defp priority_rank(_priority), do: 5
method issue_created_at_sort_key (line 547) | defp issue_created_at_sort_key(%Issue{created_at: %DateTime{} = create...
method issue_created_at_sort_key (line 551) | defp issue_created_at_sort_key(%Issue{}), do: 9_223_372_036_854_775_807
method issue_created_at_sort_key (line 552) | defp issue_created_at_sort_key(_issue), do: 9_223_372_036_854_775_807
method should_dispatch_issue? (line 554) | defp should_dispatch_issue?(
method should_dispatch_issue? (line 569) | defp should_dispatch_issue?(_issue, _state, _active_states, _terminal_...
method state_slots_available? (line 577) | defp state_slots_available?(_issue, _running), do: false
method candidate_issue? (line 607) | defp candidate_issue?(_issue, _active_states, _terminal_states), do: f...
method issue_routable_to_worker? (line 613) | defp issue_routable_to_worker?(_issue), do: true
method todo_issue_blocked_by_non_terminal? (line 630) | defp todo_issue_blocked_by_non_terminal?(_issue, _terminal_states), do...
method terminal_issue_state? (line 636) | defp terminal_issue_state?(_state_name, _terminal_states), do: false
method terminal_state_set (line 646) | defp terminal_state_set do
method active_state_set (line 653) | defp active_state_set do
method dispatch_issue (line 660) | defp dispatch_issue(%State{} = state, issue, attempt \\ nil, preferred...
method do_dispatch_issue (line 680) | defp do_dispatch_issue(%State{} = state, issue, attempt, preferred_wor...
method spawn_issue_on_worker_host (line 693) | defp spawn_issue_on_worker_host(%State{} = state, issue, attempt, reci...
method revalidate_issue_for_dispatch (line 763) | defp revalidate_issue_for_dispatch(issue, _issue_fetcher, _terminal_st...
method complete_issue (line 765) | defp complete_issue(%State{} = state, issue_id) do
method handle_retry_issue (line 829) | defp handle_retry_issue(%State{} = state, issue_id, attempt, metadata) do
method handle_retry_issue_lookup (line 849) | defp handle_retry_issue_lookup(%Issue{} = issue, state, issue_id, atte...
method handle_retry_issue_lookup (line 869) | defp handle_retry_issue_lookup(nil, state, issue_id, _attempt, _metada...
method cleanup_issue_workspace (line 874) | defp cleanup_issue_workspace(identifier, worker_host \\ nil)
method cleanup_issue_workspace (line 880) | defp cleanup_issue_workspace(_identifier, _worker_host), do: :ok
method run_terminal_workspace_cleanup (line 882) | defp run_terminal_workspace_cleanup do
method notify_dashboard (line 899) | defp notify_dashboard do
method handle_active_retry (line 903) | defp handle_active_retry(state, issue, attempt, metadata) do
method release_issue_claim (line 924) | defp release_issue_claim(%State{} = state, issue_id) do
method failure_retry_delay (line 936) | defp failure_retry_delay(attempt) do
method normalize_retry_attempt (line 942) | defp normalize_retry_attempt(_attempt), do: 0
method next_retry_attempt_from_running (line 944) | defp next_retry_attempt_from_running(running_entry) do
method pick_retry_identifier (line 951) | defp pick_retry_identifier(issue_id, previous_retry, metadata) do
method pick_retry_error (line 955) | defp pick_retry_error(previous_retry, metadata) do
method pick_retry_worker_host (line 959) | defp pick_retry_worker_host(previous_retry, metadata) do
method pick_retry_workspace_path (line 963) | defp pick_retry_workspace_path(previous_retry, metadata) do
method maybe_put_runtime_value (line 967) | defp maybe_put_runtime_value(running_entry, _key, nil), do: running_entry
method select_worker_host (line 973) | defp select_worker_host(%State{} = state, preferred_worker_host) do
method preferred_worker_host_available? (line 999) | defp preferred_worker_host_available?(_preferred_worker_host, _hosts),...
method worker_slots_available? (line 1017) | defp worker_slots_available?(%State{} = state) do
method worker_slots_available? (line 1021) | defp worker_slots_available?(%State{} = state, preferred_worker_host) do
method find_issue_id_for_ref (line 1045) | defp find_issue_id_for_ref(running, ref) do
method running_entry_session_id (line 1055) | defp running_entry_session_id(_running_entry), do: "n/a"
method issue_context (line 1057) | defp issue_context(%Issue{id: issue_id, identifier: identifier}) do
method available_slots (line 1061) | defp available_slots(%State{} = state) do
method request_refresh (line 1070) | def request_refresh do
method request_refresh (line 1075) | def request_refresh(server) do
method snapshot (line 1084) | def snapshot, do: snapshot(__MODULE__, 15_000)
method snapshot (line 1087) | def snapshot(server, timeout) do
method handle_call (line 1101) | def handle_call(:snapshot, _from, state) do
method handle_call (line 1157) | def handle_call(:request_refresh, _from, state) do
method integrate_codex_update (line 1172) | defp integrate_codex_update(running_entry, %{event: event, timestamp: ...
method codex_app_server_pid_for_update (line 1213) | defp codex_app_server_pid_for_update(existing, _update), do: existing
method session_id_for_update (line 1218) | defp session_id_for_update(existing, _update), do: existing
method turn_count_for_update (line 1236) | defp turn_count_for_update(_existing_count, _existing_session_id, _upd...
method summarize_codex_update (line 1238) | defp summarize_codex_update(update) do
method schedule_poll_cycle_start (line 1262) | defp schedule_poll_cycle_start do
method next_poll_in_ms (line 1267) | defp next_poll_in_ms(nil, _now_ms), do: nil
method pop_running_entry (line 1273) | defp pop_running_entry(state, issue_id) do
method record_session_completion_totals (line 1294) | defp record_session_completion_totals(state, _running_entry), do: state
method refresh_runtime_config (line 1296) | defp refresh_runtime_config(%State{} = state) do
method retry_candidate_issue? (line 1306) | defp retry_candidate_issue?(%Issue{} = issue, terminal_states) do
method dispatch_slots_available? (line 1311) | defp dispatch_slots_available?(%Issue{} = issue, %State{} = state) do
method apply_codex_token_delta (line 1323) | defp apply_codex_token_delta(state, _token_delta), do: state
method apply_codex_rate_limits (line 1335) | defp apply_codex_rate_limits(state, _update), do: state
method apply_token_delta (line 1337) | defp apply_token_delta(codex_totals, token_delta) do
method extract_token_delta (line 1353) | defp extract_token_delta(running_entry, %{event: _, timestamp: _} = up...
method compute_token_delta (line 1390) | defp compute_token_delta(running_entry, token_key, usage, reported_key...
method extract_token_usage (line 1407) | defp extract_token_usage(update) do
method extract_rate_limits (line 1422) | defp extract_rate_limits(update) do
method absolute_token_usage_from_payload (line 1446) | defp absolute_token_usage_from_payload(_payload), do: nil
method turn_completed_usage_from_payload (line 1462) | defp turn_completed_usage_from_payload(_payload), do: nil
method rate_limits_from_payload (line 1483) | defp rate_limits_from_payload(_payload), do: nil
method rate_limits_map? (line 1529) | defp rate_limits_map?(_payload), do: false
method explicit_map_at_paths (line 1539) | defp explicit_map_at_paths(_payload, _paths), do: nil
method map_at_path (line 1551) | defp map_at_path(_payload, _path), do: nil
method integer_token_map? (line 1553) | defp integer_token_map?(payload) do
method get_token_usage (line 1584) | defp get_token_usage(usage, :input),
method get_token_usage (line 1598) | defp get_token_usage(usage, :output),
method get_token_usage (line 1613) | defp get_token_usage(usage, :total),
method payload_get (line 1628) | defp payload_get(payload, field), do: map_integer_value(payload, field)
method map_integer_value (line 1630) | defp map_integer_value(payload, field) do
method running_seconds (line 1639) | defp running_seconds(%DateTime{} = started_at, %DateTime{} = now) do
method running_seconds (line 1643) | defp running_seconds(_started_at, _now), do: 0
method integer_like (line 1654) | defp integer_like(_value), do: nil
class State (line 24) | defmodule State
FILE: elixir/lib/symphony_elixir/path_safety.ex
class SymphonyElixir.PathSafety (line 1) | defmodule SymphonyElixir.PathSafety
method resolve_segments (line 23) | defp resolve_segments(root, resolved_segments, []), do: {:ok, join_pat...
method resolve_segments (line 25) | defp resolve_segments(root, resolved_segments, [segment | rest]) do
FILE: elixir/lib/symphony_elixir/prompt_builder.ex
class SymphonyElixir.PromptBuilder (line 1) | defmodule SymphonyElixir.PromptBuilder
method build_prompt (line 11) | def build_prompt(issue, opts \\ []) do
method prompt_template! (line 28) | defp prompt_template!({:ok, %{prompt_template: prompt}}), do: default_...
method prompt_template! (line 30) | defp prompt_template!({:error, reason}) do
method to_solid_value (line 48) | defp to_solid_value(%DateTime{} = value), do: DateTime.to_iso8601(value)
method to_solid_value (line 49) | defp to_solid_value(%NaiveDateTime{} = value), do: NaiveDateTime.to_is...
method to_solid_value (line 50) | defp to_solid_value(%Date{} = value), do: Date.to_iso8601(value)
method to_solid_value (line 51) | defp to_solid_value(%Time{} = value), do: Time.to_iso8601(value)
method to_solid_value (line 52) | defp to_solid_value(%_{} = value), do: value |> Map.from_struct() |> t...
method to_solid_value (line 55) | defp to_solid_value(value), do: value
FILE: elixir/lib/symphony_elixir/specs_check.ex
class SymphonyElixir.SpecsCheck (line 1) | defmodule SymphonyElixir.SpecsCheck
method missing_public_specs (line 13) | def missing_public_specs(paths, opts \\ []) do
method finding_identifier (line 26) | def finding_identifier(%{module: module, name: name, arity: arity}) do
method collect_elixir_files (line 30) | defp collect_elixir_files(path) do
method file_findings (line 43) | defp file_findings(file, exemptions) do
method module_nodes (line 60) | defp module_nodes(ast) do
method find_missing_specs (line 73) | defp find_missing_specs(body, module_name, file, exemptions) do
method initial_state (line 82) | defp initial_state do
method consume_form (line 86) | defp consume_form({:@, _, [{:spec, _, spec_nodes}]}, state, _module_na...
method consume_form (line 95) | defp consume_form({:@, _, [{:impl, _, _}]}, state, _module_name, _file...
method consume_form (line 99) | defp consume_form({:@, _, _}, state, _module_name, _file, _exemptions)...
method consume_form (line 101) | defp consume_form({:def, meta, [head_ast, _]} = _form, state, module_n...
method consume_form (line 132) | defp consume_form({:defp, _, _}, state, _module_name, _file, _exemptio...
method consume_form (line 136) | defp consume_form(_form, state, _module_name, _file, _exemptions) do
method compliant? (line 140) | defp compliant?(finding, state, exemptions) do
method normalize_block (line 148) | defp normalize_block({:__block__, _, forms}), do: forms
method normalize_block (line 149) | defp normalize_block(form), do: [form]
method extract_spec_identifiers (line 151) | defp extract_spec_identifiers({:"::", _, [head, _return_type]}) do
method extract_spec_identifiers (line 158) | defp extract_spec_identifiers({:when, _, [{:"::", _, [head, _return_ty...
method extract_spec_identifiers (line 165) | defp extract_spec_identifiers(_), do: []
method spec_head_to_identifier (line 167) | defp spec_head_to_identifier({:when, _, [inner | _guards]}), do: spec_...
method spec_head_to_identifier (line 170) | defp spec_head_to_identifier(_), do: nil
method def_head_to_identifier (line 172) | defp def_head_to_identifier({:when, _, [head | _guards]}), do: def_hea...
FILE: elixir/lib/symphony_elixir/ssh.ex
class SymphonyElixir.SSH (line 1) | defmodule SymphonyElixir.SSH
method ssh_executable (line 34) | defp ssh_executable do
method ssh_args (line 41) | defp ssh_args(host, command) do
method maybe_put_line_option (line 51) | defp maybe_put_line_option(port_opts, nil), do: port_opts
method maybe_put_line_option (line 52) | defp maybe_put_line_option(port_opts, line_bytes), do: Keyword.put(por...
method maybe_put_config (line 54) | defp maybe_put_config(args) do
method maybe_put_port (line 64) | defp maybe_put_port(args, nil), do: args
method maybe_put_port (line 65) | defp maybe_put_port(args, port), do: args ++ ["-p", port]
FILE: elixir/lib/symphony_elixir/status_dashboard.ex
class SymphonyElixir.StatusDashboard (line 1) | defmodule SymphonyElixir.StatusDashboard
method start_link (line 78) | def start_link(opts \\ []) do
method notify_update (line 84) | def notify_update(server \\ __MODULE__) do
method init (line 98) | def init(opts) do
method render_offline_status (line 130) | def render_offline_status do
method handle_info (line 148) | def handle_info(:tick, %{enabled: true} = state) do
method handle_info (line 155) | def handle_info(:refresh, %{enabled: true} = state), do: {:noreply, ma...
method handle_info (line 156) | def handle_info(:refresh, state), do: {:noreply, state}
method handle_info (line 158) | def handle_info({:flush_render, timer_ref}, %{enabled: true, flush_tim...
method handle_info (line 176) | def handle_info({:flush_render, _timer_ref}, state), do: {:noreply, st...
method handle_info (line 177) | def handle_info(:tick, state), do: {:noreply, state}
method refresh_runtime_config (line 179) | defp refresh_runtime_config(%__MODULE__{} = state) do
method schedule_tick (line 190) | defp schedule_tick(refresh_ms, true), do: Process.send_after(self(), :...
method schedule_tick (line 191) | defp schedule_tick(_refresh_ms, false), do: :ok
method maybe_render (line 193) | defp maybe_render(state) do
method maybe_enqueue_render (line 233) | defp maybe_enqueue_render(state, content, now_ms) do
method maybe_update_snapshot_fingerprint (line 246) | defp maybe_update_snapshot_fingerprint(state, snapshot_data) do
method periodic_rerender_due? (line 254) | defp periodic_rerender_due?(%{last_rendered_at_ms: nil}, _now_ms), do:...
method periodic_rerender_due? (line 261) | defp periodic_rerender_due?(_state, _now_ms), do: false
method render_now? (line 263) | defp render_now?(%{last_rendered_at_ms: nil, flush_timer_ref: nil}, _n...
method render_now? (line 270) | defp render_now?(_state, _now_ms), do: false
method schedule_flush_render (line 275) | defp schedule_flush_render(state, now_ms) do
method flush_delay_ms (line 282) | defp flush_delay_ms(%{last_rendered_at_ms: nil}, _now_ms), do: 1
method flush_delay_ms (line 284) | defp flush_delay_ms(
method render_content (line 292) | defp render_content(state, content, now_ms) do
method snapshot_with_samples (line 308) | defp snapshot_with_samples(token_samples, now_ms) do
method format_snapshot_content (line 333) | defp format_snapshot_content(snapshot_data, tps, terminal_columns_over...
method format_project_link_lines (line 395) | defp format_project_link_lines do
method format_project_refresh_line (line 416) | defp format_project_refresh_line(%{checking?: true}) do
method format_project_refresh_line (line 426) | defp format_project_refresh_line(_) do
method linear_project_url (line 430) | defp linear_project_url(project_slug), do: "https://linear.app/project...
method dashboard_url (line 432) | defp dashboard_url do
method dashboard_url (line 436) | defp dashboard_url(_host, nil, _bound_port), do: nil
method dashboard_url (line 438) | defp dashboard_url(host, configured_port, bound_port) do
method render_to_terminal (line 468) | defp render_to_terminal(content) do
method update_token_samples (line 477) | defp update_token_samples(samples, now_ms, total_tokens) do
method prune_samples (line 481) | defp prune_samples(samples, now_ms) do
method prune_graph_samples (line 486) | defp prune_graph_samples(samples, now_ms) do
method rolling_tps (line 493) | def rolling_tps(samples, now_ms, current_tokens) do
method throttled_tps (line 521) | def throttled_tps(last_second, last_value, now_ms, token_samples, curr...
method format_timestamp_for_test (line 533) | def format_timestamp_for_test(%DateTime{} = datetime), do: format_time...
method format_snapshot_content_for_test (line 537) | def format_snapshot_content_for_test(snapshot_data, tps), do: format_s...
method format_snapshot_content_for_test (line 541) | def format_snapshot_content_for_test(snapshot_data, tps, terminal_colu...
method dashboard_url_for_test (line 547) | def dashboard_url_for_test(host, configured_port, bound_port),
method snapshot_payload (line 550) | defp snapshot_payload do
method format_running_rows (line 576) | defp format_running_rows(running, running_event_width) do
method format_running_summary (line 590) | defp format_running_summary(running_entry, running_event_width) do
method format_running_summary_for_test (line 637) | def format_running_summary_for_test(running_entry, terminal_columns \\...
method format_tps_for_test (line 642) | def format_tps_for_test(value), do: format_tps(value)
method tps_graph_for_test (line 646) | def tps_graph_for_test(samples, now_ms, current_tokens), do: tps_graph...
method format_retry_rows (line 648) | defp format_retry_rows(retrying) do
method format_retry_summary (line 659) | defp format_retry_summary(retry_entry) do
method next_in_words (line 681) | defp next_in_words(_), do: "n/a"
method format_retry_error (line 702) | defp format_retry_error(_), do: ""
method format_runtime_seconds (line 711) | defp format_runtime_seconds(_), do: "0m 0s"
method format_runtime_and_turns (line 717) | defp format_runtime_and_turns(seconds, _turn_count), do: format_runtim...
method format_count (line 719) | defp format_count(nil), do: "0"
method format_count (line 737) | defp format_count(value), do: to_string(value)
method running_table_header_row (line 739) | defp running_table_header_row(running_event_width) do
method running_table_separator_row (line 755) | defp running_table_separator_row(running_event_width) do
method running_event_width (line 768) | defp running_event_width(terminal_columns) do
method fixed_running_width (line 777) | defp fixed_running_width do
method terminal_columns (line 786) | defp terminal_columns do
method terminal_columns_from_env (line 796) | defp terminal_columns_from_env do
method format_cell (line 809) | defp format_cell(value, width, align \\ :left) do
method truncate_plain (line 824) | defp truncate_plain(value, width) do
method compact_session_id (line 832) | defp compact_session_id(nil), do: "n/a"
method compact_session_id (line 835) | defp compact_session_id(session_id) do
method prepend (line 854) | defp prepend("", value), do: value
method prepend (line 855) | defp prepend(prefix, value), do: prefix <> value
method tps_graph (line 864) | defp tps_graph(samples, now_ms, current_tokens) do
method in_bucket? (line 917) | defp in_bucket?(timestamp, bucket_start, bucket_end, true),
method in_bucket? (line 920) | defp in_bucket?(timestamp, bucket_start, bucket_end, false),
method format_rate_limits (line 923) | defp format_rate_limits(nil), do: colorize("unavailable", @ansi_gray)
method format_rate_limits (line 943) | defp format_rate_limits(other) do
method format_rate_limit_bucket (line 950) | defp format_rate_limit_bucket(nil), do: "n/a"
method format_rate_limit_bucket (line 997) | defp format_rate_limit_bucket(other), do: to_string(other)
method format_rate_limit_credits (line 999) | defp format_rate_limit_credits(nil), do: "credits n/a"
method format_rate_limit_credits (line 1021) | defp format_rate_limit_credits(other), do: "credits #{to_string(other)}"
method format_reset_value (line 1025) | defp format_reset_value(value), do: to_string(value)
method map_value (line 1039) | defp map_value(_map, _keys), do: nil
method integer_like? (line 1042) | defp integer_like?(_value), do: false
method status_dot (line 1044) | defp status_dot(color_code) do
method snapshot_total_tokens (line 1052) | defp snapshot_total_tokens(_snapshot_data), do: 0
method format_timestamp (line 1054) | defp format_timestamp(datetime) do
method normalize_status_lines (line 1060) | defp normalize_status_lines(content) do
method closing_border (line 1064) | defp closing_border, do: "╰─"
method colorize (line 1066) | defp colorize(value, code) do
method humanize_codex_message (line 1072) | def humanize_codex_message(nil), do: "no codex message yet"
method humanize_codex_message (line 1074) | def humanize_codex_message(%{event: event, message: message}) do
method humanize_codex_message (line 1081) | def humanize_codex_message(%{message: message}) do
method humanize_codex_message (line 1088) | def humanize_codex_message(message) do
method summarize_message (line 1095) | defp summarize_message(message), do: humanize_codex_message(message)
method humanize_codex_event (line 1097) | defp humanize_codex_event(:session_started, _message, payload) do
method humanize_codex_event (line 1107) | defp humanize_codex_event(:turn_input_required, _message, _payload), d...
method humanize_codex_event (line 1109) | defp humanize_codex_event(:approval_auto_approved, message, payload) do
method humanize_codex_event (line 1127) | defp humanize_codex_event(:tool_input_auto_answered, message, payload) do
method humanize_codex_event (line 1139) | defp humanize_codex_event(:tool_call_completed, _message, payload),
method humanize_codex_event (line 1142) | defp humanize_codex_event(:tool_call_failed, _message, payload),
method humanize_codex_event (line 1145) | defp humanize_codex_event(:unsupported_tool_call, _message, payload),
method humanize_codex_event (line 1148) | defp humanize_codex_event(:turn_ended_with_error, message, _payload), ...
method humanize_codex_event (line 1149) | defp humanize_codex_event(:startup_failed, message, _payload), do: "st...
method humanize_codex_event (line 1150) | defp humanize_codex_event(:turn_failed, _message, payload), do: humani...
method humanize_codex_event (line 1151) | defp humanize_codex_event(:turn_cancelled, _message, _payload), do: "t...
method humanize_codex_event (line 1152) | defp humanize_codex_event(:malformed, _message, _payload), do: "malfor...
method humanize_codex_event (line 1153) | defp humanize_codex_event(_event, _message, _payload), do: nil
method unwrap_codex_message_payload (line 1155) | defp unwrap_codex_message_payload(%{} = message) do
method unwrap_codex_message_payload (line 1164) | defp unwrap_codex_message_payload(message), do: message
method humanize_codex_payload (line 1166) | defp humanize_codex_payload(%{} = payload) do
method humanize_codex_payload (line 1196) | defp humanize_codex_payload(payload) do
method humanize_codex_method (line 1211) | defp humanize_codex_method("thread/started", payload) do
method humanize_codex_method (line 1221) | defp humanize_codex_method("turn/started", payload) do
method humanize_codex_method (line 1231) | defp humanize_codex_method("turn/completed", payload) do
method humanize_codex_method (line 1253) | defp humanize_codex_method("turn/failed", payload) do
method humanize_codex_method (line 1261) | defp humanize_codex_method("turn/cancelled", _payload), do: "turn canc...
method humanize_codex_method (line 1263) | defp humanize_codex_method("turn/diff/updated", payload) do
method humanize_codex_method (line 1277) | defp humanize_codex_method("turn/plan/updated", payload) do
method humanize_codex_method (line 1294) | defp humanize_codex_method("thread/tokenUsage/updated", payload) do
method humanize_codex_method (line 1306) | defp humanize_codex_method("item/started", payload), do: humanize_item...
method humanize_codex_method (line 1307) | defp humanize_codex_method("item/completed", payload), do: humanize_it...
method humanize_codex_method (line 1309) | defp humanize_codex_method("item/agentMessage/delta", payload),
method humanize_codex_method (line 1312) | defp humanize_codex_method("item/plan/delta", payload),
method humanize_codex_method (line 1315) | defp humanize_codex_method("item/reasoning/summaryTextDelta", payload),
method humanize_codex_method (line 1318) | defp humanize_codex_method("item/reasoning/summaryPartAdded", payload),
method humanize_codex_method (line 1321) | defp humanize_codex_method("item/reasoning/textDelta", payload),
method humanize_codex_method (line 1324) | defp humanize_codex_method("item/commandExecution/outputDelta", payload),
method humanize_codex_method (line 1327) | defp humanize_codex_method("item/fileChange/outputDelta", payload),
method humanize_codex_method (line 1330) | defp humanize_codex_method("item/commandExecution/requestApproval", pa...
method humanize_codex_method (line 1340) | defp humanize_codex_method("item/fileChange/requestApproval", payload) do
method humanize_codex_method (line 1350) | defp humanize_codex_method("item/tool/requestUserInput", payload) do
method humanize_codex_method (line 1364) | defp humanize_codex_method("tool/requestUserInput", payload),
method humanize_codex_method (line 1367) | defp humanize_codex_method("account/updated", payload) do
method humanize_codex_method (line 1376) | defp humanize_codex_method("account/rateLimits/updated", payload) do
method humanize_codex_method (line 1384) | defp humanize_codex_method("account/chatgptAuthTokens/refresh", _paylo...
method humanize_codex_method (line 1386) | defp humanize_codex_method("item/tool/call", payload) do
method humanize_codex_method (line 1396) | defp humanize_codex_method(<<"codex/event/", suffix::binary>>, payload...
method humanize_codex_method (line 1400) | defp humanize_codex_method(method, payload) do
method humanize_dynamic_tool_event (line 1412) | defp humanize_dynamic_tool_event(base, payload) do
method dynamic_tool_name (line 1428) | defp dynamic_tool_name(payload) do
method humanize_item_lifecycle (line 1435) | defp humanize_item_lifecycle(state, payload) do
method humanize_codex_wrapper_event (line 1454) | defp humanize_codex_wrapper_event("mcp_startup_update", payload) do
method humanize_codex_wrapper_event (line 1468) | defp humanize_codex_wrapper_event("mcp_startup_complete", _payload), d...
method humanize_codex_wrapper_event (line 1469) | defp humanize_codex_wrapper_event("task_started", _payload), do: "task...
method humanize_codex_wrapper_event (line 1470) | defp humanize_codex_wrapper_event("user_message", _payload), do: "user...
method humanize_codex_wrapper_event (line 1472) | defp humanize_codex_wrapper_event("item_started", payload) do
method humanize_codex_wrapper_event (line 1480) | defp humanize_codex_wrapper_event("item_completed", payload) do
method humanize_codex_wrapper_event (line 1488) | defp humanize_codex_wrapper_event("agent_message_delta", payload),
method humanize_codex_wrapper_event (line 1491) | defp humanize_codex_wrapper_event("agent_message_content_delta", paylo...
method humanize_codex_wrapper_event (line 1494) | defp humanize_codex_wrapper_event("agent_reasoning_delta", payload),
method humanize_codex_wrapper_event (line 1497) | defp humanize_codex_wrapper_event("reasoning_content_delta", payload),
method humanize_codex_wrapper_event (line 1500) | defp humanize_codex_wrapper_event("agent_reasoning_section_break", _pa...
method humanize_codex_wrapper_event (line 1501) | defp humanize_codex_wrapper_event("agent_reasoning", payload), do: hum...
method humanize_codex_wrapper_event (line 1502) | defp humanize_codex_wrapper_event("turn_diff", _payload), do: "turn di...
method humanize_codex_wrapper_event (line 1503) | defp humanize_codex_wrapper_event("exec_command_begin", payload), do: ...
method humanize_codex_wrapper_event (line 1504) | defp humanize_codex_wrapper_event("exec_command_end", payload), do: hu...
method humanize_codex_wrapper_event (line 1505) | defp humanize_codex_wrapper_event("exec_command_output_delta", _payloa...
method humanize_codex_wrapper_event (line 1506) | defp humanize_codex_wrapper_event("mcp_tool_call_begin", _payload), do...
method humanize_codex_wrapper_event (line 1507) | defp humanize_codex_wrapper_event("mcp_tool_call_end", _payload), do: ...
method humanize_codex_wrapper_event (line 1509) | defp humanize_codex_wrapper_event("token_count", payload) do
method humanize_codex_wrapper_event (line 1518) | defp humanize_codex_wrapper_event(other, payload) do
method humanize_exec_command_begin (line 1530) | defp humanize_exec_command_begin(payload) do
method humanize_exec_command_end (line 1546) | defp humanize_exec_command_end(payload) do
method format_usage_counts (line 1613) | defp format_usage_counts(_usage), do: nil
method append_usage_part (line 1616) | defp append_usage_part(parts, label, value), do: parts ++ ["#{label} #...
method format_rate_limits_summary (line 1618) | defp format_rate_limits_summary(nil), do: "n/a"
method format_rate_limits_summary (line 1635) | defp format_rate_limits_summary(_rate_limits), do: "n/a"
method format_rate_limit_bucket_summary (line 1653) | defp format_rate_limit_bucket_summary(_bucket), do: nil
method format_error_value (line 1657) | defp format_error_value(error), do: inspect(error, limit: 10)
method format_reason (line 1671) | defp format_reason(other), do: format_error_value(other)
method humanize_streaming_event (line 1673) | defp humanize_streaming_event(label, payload) do
method humanize_reasoning_update (line 1680) | defp humanize_reasoning_update(payload) do
method extract_reasoning_focus (line 1687) | defp extract_reasoning_focus(payload) do
method extract_delta_preview (line 1698) | defp extract_delta_preview(payload) do
method extract_command (line 1711) | defp extract_command(payload) do
method fallback_command (line 1718) | defp fallback_command(nil, payload) do
method fallback_command (line 1725) | defp fallback_command(command, _payload), do: command
method normalize_command (line 1727) | defp normalize_command(%{} = command) do
method normalize_command (line 1750) | defp normalize_command(_command), do: nil
method humanize_item_type (line 1752) | defp humanize_item_type(nil), do: "item"
method humanize_item_type (line 1763) | defp humanize_item_type(type), do: to_string(type)
method humanize_status (line 1773) | defp humanize_status(_status), do: nil
method short_id (line 1777) | defp short_id(_id), do: nil
method append_if_present (line 1780) | defp append_if_present(list, _value), do: list
method wrapper_payload_type (line 1782) | defp wrapper_payload_type(payload) do
method inline_text (line 1795) | defp inline_text(other), do: other |> to_string() |> inline_text()
method parse_integer (line 1806) | defp parse_integer(_value), do: nil
method token_usage_paths (line 1808) | defp token_usage_paths do
method delta_paths (line 1819) | defp delta_paths do
method reasoning_focus_paths (line 1858) | defp reasoning_focus_paths do
method extract_first_path (line 1887) | defp extract_first_path(payload, paths) do
method map_path (line 1901) | defp map_path(_data, _path), do: nil
method alternate_key (line 1926) | defp alternate_key(key), do: key
method truncate (line 1932) | defp truncate(value, _max), do: value
method dashboard_enabled? (line 1934) | defp dashboard_enabled? do
method keyword_override (line 1946) | defp keyword_override(opts, key) do
method resolve_override (line 1950) | defp resolve_override(nil, default), do: default
method resolve_override (line 1951) | defp resolve_override(override, _default), do: override
FILE: elixir/lib/symphony_elixir/tracker.ex
class SymphonyElixir.Tracker (line 1) | defmodule SymphonyElixir.Tracker
method fetch_candidate_issues (line 15) | def fetch_candidate_issues do
method fetch_issues_by_states (line 20) | def fetch_issues_by_states(states) do
method fetch_issue_states_by_ids (line 25) | def fetch_issue_states_by_ids(issue_ids) do
method create_comment (line 30) | def create_comment(issue_id, body) do
method update_issue_state (line 35) | def update_issue_state(issue_id, state_name) do
method adapter (line 40) | def adapter do
FILE: elixir/lib/symphony_elixir/tracker/memory.ex
class SymphonyElixir.Tracker.Memory (line 1) | defmodule SymphonyElixir.Tracker.Memory
method fetch_candidate_issues (line 11) | def fetch_candidate_issues do
method fetch_issues_by_states (line 16) | def fetch_issues_by_states(state_names) do
method fetch_issue_states_by_ids (line 29) | def fetch_issue_states_by_ids(issue_ids) do
method create_comment (line 39) | def create_comment(issue_id, body) do
method update_issue_state (line 45) | def update_issue_state(issue_id, state_name) do
method configured_issues (line 50) | defp configured_issues do
method issue_entries (line 54) | defp issue_entries do
method send_event (line 58) | defp send_event(message) do
method normalize_state (line 71) | defp normalize_state(_state), do: ""
FILE: elixir/lib/symphony_elixir/workflow.ex
class SymphonyElixir.Workflow (line 1) | defmodule SymphonyElixir.Workflow
method workflow_file_path (line 11) | def workflow_file_path do
method clear_workflow_file_path (line 24) | def clear_workflow_file_path do
method current (line 37) | def current do
method load (line 48) | def load do
method parse (line 63) | defp parse(content) do
method split_front_matter (line 85) | defp split_front_matter(content) do
method front_matter_yaml_to_map (line 102) | defp front_matter_yaml_to_map(lines) do
method maybe_reload_store (line 116) | defp maybe_reload_store do
FILE: elixir/lib/symphony_elixir/workflow_store.ex
class SymphonyElixir.WorkflowStore (line 1) | defmodule SymphonyElixir.WorkflowStore
method start_link (line 20) | def start_link(opts \\ []) do
method current (line 25) | def current do
method force_reload (line 36) | def force_reload do
method init (line 50) | def init(_opts) do
method handle_call (line 62) | def handle_call(:current, _from, %State{} = state) do
method handle_call (line 72) | def handle_call(:force_reload, _from, %State{} = state) do
method handle_info (line 83) | def handle_info(:poll, %State{} = state) do
method schedule_poll (line 92) | defp schedule_poll do
method reload_state (line 96) | defp reload_state(%State{} = state) do
method reload_path (line 106) | defp reload_path(path, state) do
method reload_current_path (line 117) | defp reload_current_path(path, state) do
method load_state (line 131) | defp load_state(path) do
method log_reload_error (line 150) | defp log_reload_error(path, reason) do
class State (line 13) | defmodule State
FILE: elixir/lib/symphony_elixir/workspace.ex
class SymphonyElixir.Workspace (line 1) | defmodule SymphonyElixir.Workspace
method create_for_issue (line 15) | def create_for_issue(issue_or_identifier, worker_host \\ nil) do
method ensure_workspace (line 34) | defp ensure_workspace(workspace, nil) do
method create_workspace (line 81) | defp create_workspace(workspace) do
method remove (line 88) | def remove(workspace), do: remove(workspace, nil)
method remove (line 91) | def remove(workspace, nil) do
method remove_issue_workspaces (line 131) | def remove_issue_workspaces(identifier), do: remove_issue_workspaces(i...
method remove_issue_workspaces (line 162) | def remove_issue_workspaces(_identifier, _worker_host) do
method safe_identifier (line 206) | defp safe_identifier(identifier) do
method maybe_run_after_create_hook (line 210) | defp maybe_run_after_create_hook(workspace, issue_context, created?, w...
method maybe_run_before_remove_hook (line 228) | defp maybe_run_before_remove_hook(workspace, nil) do
method ignore_hook_failure (line 291) | defp ignore_hook_failure(:ok), do: :ok
method ignore_hook_failure (line 292) | defp ignore_hook_failure({:error, _reason}), do: :ok
method run_hook (line 294) | defp run_hook(command, workspace, issue_context, hook_name, nil) do
method handle_hook_command_result (line 334) | defp handle_hook_command_result({_output, 0}, _workspace, _issue_id, _...
method handle_hook_command_result (line 338) | defp handle_hook_command_result({output, status}, workspace, issue_con...
method sanitize_hook_output_for_log (line 346) | defp sanitize_hook_output_for_log(output, max_bytes \\ 2_048) do
method parse_remote_workspace_output (line 412) | defp parse_remote_workspace_output(output) do
method worker_host_for_log (line 456) | defp worker_host_for_log(nil), do: "local"
method worker_host_for_log (line 457) | defp worker_host_for_log(worker_host), do: worker_host
method issue_context (line 459) | defp issue_context(%{id: issue_id, identifier: identifier}) do
method issue_context (line 473) | defp issue_context(_identifier) do
method issue_log_context (line 480) | defp issue_log_context(%{issue_id: issue_id, issue_identifier: issue_i...
FILE: elixir/lib/symphony_elixir_web/components/layouts.ex
class SymphonyElixirWeb.Layouts (line 1) | defmodule SymphonyElixirWeb.Layouts
method root (line 9) | def root(assigns) do
method app (line 49) | def app(assigns) do
FILE: elixir/lib/symphony_elixir_web/controllers/observability_api_controller.ex
class SymphonyElixirWeb.ObservabilityApiController (line 1) | defmodule SymphonyElixirWeb.ObservabilityApiController
method state (line 12) | def state(conn, _params) do
method issue (line 17) | def issue(conn, %{"issue_identifier" => issue_identifier}) do
method refresh (line 28) | def refresh(conn, _params) do
method method_not_allowed (line 41) | def method_not_allowed(conn, _params) do
method not_found (line 46) | def not_found(conn, _params) do
method error_response (line 50) | defp error_response(conn, status, code, message) do
method orchestrator (line 56) | defp orchestrator do
method snapshot_timeout_ms (line 60) | defp snapshot_timeout_ms do
FILE: elixir/lib/symphony_elixir_web/controllers/static_asset_controller.ex
class SymphonyElixirWeb.StaticAssetController (line 1) | defmodule SymphonyElixirWeb.StaticAssetController
method dashboard_css (line 12) | def dashboard_css(conn, _params), do: serve(conn, "/dashboard.css")
method phoenix_html_js (line 15) | def phoenix_html_js(conn, _params), do: serve(conn, "/vendor/phoenix_h...
method phoenix_js (line 18) | def phoenix_js(conn, _params), do: serve(conn, "/vendor/phoenix/phoeni...
method phoenix_live_view_js (line 21) | def phoenix_live_view_js(conn, _params), do: serve(conn, "/vendor/phoe...
method serve (line 23) | defp serve(conn, path) do
FILE: elixir/lib/symphony_elixir_web/endpoint.ex
class SymphonyElixirWeb.Endpoint (line 1) | defmodule SymphonyElixirWeb.Endpoint
FILE: elixir/lib/symphony_elixir_web/error_html.ex
class SymphonyElixirWeb.ErrorHTML (line 1) | defmodule SymphonyElixirWeb.ErrorHTML
method render (line 5) | def render(template, _assigns) do
FILE: elixir/lib/symphony_elixir_web/error_json.ex
class SymphonyElixirWeb.ErrorJSON (line 1) | defmodule SymphonyElixirWeb.ErrorJSON
method render (line 5) | def render(template, _assigns) do
FILE: elixir/lib/symphony_elixir_web/live/dashboard_live.ex
class SymphonyElixirWeb.DashboardLive (line 1) | defmodule SymphonyElixirWeb.DashboardLive
method mount (line 12) | def mount(_params, _session, socket) do
method handle_info (line 27) | def handle_info(:runtime_tick, socket) do
method handle_info (line 33) | def handle_info(:observability_updated, socket) do
method render (line 41) | def render(assigns) do
method load_payload (line 252) | defp load_payload do
method orchestrator (line 256) | defp orchestrator do
method snapshot_timeout_ms (line 260) | defp snapshot_timeout_ms do
method completed_runtime_seconds (line 264) | defp completed_runtime_seconds(payload) do
method total_runtime_seconds (line 268) | defp total_runtime_seconds(payload, now) do
method format_runtime_and_turns (line 279) | defp format_runtime_and_turns(started_at, _turn_count, now),
method runtime_seconds_from_started_at (line 289) | defp runtime_seconds_from_started_at(%DateTime{} = started_at, %DateTi...
method runtime_seconds_from_started_at (line 300) | defp runtime_seconds_from_started_at(_started_at, _now), do: 0
method format_int (line 310) | defp format_int(_value), do: "n/a"
method state_badge_class (line 312) | defp state_badge_class(state) do
method schedule_runtime_tick (line 324) | defp schedule_runtime_tick do
method pretty_value (line 328) | defp pretty_value(nil), do: "n/a"
method pretty_value (line 329) | defp pretty_value(value), do: inspect(value, pretty: true, limit: :inf...
FILE: elixir/lib/symphony_elixir_web/observability_pubsub.ex
class SymphonyElixirWeb.ObservabilityPubSub (line 1) | defmodule SymphonyElixirWeb.ObservabilityPubSub
method subscribe (line 11) | def subscribe do
method broadcast_update (line 16) | def broadcast_update do
FILE: elixir/lib/symphony_elixir_web/presenter.ex
class SymphonyElixirWeb.Presenter (line 1) | defmodule SymphonyElixirWeb.Presenter
method state_payload (line 9) | def state_payload(orchestrator, snapshot_timeout_ms) do
method refresh_payload (line 53) | def refresh_payload(orchestrator) do
method issue_payload_body (line 63) | defp issue_payload_body(issue_identifier, running, retry) do
method issue_id_from_entries (line 87) | defp issue_id_from_entries(running, retry),
method restart_count (line 90) | defp restart_count(retry), do: max(retry_attempt(retry) - 1, 0)
method retry_attempt (line 91) | defp retry_attempt(nil), do: 0
method retry_attempt (line 92) | defp retry_attempt(retry), do: retry.attempt || 0
method issue_status (line 94) | defp issue_status(_running, nil), do: "running"
method issue_status (line 95) | defp issue_status(nil, _retry), do: "retrying"
method issue_status (line 96) | defp issue_status(_running, _retry), do: "running"
method running_entry_payload (line 98) | defp running_entry_payload(entry) do
method retry_entry_payload (line 119) | defp retry_entry_payload(entry) do
method running_issue_payload (line 131) | defp running_issue_payload(running) do
method retry_issue_payload (line 150) | defp retry_issue_payload(retry) do
method workspace_path (line 160) | defp workspace_path(issue_identifier, running, retry) do
method workspace_host (line 166) | defp workspace_host(running, retry) do
method recent_events_payload (line 170) | defp recent_events_payload(running) do
method summarize_message (line 181) | defp summarize_message(nil), do: nil
method summarize_message (line 182) | defp summarize_message(message), do: StatusDashboard.humanize_codex_me...
method due_at_iso8601 (line 191) | defp due_at_iso8601(_due_in_ms), do: nil
method iso8601 (line 193) | defp iso8601(%DateTime{} = datetime) do
method iso8601 (line 199) | defp iso8601(_datetime), do: nil
FILE: elixir/lib/symphony_elixir_web/router.ex
class SymphonyElixirWeb.Router (line 1) | defmodule SymphonyElixirWeb.Router
FILE: elixir/lib/symphony_elixir_web/static_assets.ex
class SymphonyElixirWeb.StaticAssets (line 1) | defmodule SymphonyElixirWeb.StaticAssets
FILE: elixir/mix.exs
class SymphonyElixir.MixProject (line 1) | defmodule SymphonyElixir.MixProject
method project (line 4) | def project do
method application (line 56) | def application do
method deps (line 64) | defp deps do
method aliases (line 82) | defp aliases do
method escript (line 90) | defp escript do
FILE: elixir/test/mix/tasks/pr_body_check_test.exs
class Mix.Tasks.PrBody.CheckTest (line 1) | defmodule Mix.Tasks.PrBody.CheckTest
method in_temp_repo (line 319) | defp in_temp_repo(fun) do
method write_template! (line 337) | defp write_template!(content) do
FILE: elixir/test/mix/tasks/specs_check_task_test.exs
class Mix.Tasks.Specs.CheckTaskTest (line 1) | defmodule Mix.Tasks.Specs.CheckTaskTest
method in_temp_project (line 93) | defp in_temp_project(fun) do
method write_module! (line 108) | defp write_module!(path, source) do
FILE: elixir/test/mix/tasks/workspace_before_remove_test.exs
class Mix.Tasks.Workspace.BeforeRemoveTest (line 1) | defmodule Mix.Tasks.Workspace.BeforeRemoveTest
method with_fake_gh (line 257) | defp with_fake_gh(fun) do
method with_fake_gh (line 289) | defp with_fake_gh(script, fun) do
method with_fake_gh_and_git (line 293) | defp with_fake_gh_and_git(gh_script, git_script, fun) do
method with_fake_binaries (line 297) | defp with_fake_binaries(scripts, fun) do
method with_path (line 330) | defp with_path(paths, fun) do
method with_env (line 334) | defp with_env(overrides, fun) do
method in_temp_dir (line 349) | defp in_temp_dir(fun) do
method capture_task_output (line 367) | defp capture_task_output(fun) do
FILE: elixir/test/support/snapshot_support.exs
class SymphonyElixir.TestSupport.Snapshot (line 1) | defmodule SymphonyElixir.TestSupport.Snapshot
method snapshot_path (line 63) | defp snapshot_path(relative_path), do: Path.join(@snapshot_root, relat...
method update_snapshots? (line 65) | defp update_snapshots? do
method normalize_content (line 72) | defp normalize_content(content) do
FILE: elixir/test/support/test_support.exs
class SymphonyElixir.TestSupport (line 1) | defmodule SymphonyElixir.TestSupport
method write_workflow_file! (line 54) | def write_workflow_file!(path, overrides \\ []) do
method restore_env (line 69) | def restore_env(key, nil), do: System.delete_env(key)
method restore_env (line 70) | def restore_env(key, value), do: System.put_env(key, value)
method stop_default_http_server (line 72) | def stop_default_http_server do
method workflow_content (line 91) | defp workflow_content(overrides) do
method yaml_value (line 211) | defp yaml_value(true), do: "true"
method yaml_value (line 212) | defp yaml_value(false), do: "false"
method yaml_value (line 213) | defp yaml_value(nil), do: "null"
method yaml_value (line 226) | defp yaml_value(value), do: yaml_value(to_string(value))
method hooks_yaml (line 228) | defp hooks_yaml(nil, nil, nil, nil, timeout_ms), do: "hooks:\n timeou...
method hooks_yaml (line 230) | defp hooks_yaml(hook_after_create, hook_before_run, hook_after_run, ho...
method worker_yaml (line 247) | defp worker_yaml(ssh_hosts, max_concurrent_agents_per_host) do
method observability_yaml (line 258) | defp observability_yaml(enabled, refresh_ms, render_interval_ms) do
method server_yaml (line 268) | defp server_yaml(nil, nil), do: nil
method server_yaml (line 270) | defp server_yaml(port, host) do
method hook_entry (line 280) | defp hook_entry(_name, nil), do: nil
FILE: elixir/test/symphony_elixir/app_server_test.exs
class SymphonyElixir.AppServerTest (line 1) | defmodule SymphonyElixir.AppServerTest
FILE: elixir/test/symphony_elixir/cli_test.exs
class SymphonyElixir.CLITest (line 1) | defmodule SymphonyElixir.CLITest
FILE: elixir/test/symphony_elixir/core_test.exs
class SymphonyElixir.CoreTest (line 1) | defmodule SymphonyElixir.CoreTest
method assert_due_in_range (line 753) | defp assert_due_in_range(due_at_ms, min_remaining_ms, max_remaining_ms...
method restore_app_env (line 760) | defp restore_app_env(key, nil), do: Application.delete_env(:symphony_e...
method restore_app_env (line 761) | defp restore_app_env(key, value), do: Application.put_env(:symphony_el...
FILE: elixir/test/symphony_elixir/dynamic_tool_test.exs
class SymphonyElixir.Codex.DynamicToolTest (line 1) | defmodule SymphonyElixir.Codex.DynamicToolTest
FILE: elixir/test/symphony_elixir/extensions_test.exs
class SymphonyElixir.ExtensionsTest (line 1) | defmodule SymphonyElixir.ExtensionsTest
method start_test_endpoint (line 675) | defp start_test_endpoint(overrides) do
method static_snapshot (line 686) | defp static_snapshot do
method wait_for_bound_port (line 719) | defp wait_for_bound_port do
method assert_eventually (line 727) | defp assert_eventually(fun, attempts \\ 20)
method assert_eventually (line 738) | defp assert_eventually(_fun, 0), do: flunk("condition not met in time")
method ensure_workflow_store_running (line 740) | defp ensure_workflow_store_running do
class FakeLinearClient (line 12) | defmodule FakeLinearClient
method fetch_candidate_issues (line 13) | def fetch_candidate_issues do
method fetch_issues_by_states (line 18) | def fetch_issues_by_states(states) do
method fetch_issue_states_by_ids (line 23) | def fetch_issue_states_by_ids(issue_ids) do
method graphql (line 28) | def graphql(query, variables) do
class SlowOrchestrator (line 42) | defmodule SlowOrchestrator
method start_link (line 45) | def start_link(opts) do
method init (line 49) | def init(:ok), do: {:ok, :ok}
method handle_call (line 51) | def handle_call(:snapshot, _from, state) do
method handle_call (line 56) | def handle_call(:request_refresh, _from, state) do
class StaticOrchestrator (line 61) | defmodule StaticOrchestrator
method start_link (line 64) | def start_link(opts) do
method init (line 69) | def init(opts), do: {:ok, opts}
method handle_call (line 71) | def handle_call(:snapshot, _from, state) do
method handle_call (line 75) | def handle_call(:request_refresh, _from, state) do
FILE: elixir/test/symphony_elixir/live_e2e_test.exs
class SymphonyElixir.LiveE2ETest (line 1) | defmodule SymphonyElixir.LiveE2ETest
method fetch_team! (line 133) | defp fetch_team!(team_key) do
method completed_project_status! (line 173) | defp completed_project_status! do
method create_project! (line 187) | defp create_project!(team_id, name) do
method create_issue! (line 193) | defp create_issue!(team_id, project_id, state_id, title) do
method issue_completed? (line 241) | defp issue_completed?(%{"state" => %{"type" => type}}), do: type in ["...
method issue_completed? (line 242) | defp issue_completed?(_issue), do: false
method issue_has_comment? (line 248) | defp issue_has_comment?(_issue, _expected_body), do: false
method update_entity (line 250) | defp update_entity(mutation, variables, mutation_name, entity_name) do
method live_prompt (line 299) | defp live_prompt(project_slug) do
method expected_result (line 391) | defp expected_result(issue_identifier, project_slug) do
method expected_comment (line 395) | defp expected_comment(issue_identifier, project_slug) do
method receive_runtime_info! (line 399) | defp receive_runtime_info!(issue_id) do
method cleanup_live_worker_setup (line 544) | defp cleanup_live_worker_setup(_worker_setup), do: :ok
method restart_orchestrator_if_needed (line 546) | defp restart_orchestrator_if_needed do
method live_ssh_worker_hosts (line 626) | defp live_ssh_worker_hosts do
method shared_remote_home! (line 655) | defp shared_remote_home!(_worker_hosts), do: flunk("expected at least ...
method reserve_tcp_ports (line 679) | defp reserve_tcp_ports(0, _seen, ports), do: Enum.reverse(ports)
method reserve_tcp_ports (line 681) | defp reserve_tcp_ports(remaining, seen, ports) do
method reserve_tcp_port! (line 691) | defp reserve_tcp_port! do
method retry_or_flunk_ssh_host (line 794) | defp retry_or_flunk_ssh_host(worker_host, deadline_ms) do
FILE: elixir/test/symphony_elixir/log_file_test.exs
class SymphonyElixir.LogFileTest (line 1) | defmodule SymphonyElixir.LogFileTest
FILE: elixir/test/symphony_elixir/observability_pubsub_test.exs
class SymphonyElixir.ObservabilityPubSubTest (line 1) | defmodule SymphonyElixir.ObservabilityPubSubTest
FILE: elixir/test/symphony_elixir/orchestrator_status_test.exs
class SymphonyElixir.OrchestratorStatusTest (line 1) | defmodule SymphonyElixir.OrchestratorStatusTest
method do_wait_for_snapshot (line 1560) | defp do_wait_for_snapshot(pid, predicate, deadline_ms) do
method graph_samples_from_rates (line 1575) | defp graph_samples_from_rates(rates_per_bucket) do
method graph_samples_for_stability_test (line 1588) | defp graph_samples_for_stability_test(now_ms) do
FILE: elixir/test/symphony_elixir/specs_check_test.exs
class SymphonyElixir.SpecsCheckTest (line 1) | defmodule SymphonyElixir.SpecsCheckTest
method create_tmp_dir (line 80) | defp create_tmp_dir do
method write_module! (line 88) | defp write_module!(dir, rel_path, source) do
FILE: elixir/test/symphony_elixir/ssh_test.exs
class SymphonyElixir.SSHTest (line 1) | defmodule SymphonyElixir.SSHTest
method install_fake_ssh! (line 165) | defp install_fake_ssh!(test_root, trace_file, script \\ nil) do
method wait_for_trace! (line 185) | defp wait_for_trace!(trace_file, attempts \\ 20)
method wait_for_trace! (line 186) | defp wait_for_trace!(trace_file, 0), do: flunk("timed out waiting for ...
method wait_for_trace! (line 188) | defp wait_for_trace!(trace_file, attempts) do
method restore_env (line 197) | defp restore_env(key, nil), do: System.delete_env(key)
method restore_env (line 198) | defp restore_env(key, value), do: System.put_env(key, value)
FILE: elixir/test/symphony_elixir/status_dashboard_snapshot_test.exs
class SymphonyElixir.StatusDashboardSnapshotTest (line 1) | defmodule SymphonyElixir.StatusDashboardSnapshotTest
method render_snapshot (line 197) | defp render_snapshot(snapshot_data, tps) do
method running_entry (line 201) | defp running_entry(overrides) do
method retry_entry (line 218) | defp retry_entry(overrides) do
method turn_started_message (line 231) | defp turn_started_message do
method turn_completed_message (line 241) | defp turn_completed_message(status) do
method exec_command_message (line 251) | defp exec_command_message(command) do
method agent_message_delta (line 261) | defp agent_message_delta(delta) do
method token_usage_message (line 271) | defp token_usage_message(input_tokens, output_tokens, total_tokens) do
FILE: elixir/test/symphony_elixir/workspace_and_config_test.exs
class SymphonyElixir.WorkspaceAndConfigTest (line 1) | defmodule SymphonyElixir.WorkspaceAndConfigTest
Condensed preview — 98 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (861K chars).
[
{
"path": ".codex/skills/commit/SKILL.md",
"chars": 2480,
"preview": "---\nname: commit\ndescription:\n Create a well-formed git commit from current changes using session history for\n rationa"
},
{
"path": ".codex/skills/debug/SKILL.md",
"chars": 4314,
"preview": "---\nname: debug\ndescription:\n Investigate stuck runs and execution failures by tracing Symphony and Codex\n logs with i"
},
{
"path": ".codex/skills/land/SKILL.md",
"chars": 10054,
"preview": "---\nname: land\ndescription:\n Land a PR by monitoring conflicts, resolving them, waiting for checks, and\n squash-mergin"
},
{
"path": ".codex/skills/land/land_watch.py",
"chars": 19585,
"preview": "#!/usr/bin/env python3\nimport asyncio\nimport json\nimport random\nimport re\nfrom dataclasses import dataclass\nfrom datetim"
},
{
"path": ".codex/skills/linear/SKILL.md",
"chars": 6838,
"preview": "---\nname: linear\ndescription: |\n Use Symphony's `linear_graphql` client tool for raw Linear GraphQL\n operations such a"
},
{
"path": ".codex/skills/pull/SKILL.md",
"chars": 4683,
"preview": "---\nname: pull\ndescription:\n Pull latest origin/main into the current local branch and resolve merge\n conflicts (aka u"
},
{
"path": ".codex/skills/push/SKILL.md",
"chars": 4393,
"preview": "---\nname: push\ndescription:\n Push current branch changes to origin and create or update the corresponding\n pull reques"
},
{
"path": ".codex/worktree_init.sh",
"chars": 345,
"preview": "#!/usr/bin/env bash\nset -eo pipefail\n\nscript_dir=\"$(cd \"$(dirname \"$0\")\" && pwd)\"\nrepo_root=\"$(cd \"$script_dir/..\" && pw"
},
{
"path": ".github/pull_request_template.md",
"chars": 537,
"preview": "#### Context\n\n<!-- Why is this change needed? Length <= 240 chars -->\n\n#### TL;DR\n\n*<!-- A short description of what we "
},
{
"path": ".github/workflows/make-all.yml",
"chars": 741,
"preview": "name: make-all\n\non:\n pull_request:\n push:\n branches:\n - main\n\njobs:\n make-all:\n runs-on: ubuntu-latest\n "
},
{
"path": ".github/workflows/pr-description-lint.yml",
"chars": 819,
"preview": "name: pr-description-lint\n\non:\n pull_request:\n types: [opened, edited, reopened, synchronize, ready_for_review]\n\njob"
},
{
"path": "LICENSE",
"chars": 11357,
"preview": " Apache License\n Version 2.0, January 2004\n "
},
{
"path": "NOTICE",
"chars": 547,
"preview": "Copyright 2025 OpenAI\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except "
},
{
"path": "README.md",
"chars": 1731,
"preview": "# Symphony\n\nSymphony turns project work into isolated, autonomous implementation runs, allowing teams to manage\nwork ins"
},
{
"path": "SPEC.md",
"chars": 80220,
"preview": "# Symphony Service Specification\n\nStatus: Draft v1 (language-agnostic)\n\nPurpose: Define a service that orchestrates codi"
},
{
"path": "elixir/.formatter.exs",
"chars": 117,
"preview": "# Used by \"mix format\"\n[\n inputs: [\"{mix,.formatter}.exs\", \"{config,lib,test}/**/*.{ex,exs}\"],\n line_length: 200\n]\n"
},
{
"path": "elixir/.gitattributes",
"chars": 67,
"preview": "test/fixtures/status_dashboard_snapshots/* linguist-generated=true\n"
},
{
"path": "elixir/.gitignore",
"chars": 914,
"preview": "# The directory Mix will write compiled artifacts to.\n/_build/\n\n# If you run \"mix test --cover\", coverage assets end up "
},
{
"path": "elixir/AGENTS.md",
"chars": 2190,
"preview": "# Symphony Elixir\n\nThis directory contains the Elixir agent orchestration service that polls Linear, creates per-issue w"
},
{
"path": "elixir/Makefile",
"chars": 650,
"preview": ".PHONY: help all setup deps build fmt fmt-check lint test coverage ci dialyzer e2e\n\nMIX ?= mix\n\nhelp:\n\t@echo \"Targets: s"
},
{
"path": "elixir/README.md",
"chars": 8445,
"preview": "# Symphony Elixir\n\nThis directory contains the current Elixir/OTP implementation of Symphony, based on\n[`SPEC.md`](../SP"
},
{
"path": "elixir/WORKFLOW.md",
"chars": 19001,
"preview": "---\ntracker:\n kind: linear\n project_slug: \"symphony-0c79b11b75ea\"\n active_states:\n - Todo\n - In Progress\n - "
},
{
"path": "elixir/config/config.exs",
"chars": 472,
"preview": "import Config\n\nconfig :phoenix, :json_library, Jason\n\nconfig :symphony_elixir, SymphonyElixirWeb.Endpoint,\n adapter: Ba"
},
{
"path": "elixir/docs/logging.md",
"chars": 1607,
"preview": "# Logging Best Practices\n\nThis guide defines logging conventions for Symphony so Codex can diagnose failures quickly.\n\n#"
},
{
"path": "elixir/docs/token_accounting.md",
"chars": 9172,
"preview": "# Codex Token Accounting\n\nThis document explains how Codex reports token usage through the app-server protocol and how S"
},
{
"path": "elixir/lib/mix/tasks/pr_body.check.ex",
"chars": 6369,
"preview": "defmodule Mix.Tasks.PrBody.Check do\n use Mix.Task\n\n @shortdoc \"Validate PR body format against the repository PR templ"
},
{
"path": "elixir/lib/mix/tasks/specs.check.ex",
"chars": 1489,
"preview": "defmodule Mix.Tasks.Specs.Check do\n use Mix.Task\n\n alias SymphonyElixir.SpecsCheck\n\n @moduledoc \"\"\"\n Enforces adjace"
},
{
"path": "elixir/lib/mix/tasks/workspace.before_remove.ex",
"chars": 3430,
"preview": "defmodule Mix.Tasks.Workspace.BeforeRemove do\n use Mix.Task\n\n @shortdoc \"Close open GitHub PRs for the current branch "
},
{
"path": "elixir/lib/symphony_elixir/agent_runner.ex",
"chars": 7161,
"preview": "defmodule SymphonyElixir.AgentRunner do\n @moduledoc \"\"\"\n Executes a single Linear issue in its workspace with Codex.\n "
},
{
"path": "elixir/lib/symphony_elixir/cli.ex",
"chars": 5499,
"preview": "defmodule SymphonyElixir.CLI do\n @moduledoc \"\"\"\n Escript entrypoint for running Symphony with an explicit WORKFLOW.md "
},
{
"path": "elixir/lib/symphony_elixir/codex/app_server.ex",
"chars": 30051,
"preview": "defmodule SymphonyElixir.Codex.AppServer do\n @moduledoc \"\"\"\n Minimal client for the Codex app-server JSON-RPC 2.0 stre"
},
{
"path": "elixir/lib/symphony_elixir/codex/dynamic_tool.ex",
"chars": 5564,
"preview": "defmodule SymphonyElixir.Codex.DynamicTool do\n @moduledoc \"\"\"\n Executes client-side tool calls requested by Codex app-"
},
{
"path": "elixir/lib/symphony_elixir/config/schema.ex",
"chars": 16800,
"preview": "defmodule SymphonyElixir.Config.Schema do\n @moduledoc false\n\n use Ecto.Schema\n\n import Ecto.Changeset\n\n alias Sympho"
},
{
"path": "elixir/lib/symphony_elixir/config.ex",
"chars": 4327,
"preview": "defmodule SymphonyElixir.Config do\n @moduledoc \"\"\"\n Runtime configuration loaded from `WORKFLOW.md`.\n \"\"\"\n\n alias Sy"
},
{
"path": "elixir/lib/symphony_elixir/http_server.ex",
"chars": 2587,
"preview": "defmodule SymphonyElixir.HttpServer do\n @moduledoc \"\"\"\n Compatibility facade that starts the Phoenix observability end"
},
{
"path": "elixir/lib/symphony_elixir/linear/adapter.ex",
"chars": 3057,
"preview": "defmodule SymphonyElixir.Linear.Adapter do\n @moduledoc \"\"\"\n Linear-backed tracker adapter.\n \"\"\"\n\n @behaviour Symphon"
},
{
"path": "elixir/lib/symphony_elixir/linear/client.ex",
"chars": 16285,
"preview": "defmodule SymphonyElixir.Linear.Client do\n @moduledoc \"\"\"\n Thin Linear GraphQL client for polling candidate issues.\n "
},
{
"path": "elixir/lib/symphony_elixir/linear/issue.ex",
"chars": 1027,
"preview": "defmodule SymphonyElixir.Linear.Issue do\n @moduledoc \"\"\"\n Normalized Linear issue representation used by the orchestra"
},
{
"path": "elixir/lib/symphony_elixir/log_file.ex",
"chars": 2235,
"preview": "defmodule SymphonyElixir.LogFile do\n @moduledoc \"\"\"\n Configures OTP's built-in rotating disk log handler for applicati"
},
{
"path": "elixir/lib/symphony_elixir/orchestrator.ex",
"chars": 52562,
"preview": "defmodule SymphonyElixir.Orchestrator do\n @moduledoc \"\"\"\n Polls Linear and dispatches repository copies to Codex-backe"
},
{
"path": "elixir/lib/symphony_elixir/path_safety.ex",
"chars": 1707,
"preview": "defmodule SymphonyElixir.PathSafety do\n @moduledoc false\n\n @spec canonicalize(Path.t()) :: {:ok, Path.t()} | {:error, "
},
{
"path": "elixir/lib/symphony_elixir/prompt_builder.ex",
"chars": 2040,
"preview": "defmodule SymphonyElixir.PromptBuilder do\n @moduledoc \"\"\"\n Builds agent prompts from Linear issue data.\n \"\"\"\n\n alias"
},
{
"path": "elixir/lib/symphony_elixir/specs_check.ex",
"chars": 5206,
"preview": "defmodule SymphonyElixir.SpecsCheck do\n @moduledoc false\n\n @type finding :: %{\n file: String.t(),\n m"
},
{
"path": "elixir/lib/symphony_elixir/ssh.ex",
"chars": 3374,
"preview": "defmodule SymphonyElixir.SSH do\n @moduledoc false\n\n @spec run(String.t(), String.t(), keyword()) :: {:ok, {String.t(),"
},
{
"path": "elixir/lib/symphony_elixir/status_dashboard.ex",
"chars": 60493,
"preview": "defmodule SymphonyElixir.StatusDashboard do\n @moduledoc \"\"\"\n Renders a status snapshot for orchestrator and worker act"
},
{
"path": "elixir/lib/symphony_elixir/tracker/memory.ex",
"chars": 1971,
"preview": "defmodule SymphonyElixir.Tracker.Memory do\n @moduledoc \"\"\"\n In-memory tracker adapter used for tests and local develop"
},
{
"path": "elixir/lib/symphony_elixir/tracker.ex",
"chars": 1623,
"preview": "defmodule SymphonyElixir.Tracker do\n @moduledoc \"\"\"\n Adapter boundary for issue tracker reads and writes.\n \"\"\"\n\n ali"
},
{
"path": "elixir/lib/symphony_elixir/workflow.ex",
"chars": 3004,
"preview": "defmodule SymphonyElixir.Workflow do\n @moduledoc \"\"\"\n Loads workflow configuration and prompt from WORKFLOW.md.\n \"\"\"\n"
},
{
"path": "elixir/lib/symphony_elixir/workflow_store.ex",
"chars": 3641,
"preview": "defmodule SymphonyElixir.WorkflowStore do\n @moduledoc \"\"\"\n Caches the last known good workflow and reloads it when `WO"
},
{
"path": "elixir/lib/symphony_elixir/workspace.ex",
"chars": 14626,
"preview": "defmodule SymphonyElixir.Workspace do\n @moduledoc \"\"\"\n Creates isolated per-issue workspaces for parallel Codex agents"
},
{
"path": "elixir/lib/symphony_elixir.ex",
"chars": 1069,
"preview": "defmodule SymphonyElixir do\n @moduledoc \"\"\"\n Entry point for the Symphony orchestrator.\n \"\"\"\n\n @doc \"\"\"\n Start the "
},
{
"path": "elixir/lib/symphony_elixir_web/components/layouts.ex",
"chars": 1665,
"preview": "defmodule SymphonyElixirWeb.Layouts do\n @moduledoc \"\"\"\n Shared layouts for the observability dashboard.\n \"\"\"\n\n use P"
},
{
"path": "elixir/lib/symphony_elixir_web/controllers/observability_api_controller.ex",
"chars": 1802,
"preview": "defmodule SymphonyElixirWeb.ObservabilityApiController do\n @moduledoc \"\"\"\n JSON API for Symphony observability data.\n "
},
{
"path": "elixir/lib/symphony_elixir_web/controllers/static_asset_controller.ex",
"chars": 1142,
"preview": "defmodule SymphonyElixirWeb.StaticAssetController do\n @moduledoc \"\"\"\n Serves the dashboard's embedded CSS and JavaScri"
},
{
"path": "elixir/lib/symphony_elixir_web/endpoint.ex",
"chars": 750,
"preview": "defmodule SymphonyElixirWeb.Endpoint do\n @moduledoc \"\"\"\n Phoenix endpoint for Symphony's optional observability UI and"
},
{
"path": "elixir/lib/symphony_elixir_web/error_html.ex",
"chars": 217,
"preview": "defmodule SymphonyElixirWeb.ErrorHTML do\n @moduledoc false\n\n @spec render(String.t(), map()) :: String.t()\n def rende"
},
{
"path": "elixir/lib/symphony_elixir_web/error_json.ex",
"chars": 258,
"preview": "defmodule SymphonyElixirWeb.ErrorJSON do\n @moduledoc false\n\n @spec render(String.t(), map()) :: map()\n def render(tem"
},
{
"path": "elixir/lib/symphony_elixir_web/live/dashboard_live.ex",
"chars": 11950,
"preview": "defmodule SymphonyElixirWeb.DashboardLive do\n @moduledoc \"\"\"\n Live observability dashboard for Symphony.\n \"\"\"\n\n use "
},
{
"path": "elixir/lib/symphony_elixir_web/observability_pubsub.ex",
"chars": 592,
"preview": "defmodule SymphonyElixirWeb.ObservabilityPubSub do\n @moduledoc \"\"\"\n PubSub helpers for observability dashboard updates"
},
{
"path": "elixir/lib/symphony_elixir_web/presenter.ex",
"chars": 6652,
"preview": "defmodule SymphonyElixirWeb.Presenter do\n @moduledoc \"\"\"\n Shared projections for the observability API and dashboard.\n"
},
{
"path": "elixir/lib/symphony_elixir_web/router.ex",
"chars": 1506,
"preview": "defmodule SymphonyElixirWeb.Router do\n @moduledoc \"\"\"\n Router for Symphony's observability dashboard and API.\n \"\"\"\n\n "
},
{
"path": "elixir/lib/symphony_elixir_web/static_assets.ex",
"chars": 1389,
"preview": "defmodule SymphonyElixirWeb.StaticAssets do\n @moduledoc false\n\n @dashboard_css_path Path.expand(\"../../priv/static/das"
},
{
"path": "elixir/mise.toml",
"chars": 47,
"preview": "[tools]\nerlang = \"28\"\nelixir = \"1.19.5-otp-28\"\n"
},
{
"path": "elixir/mix.exs",
"chars": 2635,
"preview": "defmodule SymphonyElixir.MixProject do\n use Mix.Project\n\n def project do\n [\n app: :symphony_elixir,\n vers"
},
{
"path": "elixir/priv/static/dashboard.css",
"chars": 8078,
"preview": ":root {\n color-scheme: light;\n --page: #f7f7f8;\n --page-soft: #fbfbfc;\n --page-deep: #ececf1;\n --card: rgba(255, 25"
},
{
"path": "elixir/test/fixtures/status_dashboard_snapshots/backoff_queue.evidence.md",
"chars": 1003,
"preview": "```text\n╭─ SYMPHONY STATUS\n│ Agents: 1/10\n│ Throughput: 15 tps\n│ Runtime: 45m 0s\n│ Tokens: in 18,000 | out 2,200 | total"
},
{
"path": "elixir/test/fixtures/status_dashboard_snapshots/backoff_queue.snapshot.txt",
"chars": 1666,
"preview": "\\e[1m╭─ SYMPHONY STATUS\\e[0m\n\\e[1m│ Agents: \\e[0m\\e[32m1\\e[0m\\e[90m/\\e[0m\\e[90m10\\e[0m\n\\e[1m│ Throughput: \\e[0m\\e[36m15 "
},
{
"path": "elixir/test/fixtures/status_dashboard_snapshots/credits_unlimited.evidence.md",
"chars": 706,
"preview": "```text\n╭─ SYMPHONY STATUS\n│ Agents: 1/10\n│ Throughput: 42 tps\n│ Runtime: 1m 15s\n│ Tokens: in 90 | out 12 | total 102\n│ "
},
{
"path": "elixir/test/fixtures/status_dashboard_snapshots/credits_unlimited.snapshot.txt",
"chars": 1124,
"preview": "\\e[1m╭─ SYMPHONY STATUS\\e[0m\n\\e[1m│ Agents: \\e[0m\\e[32m1\\e[0m\\e[90m/\\e[0m\\e[90m10\\e[0m\n\\e[1m│ Throughput: \\e[0m\\e[36m42 "
},
{
"path": "elixir/test/fixtures/status_dashboard_snapshots/idle.evidence.md",
"chars": 526,
"preview": "```text\n╭─ SYMPHONY STATUS\n│ Agents: 0/10\n│ Throughput: 0 tps\n│ Runtime: 0m 0s\n│ Tokens: in 0 | out 0 | total 0\n│ Rate L"
},
{
"path": "elixir/test/fixtures/status_dashboard_snapshots/idle.snapshot.txt",
"chars": 801,
"preview": "\\e[1m╭─ SYMPHONY STATUS\\e[0m\n\\e[1m│ Agents: \\e[0m\\e[32m0\\e[0m\\e[90m/\\e[0m\\e[90m10\\e[0m\n\\e[1m│ Throughput: \\e[0m\\e[36m0 t"
},
{
"path": "elixir/test/fixtures/status_dashboard_snapshots/idle_with_dashboard_url.evidence.md",
"chars": 562,
"preview": "```text\n╭─ SYMPHONY STATUS\n│ Agents: 0/10\n│ Throughput: 0 tps\n│ Runtime: 0m 0s\n│ Tokens: in 0 | out 0 | total 0\n│ Rate L"
},
{
"path": "elixir/test/fixtures/status_dashboard_snapshots/idle_with_dashboard_url.snapshot.txt",
"chars": 858,
"preview": "\\e[1m╭─ SYMPHONY STATUS\\e[0m\n\\e[1m│ Agents: \\e[0m\\e[32m0\\e[0m\\e[90m/\\e[0m\\e[90m10\\e[0m\n\\e[1m│ Throughput: \\e[0m\\e[36m0 t"
},
{
"path": "elixir/test/fixtures/status_dashboard_snapshots/super_busy.evidence.md",
"chars": 834,
"preview": "```text\n╭─ SYMPHONY STATUS\n│ Agents: 2/10\n│ Throughput: 1,842 tps\n│ Runtime: 72m 1s\n│ Tokens: in 250,000 | out 18,500 | "
},
{
"path": "elixir/test/fixtures/status_dashboard_snapshots/super_busy.snapshot.txt",
"chars": 1340,
"preview": "\\e[1m╭─ SYMPHONY STATUS\\e[0m\n\\e[1m│ Agents: \\e[0m\\e[32m2\\e[0m\\e[90m/\\e[0m\\e[90m10\\e[0m\n\\e[1m│ Throughput: \\e[0m\\e[36m1,8"
},
{
"path": "elixir/test/mix/tasks/pr_body_check_test.exs",
"chars": 7502,
"preview": "defmodule Mix.Tasks.PrBody.CheckTest do\n use ExUnit.Case, async: false\n\n alias Mix.Tasks.PrBody.Check\n\n import ExUnit"
},
{
"path": "elixir/test/mix/tasks/specs_check_task_test.exs",
"chars": 2725,
"preview": "defmodule Mix.Tasks.Specs.CheckTaskTest do\n use ExUnit.Case, async: false\n\n import ExUnit.CaptureIO\n\n alias Mix.Tasks"
},
{
"path": "elixir/test/mix/tasks/workspace_before_remove_test.exs",
"chars": 9284,
"preview": "defmodule Mix.Tasks.Workspace.BeforeRemoveTest do\n use ExUnit.Case, async: false\n\n alias Mix.Tasks.Workspace.BeforeRem"
},
{
"path": "elixir/test/support/live_e2e_docker/Dockerfile",
"chars": 600,
"preview": "FROM node:20-bookworm-slim\n\nRUN apt-get update && apt-get install -y --no-install-recommends \\\n bash \\\n ca-certifi"
},
{
"path": "elixir/test/support/live_e2e_docker/docker-compose.yml",
"chars": 607,
"preview": "services:\n worker1:\n build:\n context: .\n dockerfile: Dockerfile\n ports:\n - \"${SYMPHONY_LIVE_DOCKER"
},
{
"path": "elixir/test/support/live_e2e_docker/live_worker_entrypoint.sh",
"chars": 312,
"preview": "#!/bin/sh\nset -eu\n\ninstall -d -m 700 /root/.ssh /root/.codex\n\nif [ ! -s /run/symphony/ssh/authorized_key.pub ]; then\n e"
},
{
"path": "elixir/test/support/live_e2e_docker/symphony-live-worker.conf",
"chars": 188,
"preview": "PubkeyAuthentication yes\nPasswordAuthentication no\nKbdInteractiveAuthentication no\nChallengeResponseAuthentication no\nUs"
},
{
"path": "elixir/test/support/snapshot_support.exs",
"chars": 2308,
"preview": "defmodule SymphonyElixir.TestSupport.Snapshot do\n import ExUnit.Assertions\n\n @snapshot_root Path.expand(\"../fixtures\","
},
{
"path": "elixir/test/support/test_support.exs",
"chars": 11038,
"preview": "defmodule SymphonyElixir.TestSupport do\n @workflow_prompt \"You are an agent for this repository.\"\n\n defmacro __using__"
},
{
"path": "elixir/test/symphony_elixir/app_server_test.exs",
"chars": 45411,
"preview": "defmodule SymphonyElixir.AppServerTest do\n use SymphonyElixir.TestSupport\n\n test \"app server rejects the workspace roo"
},
{
"path": "elixir/test/symphony_elixir/cli_test.exs",
"chars": 4499,
"preview": "defmodule SymphonyElixir.CLITest do\n use ExUnit.Case, async: true\n\n alias SymphonyElixir.CLI\n\n @ack_flag \"--i-underst"
},
{
"path": "elixir/test/symphony_elixir/core_test.exs",
"chars": 60030,
"preview": "defmodule SymphonyElixir.CoreTest do\n use SymphonyElixir.TestSupport\n\n test \"config defaults and validation checks\" do"
},
{
"path": "elixir/test/symphony_elixir/dynamic_tool_test.exs",
"chars": 9902,
"preview": "defmodule SymphonyElixir.Codex.DynamicToolTest do\n use SymphonyElixir.TestSupport\n\n alias SymphonyElixir.Codex.Dynamic"
},
{
"path": "elixir/test/symphony_elixir/extensions_test.exs",
"chars": 25911,
"preview": "defmodule SymphonyElixir.ExtensionsTest do\n use SymphonyElixir.TestSupport\n\n import Phoenix.ConnTest\n import Phoenix."
},
{
"path": "elixir/test/symphony_elixir/live_e2e_test.exs",
"chars": 24740,
"preview": "defmodule SymphonyElixir.LiveE2ETest do\n use SymphonyElixir.TestSupport\n\n require Logger\n alias SymphonyElixir.SSH\n\n "
},
{
"path": "elixir/test/symphony_elixir/log_file_test.exs",
"chars": 441,
"preview": "defmodule SymphonyElixir.LogFileTest do\n use ExUnit.Case, async: true\n\n alias SymphonyElixir.LogFile\n\n test \"default_"
},
{
"path": "elixir/test/symphony_elixir/observability_pubsub_test.exs",
"chars": 946,
"preview": "defmodule SymphonyElixir.ObservabilityPubSubTest do\n use SymphonyElixir.TestSupport\n\n alias SymphonyElixirWeb.Observab"
},
{
"path": "elixir/test/symphony_elixir/orchestrator_status_test.exs",
"chars": 48246,
"preview": "defmodule SymphonyElixir.OrchestratorStatusTest do\n use SymphonyElixir.TestSupport\n\n test \"snapshot returns :timeout w"
},
{
"path": "elixir/test/symphony_elixir/specs_check_test.exs",
"chars": 2092,
"preview": "defmodule SymphonyElixir.SpecsCheckTest do\n use ExUnit.Case, async: true\n\n alias SymphonyElixir.SpecsCheck\n\n test \"re"
},
{
"path": "elixir/test/symphony_elixir/ssh_test.exs",
"chars": 6219,
"preview": "defmodule SymphonyElixir.SSHTest do\n use ExUnit.Case, async: false\n\n alias SymphonyElixir.SSH\n\n test \"run/3 keeps bra"
},
{
"path": "elixir/test/symphony_elixir/status_dashboard_snapshot_test.exs",
"chars": 8395,
"preview": "defmodule SymphonyElixir.StatusDashboardSnapshotTest do\n use SymphonyElixir.TestSupport\n\n alias SymphonyElixir.TestSup"
},
{
"path": "elixir/test/symphony_elixir/workspace_and_config_test.exs",
"chars": 45885,
"preview": "defmodule SymphonyElixir.WorkspaceAndConfigTest do\n use SymphonyElixir.TestSupport\n alias Ecto.Changeset\n alias Symph"
},
{
"path": "elixir/test/test_helper.exs",
"chars": 129,
"preview": "ExUnit.start()\nCode.require_file(\"support/snapshot_support.exs\", __DIR__)\nCode.require_file(\"support/test_support.exs\", "
}
]
About this extraction
This page contains the full source code of the openai/symphony GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 98 files (794.9 KB), approximately 205.0k tokens, and a symbol index with 898 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.