Full Code of openai/symphony for AI

main a164593aacb3 cached
98 files
794.9 KB
205.0k tokens
898 symbols
1 requests
Download .txt
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.

[![Symphony demo video preview](.github/media/symphony-demo-poster.jpg)](.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

![Symphony Elixir screenshot](../.github/media/elixir-screenshot.png)

## 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
Download .txt
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
Download .txt
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.

Copied to clipboard!