[
  {
    "path": ".codex/skills/commit/SKILL.md",
    "content": "---\nname: commit\ndescription:\n  Create a well-formed git commit from current changes using session history for\n  rationale and summary; use when asked to commit, prepare a commit message, or\n  finalize staged work.\n---\n\n# Commit\n\n## Goals\n\n- Produce a commit that reflects the actual code changes and the session\n  context.\n- Follow common git conventions (type prefix, short subject, wrapped body).\n- Include both summary and rationale in the body.\n\n## Inputs\n\n- Codex session history for intent and rationale.\n- `git status`, `git diff`, and `git diff --staged` for actual changes.\n- Repo-specific commit conventions if documented.\n\n## Steps\n\n1. Read session history to identify scope, intent, and rationale.\n2. Inspect the working tree and staged changes (`git status`, `git diff`,\n   `git diff --staged`).\n3. Stage intended changes, including new files (`git add -A`) after confirming\n   scope.\n4. Sanity-check newly added files; if anything looks random or likely ignored\n   (build artifacts, logs, temp files), flag it to the user before committing.\n5. If staging is incomplete or includes unrelated files, fix the index or ask\n   for confirmation.\n6. Choose a conventional type and optional scope that match the change (e.g.,\n   `feat(scope): ...`, `fix(scope): ...`, `refactor(scope): ...`).\n7. Write a subject line in imperative mood, <= 72 characters, no trailing\n   period.\n8. Write a body that includes:\n   - Summary of key changes (what changed).\n   - Rationale and trade-offs (why it changed).\n   - Tests or validation run (or explicit note if not run).\n9. Append a `Co-authored-by` trailer for Codex using `Codex <codex@openai.com>`\n   unless the user explicitly requests a different identity.\n10. Wrap body lines at 72 characters.\n11. Create the commit message with a here-doc or temp file and use\n    `git commit -F <file>` so newlines are literal (avoid `-m` with `\\n`).\n12. Commit only when the message matches the staged changes: if the staged diff\n    includes unrelated files or the message describes work that isn't staged,\n    fix the index or revise the message before committing.\n\n## Output\n\n- A single commit created with `git commit` whose message reflects the session.\n\n## Template\n\nType and scope are examples only; adjust to fit the repo and changes.\n\n```\n<type>(<scope>): <short summary>\n\nSummary:\n- <what changed>\n- <what changed>\n\nRationale:\n- <why>\n- <why>\n\nTests:\n- <command or \"not run (reason)\">\n\nCo-authored-by: Codex <codex@openai.com>\n```\n"
  },
  {
    "path": ".codex/skills/debug/SKILL.md",
    "content": "---\nname: debug\ndescription:\n  Investigate stuck runs and execution failures by tracing Symphony and Codex\n  logs with issue/session identifiers; use when runs stall, retry repeatedly, or\n  fail unexpectedly.\n---\n\n# Debug\n\n## Goals\n\n- Find why a run is stuck, retrying, or failing.\n- Correlate Linear issue identity to a Codex session quickly.\n- Read the right logs in the right order to isolate root cause.\n\n## Log Sources\n\n- Primary runtime log: `log/symphony.log`\n  - Default comes from `SymphonyElixir.LogFile` (`log/symphony.log`).\n  - Includes orchestrator, agent runner, and Codex app-server lifecycle logs.\n- Rotated runtime logs: `log/symphony.log*`\n  - Check these when the relevant run is older.\n\n## Correlation Keys\n\n- `issue_identifier`: human ticket key (example: `MT-625`)\n- `issue_id`: Linear UUID (stable internal ID)\n- `session_id`: Codex thread-turn pair (`<thread_id>-<turn_id>`)\n\n`elixir/docs/logging.md` requires these fields for issue/session lifecycle logs. Use\nthem as your join keys during debugging.\n\n## Quick Triage (Stuck Run)\n\n1. Confirm scheduler/worker symptoms for the ticket.\n2. Find recent lines for the ticket (`issue_identifier` first).\n3. Extract `session_id` from matching lines.\n4. Trace that `session_id` across start, stream, completion/failure, and stall\n   handling logs.\n5. Decide class of failure: timeout/stall, app-server startup failure, turn\n   failure, or orchestrator retry loop.\n\n## Commands\n\n```bash\n# 1) Narrow by ticket key (fastest entry point)\nrg -n \"issue_identifier=MT-625\" log/symphony.log*\n\n# 2) If needed, narrow by Linear UUID\nrg -n \"issue_id=<linear-uuid>\" log/symphony.log*\n\n# 3) Pull session IDs seen for that ticket\nrg -o \"session_id=[^ ;]+\" log/symphony.log* | sort -u\n\n# 4) Trace one session end-to-end\nrg -n \"session_id=<thread>-<turn>\" log/symphony.log*\n\n# 5) Focus on stuck/retry signals\nrg -n \"Issue stalled|scheduling retry|turn_timeout|turn_failed|Codex session failed|Codex session ended with error\" log/symphony.log*\n```\n\n## Investigation Flow\n\n1. Locate the ticket slice:\n    - Search by `issue_identifier=<KEY>`.\n    - If noise is high, add `issue_id=<UUID>`.\n2. Establish timeline:\n    - Identify first `Codex session started ... session_id=...`.\n    - Follow with `Codex session completed`, `ended with error`, or worker exit\n      lines.\n3. Classify the problem:\n    - Stall loop: `Issue stalled ... restarting with backoff`.\n    - App-server startup: `Codex session failed ...`.\n    - Turn execution failure: `turn_failed`, `turn_cancelled`, `turn_timeout`, or\n      `ended with error`.\n    - Worker crash: `Agent task exited ... reason=...`.\n4. Validate scope:\n    - Check whether failures are isolated to one issue/session or repeating across\n      multiple tickets.\n5. Capture evidence:\n    - Save key log lines with timestamps, `issue_identifier`, `issue_id`, and\n      `session_id`.\n    - Record probable root cause and the exact failing stage.\n\n## Reading Codex Session Logs\n\nIn Symphony, Codex session diagnostics are emitted into `log/symphony.log` and\nkeyed by `session_id`. Read them as a lifecycle:\n\n1. `Codex session started ... session_id=...`\n2. Session stream/lifecycle events for the same `session_id`\n3. Terminal event:\n    - `Codex session completed ...`, or\n    - `Codex session ended with error ...`, or\n    - `Issue stalled ... restarting with backoff`\n\nFor one specific session investigation, keep the trace narrow:\n\n1. Capture one `session_id` for the ticket.\n2. Build a timestamped slice for only that session:\n    - `rg -n \"session_id=<thread>-<turn>\" log/symphony.log*`\n3. Mark the exact failing stage:\n    - Startup failure before stream events (`Codex session failed ...`).\n    - Turn/runtime failure after stream events (`turn_*` / `ended with error`).\n    - Stall recovery (`Issue stalled ... restarting with backoff`).\n4. Pair findings with `issue_identifier` and `issue_id` from nearby lines to\n   confirm you are not mixing concurrent retries.\n\nAlways pair session findings with `issue_identifier`/`issue_id` to avoid mixing\nconcurrent runs.\n\n## Notes\n\n- Prefer `rg` over `grep` for speed on large logs.\n- Check rotated logs (`log/symphony.log*`) before concluding data is missing.\n- If required context fields are missing in new log statements, align with\n  `elixir/docs/logging.md` conventions.\n"
  },
  {
    "path": ".codex/skills/land/SKILL.md",
    "content": "---\nname: land\ndescription:\n  Land a PR by monitoring conflicts, resolving them, waiting for checks, and\n  squash-merging when green; use when asked to land, merge, or shepherd a PR to\n  completion.\n---\n\n# Land\n\n## Goals\n\n- Ensure the PR is conflict-free with main.\n- Keep CI green and fix failures when they occur.\n- Squash-merge the PR once checks pass.\n- Do not yield to the user until the PR is merged; keep the watcher loop running\n  unless blocked.\n- No need to delete remote branches after merge; the repo auto-deletes head\n  branches.\n\n## Preconditions\n\n- `gh` CLI is authenticated.\n- You are on the PR branch with a clean working tree.\n\n## Steps\n\n1. Locate the PR for the current branch.\n2. Confirm the full gauntlet is green locally before any push.\n3. If the working tree has uncommitted changes, commit with the `commit` skill\n   and push with the `push` skill before proceeding.\n4. Check mergeability and conflicts against main.\n5. If conflicts exist, use the `pull` skill to fetch/merge `origin/main` and\n   resolve conflicts, then use the `push` skill to publish the updated branch.\n6. Ensure Codex review comments (if present) are acknowledged and any required\n   fixes are handled before merging.\n7. Watch checks until complete.\n8. If checks fail, pull logs, fix the issue, commit with the `commit` skill,\n   push with the `push` skill, and re-run checks.\n9. When all checks are green and review feedback is addressed, squash-merge and\n   delete the branch using the PR title/body for the merge subject/body.\n10. **Context guard:** Before implementing review feedback, confirm it does not\n    conflict with the user’s stated intent or task context. If it conflicts,\n    respond inline with a justification and ask the user before changing code.\n11. **Pushback template:** When disagreeing, reply inline with: acknowledge +\n    rationale + offer alternative.\n12. **Ambiguity gate:** When ambiguity blocks progress, use the clarification\n    flow (assign PR to current GH user, mention them, wait for response). Do not\n    implement until ambiguity is resolved.\n    - If you are confident you know better than the reviewer, you may proceed\n      without asking the user, but reply inline with your rationale.\n13. **Per-comment mode:** For each review comment, choose one of: accept,\n    clarify, or push back. Reply inline (or in the issue thread for Codex\n    reviews) stating the mode before changing code.\n14. **Reply before change:** Always respond with intended action before pushing\n    code changes (inline for review comments, issue thread for Codex reviews).\n\n## Commands\n\n```\n# Ensure branch and PR context\nbranch=$(git branch --show-current)\npr_number=$(gh pr view --json number -q .number)\npr_title=$(gh pr view --json title -q .title)\npr_body=$(gh pr view --json body -q .body)\n\n# Check mergeability and conflicts\nmergeable=$(gh pr view --json mergeable -q .mergeable)\n\nif [ \"$mergeable\" = \"CONFLICTING\" ]; then\n  # Run the `pull` skill to handle fetch + merge + conflict resolution.\n  # Then run the `push` skill to publish the updated branch.\nfi\n\n# Preferred: use the Async Watch Helper below. The manual loop is a fallback\n# when Python cannot run or the helper script is unavailable.\n# Wait for review feedback: Codex reviews arrive as issue comments that start\n# with \"## Codex Review — <persona>\". Treat them like reviewer feedback: reply\n# with a `[codex]` issue comment acknowledging the findings and whether you're\n# addressing or deferring them.\nwhile true; do\n  gh api repos/{owner}/{repo}/issues/\"$pr_number\"/comments \\\n    --jq '.[] | select(.body | startswith(\"## Codex Review\")) | .id' | rg -q '.' \\\n    && break\n  sleep 10\ndone\n\n# Watch checks\nif ! gh pr checks --watch; then\n  gh pr checks\n  # Identify failing run and inspect logs\n  # gh run list --branch \"$branch\"\n  # gh run view <run-id> --log\n  exit 1\nfi\n\n# Squash-merge (remote branches auto-delete on merge in this repo)\ngh pr merge --squash --subject \"$pr_title\" --body \"$pr_body\"\n```\n\n## Async Watch Helper\n\nPreferred: use the asyncio watcher to monitor review comments, CI, and head\nupdates in parallel:\n\n```\npython3 .codex/skills/land/land_watch.py\n```\n\nExit codes:\n\n- 2: Review comments detected (address feedback)\n- 3: CI checks failed\n- 4: PR head updated (autofix commit detected)\n\n## Failure Handling\n\n- If checks fail, pull details with `gh pr checks` and `gh run view --log`, then\n  fix locally, commit with the `commit` skill, push with the `push` skill, and\n  re-run the watch.\n- Use judgment to identify flaky failures. If a failure is a flake (e.g., a\n  timeout on only one platform), you may proceed without fixing it.\n- If CI pushes an auto-fix commit (authored by GitHub Actions), it does not\n  trigger a fresh CI run. Detect the updated PR head, pull locally, merge\n  `origin/main` if needed, add a real author commit, and force-push to retrigger\n  CI, then restart the checks loop.\n- If all jobs fail with corrupted pnpm lockfile errors on the merge commit, the\n  remediation is to fetch latest `origin/main`, merge, force-push, and rerun CI.\n- If mergeability is `UNKNOWN`, wait and re-check.\n- Do not merge while review comments (human or Codex review) are outstanding.\n- Codex review jobs retry on failure and are non-blocking; use the presence of\n  `## Codex Review — <persona>` issue comments (not job status) as the signal\n  that review feedback is available.\n- Do not enable auto-merge; this repo has no required checks so auto-merge can\n  skip tests.\n- If the remote PR branch advanced due to your own prior force-push or merge,\n  avoid redundant merges; re-run the formatter locally if needed and\n  `git push --force-with-lease`.\n\n## Review Handling\n\n- Codex reviews now arrive as issue comments posted by GitHub Actions. They\n  start with `## Codex Review — <persona>` and include the reviewer’s\n  methodology + guardrails used. Treat these as feedback that must be\n  acknowledged before merge.\n- Human review comments are blocking and must be addressed (responded to and\n  resolved) before requesting a new review or merging.\n- If multiple reviewers comment in the same thread, respond to each comment\n  (batching is fine) before closing the thread.\n- Fetch review comments via `gh api` and reply with a prefixed comment.\n- Use review comment endpoints (not issue comments) to find inline feedback:\n  - List PR review comments:\n    ```\n    gh api repos/{owner}/{repo}/pulls/<pr_number>/comments\n    ```\n  - PR issue comments (top-level discussion):\n    ```\n    gh api repos/{owner}/{repo}/issues/<pr_number>/comments\n    ```\n  - Reply to a specific review comment:\n    ```\n    gh api -X POST /repos/{owner}/{repo}/pulls/<pr_number>/comments \\\n      -f body='[codex] <response>' -F in_reply_to=<comment_id>\n    ```\n- `in_reply_to` must be the numeric review comment id (e.g., `2710521800`), not\n  the GraphQL node id (e.g., `PRRC_...`), and the endpoint must include the PR\n  number (`/pulls/<pr_number>/comments`).\n- If GraphQL review reply mutation is forbidden, use REST.\n- A 404 on reply typically means the wrong endpoint (missing PR number) or\n  insufficient scope; verify by listing comments first.\n- All GitHub comments generated by this agent must be prefixed with `[codex]`.\n- For Codex review issue comments, reply in the issue thread (not a review\n  thread) with `[codex]` and state whether you will address the feedback now or\n  defer it (include rationale).\n- If feedback requires changes:\n  - For inline review comments (human), reply with intended fixes\n    (`[codex] ...`) **as an inline reply to the original review comment** using\n    the review comment endpoint and `in_reply_to` (do not use issue comments for\n    this).\n  - Implement fixes, commit, push.\n  - Reply with the fix details and commit sha (`[codex] ...`) in the same place\n    you acknowledged the feedback (issue comment for Codex reviews, inline reply\n    for review comments).\n  - The land watcher treats Codex review issue comments as unresolved until a\n    newer `[codex]` issue comment is posted acknowledging the findings.\n- Only request a new Codex review when you need a rerun (e.g., after new\n  commits). Do not request one without changes since the last review.\n  - Before requesting a new Codex review, re-run the land watcher and ensure\n    there are zero outstanding review comments (all have `[codex]` inline\n    replies).\n  - After pushing new commits, the Codex review workflow will rerun on PR\n    synchronization (or you can re-run the workflow manually). Post a concise\n    root-level summary comment so reviewers have the latest delta:\n    ```\n    [codex] Changes since last review:\n    - <short bullets of deltas>\n    Commits: <sha>, <sha>\n    Tests: <commands run>\n    ```\n  - Only request a new review if there is at least one new commit since the\n    previous request.\n  - Wait for the next Codex review comment before merging.\n\n## Scope + PR Metadata\n\n- The PR title and description should reflect the full scope of the change, not\n  just the most recent fix.\n- If review feedback expands scope, decide whether to include it now or defer\n  it. You can accept, defer, or decline feedback. If deferring or declining,\n  call it out in the root-level `[codex]` update with a brief reason (e.g.,\n  out-of-scope, conflicts with intent, unnecessary).\n- Correctness issues raised in review comments should be addressed. If you plan\n  to defer or decline a correctness concern, validate first and explain why the\n  concern does not apply.\n- Classify each review comment as one of: correctness, design, style,\n  clarification, scope.\n- For correctness feedback, provide concrete validation (test, log, or\n  reasoning) before closing it.\n- When accepting feedback, include a one-line rationale in the root-level\n  update.\n- When declining feedback, offer a brief alternative or follow-up trigger.\n- Prefer a single consolidated \"review addressed\" root-level comment after a\n  batch of fixes instead of many small updates.\n- For doc feedback, confirm the doc change matches behavior (no doc-only edits\n  to appease review).\n"
  },
  {
    "path": ".codex/skills/land/land_watch.py",
    "content": "#!/usr/bin/env python3\nimport asyncio\nimport json\nimport random\nimport re\nfrom dataclasses import dataclass\nfrom datetime import datetime\nfrom typing import Any\n\nPOLL_SECONDS = 10\nCHECKS_APPEAR_TIMEOUT_SECONDS = 120\nCODEX_BOTS = {\n    \"chatgpt-codex-connector[bot]\",\n    \"github-actions[bot]\",\n    \"codex-gc-app[bot]\",\n    \"app/codex-gc-app\",\n}\nMAX_GH_RETRIES = 5\nBASE_GH_BACKOFF_SECONDS = 2\n\n\n@dataclass\nclass PrInfo:\n    number: int\n    url: str\n    head_sha: str\n    mergeable: str | None\n    merge_state: str | None\n\n\nclass RateLimitError(RuntimeError):\n    pass\n\n\ndef is_rate_limit_error(error: str) -> bool:\n    return \"HTTP 429\" in error or \"rate limit\" in error.lower()\n\n\nasync def run_gh(*args: str) -> str:\n    max_delay = BASE_GH_BACKOFF_SECONDS * (2 ** (MAX_GH_RETRIES - 1))\n    delay_seconds = BASE_GH_BACKOFF_SECONDS\n    last_error = \"gh command failed\"\n    for attempt in range(1, MAX_GH_RETRIES + 1):\n        proc = await asyncio.create_subprocess_exec(\n            \"gh\",\n            *args,\n            stdout=asyncio.subprocess.PIPE,\n            stderr=asyncio.subprocess.PIPE,\n        )\n        stdout, stderr = await proc.communicate()\n        if proc.returncode == 0:\n            return stdout.decode()\n        error = stderr.decode().strip() or \"gh command failed\"\n        if not is_rate_limit_error(error):\n            raise RuntimeError(error)\n        last_error = error\n        if attempt >= MAX_GH_RETRIES:\n            break\n        jitter = random.uniform(0, delay_seconds)\n        await asyncio.sleep(min(delay_seconds + jitter, max_delay))\n        delay_seconds = min(delay_seconds * 2, max_delay)\n    raise RateLimitError(last_error)\n\n\nasync def get_pr_info() -> PrInfo:\n    data = await run_gh(\n        \"pr\",\n        \"view\",\n        \"--json\",\n        \"number,url,headRefOid,mergeable,mergeStateStatus\",\n    )\n    parsed = json.loads(data)\n    return PrInfo(\n        number=parsed[\"number\"],\n        url=parsed[\"url\"],\n        head_sha=parsed[\"headRefOid\"],\n        mergeable=parsed.get(\"mergeable\"),\n        merge_state=parsed.get(\"mergeStateStatus\"),\n    )\n\n\nasync def get_paginated_list(endpoint: str) -> list[dict[str, Any]]:\n    page = 1\n    items: list[dict[str, Any]] = []\n    while True:\n        data = await run_gh(\n            \"api\",\n            \"--method\",\n            \"GET\",\n            endpoint,\n            \"-f\",\n            \"per_page=100\",\n            \"-f\",\n            f\"page={page}\",\n        )\n        batch = json.loads(data)\n        if not batch:\n            break\n        items.extend(batch)\n        page += 1\n    return items\n\n\nasync def get_issue_comments(pr_number: int) -> list[dict[str, Any]]:\n    return await get_paginated_list(\n        f\"repos/{{owner}}/{{repo}}/issues/{pr_number}/comments\",\n    )\n\n\nasync def get_review_comments(pr_number: int) -> list[dict[str, Any]]:\n    return await get_paginated_list(\n        f\"repos/{{owner}}/{{repo}}/pulls/{pr_number}/comments\",\n    )\n\n\nasync def get_reviews(pr_number: int) -> list[dict[str, Any]]:\n    page = 1\n    reviews: list[dict[str, Any]] = []\n    while True:\n        data = await run_gh(\n            \"api\",\n            \"--method\",\n            \"GET\",\n            f\"repos/{{owner}}/{{repo}}/pulls/{pr_number}/reviews\",\n            \"-f\",\n            \"per_page=100\",\n            \"-f\",\n            f\"page={page}\",\n        )\n        batch = json.loads(data)\n        if not batch:\n            break\n        reviews.extend(batch)\n        page += 1\n    return reviews\n\n\nasync def get_check_runs(head_sha: str) -> list[dict[str, Any]]:\n    page = 1\n    check_runs: list[dict[str, Any]] = []\n    while True:\n        data = await run_gh(\n            \"api\",\n            \"--method\",\n            \"GET\",\n            f\"repos/{{owner}}/{{repo}}/commits/{head_sha}/check-runs\",\n            \"-f\",\n            \"per_page=100\",\n            \"-f\",\n            f\"page={page}\",\n        )\n        payload = json.loads(data)\n        batch = payload.get(\"check_runs\", [])\n        if not batch:\n            break\n        check_runs.extend(batch)\n        total_count = payload.get(\"total_count\")\n        if total_count is not None and len(check_runs) >= total_count:\n            break\n        page += 1\n    return check_runs\n\n\ndef parse_time(value: str) -> datetime:\n    normalized = value.replace(\"Z\", \"+00:00\")\n    return datetime.fromisoformat(normalized)\n\n\nCONTROL_CHARS_RE = re.compile(r\"[\\x00-\\x08\\x0b-\\x1f\\x7f-\\x9f]\")\n\n\ndef sanitize_terminal_output(value: str) -> str:\n    return CONTROL_CHARS_RE.sub(\"\", value)\n\n\ndef check_timestamp(check: dict[str, Any]) -> datetime | None:\n    for key in (\"completed_at\", \"started_at\", \"run_started_at\", \"created_at\"):\n        value = check.get(key)\n        if value:\n            return parse_time(value)\n    return None\n\n\ndef dedupe_check_runs(check_runs: list[dict[str, Any]]) -> list[dict[str, Any]]:\n    latest_by_name: dict[str, dict[str, Any]] = {}\n    for check in check_runs:\n        name = check.get(\"name\", \"unknown\")\n        timestamp = check_timestamp(check)\n        if name not in latest_by_name:\n            latest_by_name[name] = check\n            continue\n        existing = latest_by_name[name]\n        existing_timestamp = check_timestamp(existing)\n        if timestamp is None:\n            continue\n        if existing_timestamp is None or timestamp > existing_timestamp:\n            latest_by_name[name] = check\n    return list(latest_by_name.values())\n\n\ndef summarize_checks(check_runs: list[dict[str, Any]]) -> tuple[bool, bool, list[str]]:\n    if not check_runs:\n        return True, False, [\"no checks reported\"]\n    check_runs = dedupe_check_runs(check_runs)\n    pending = False\n    failed = False\n    failures: list[str] = []\n    for check in check_runs:\n        status = check.get(\"status\")\n        conclusion = check.get(\"conclusion\")\n        name = check.get(\"name\", \"unknown\")\n        if status != \"completed\":\n            pending = True\n            continue\n        if conclusion not in (\"success\", \"skipped\", \"neutral\"):\n            failed = True\n            failures.append(f\"{name}: {conclusion}\")\n    return pending, failed, failures\n\n\ndef latest_review_request_at(comments: list[dict[str, Any]]) -> datetime | None:\n    latest: datetime | None = None\n    for comment in comments:\n        if is_codex_bot_user(comment.get(\"user\", {})):\n            continue\n        body = comment.get(\"body\") or \"\"\n        if \"@codex review\" not in body:\n            continue\n        timestamp = comment_time(comment)\n        if timestamp is None:\n            continue\n        if latest is None or timestamp > latest:\n            latest = timestamp\n    return latest\n\n\ndef filter_codex_comments(\n    comments: list[dict[str, Any]],\n    review_requested_at: datetime | None,\n) -> list[dict[str, Any]]:\n    latest_codex_reply = latest_codex_reply_by_thread(comments)\n    latest_issue_ack = latest_codex_issue_reply_time(comments)\n    codex_comments = [c for c in comments if is_codex_bot_user(c.get(\"user\", {}))]\n    filtered: list[dict[str, Any]] = []\n    for comment in codex_comments:\n        created_time = comment_time(comment)\n        if created_time is None:\n            continue\n        if review_requested_at is not None and created_time <= review_requested_at:\n            continue\n        is_threaded = bool(\n            comment.get(\"in_reply_to_id\") or comment.get(\"pull_request_review_id\")\n        )\n        if not is_threaded:\n            if latest_issue_ack is not None and created_time <= latest_issue_ack:\n                continue\n        else:\n            thread_root = thread_root_id(comment)\n            last_reply = None\n            if thread_root is not None:\n                last_reply = latest_codex_reply.get(thread_root)\n            if last_reply and last_reply > created_time:\n                continue\n        filtered.append(comment)\n    return filtered\n\n\ndef is_codex_bot_user(user: dict[str, Any]) -> bool:\n    login = user.get(\"login\") or \"\"\n    return login in CODEX_BOTS\n\n\ndef is_bot_user(user: dict[str, Any]) -> bool:\n    login = user.get(\"login\") or \"\"\n    if is_codex_bot_user(user):\n        return True\n    if user.get(\"type\") == \"Bot\":\n        return True\n    return login.endswith(\"[bot]\")\n\n\ndef is_codex_reply_body(body: str) -> bool:\n    return body.startswith(\"[codex]\")\n\n\ndef is_codex_review_body(body: str) -> bool:\n    return body.startswith(\"## Codex Review\")\n\n\ndef latest_codex_issue_reply_time(\n    comments: list[dict[str, Any]],\n) -> datetime | None:\n    latest: datetime | None = None\n    for comment in comments:\n        body = (comment.get(\"body\") or \"\").strip()\n        if not is_codex_reply_body(body):\n            continue\n        created_time = comment_time(comment)\n        if created_time is None:\n            continue\n        if latest is None or created_time > latest:\n            latest = created_time\n    return latest\n\n\ndef filter_human_issue_comments(comments: list[dict[str, Any]]) -> list[dict[str, Any]]:\n    latest_ack = latest_codex_issue_reply_time(comments)\n    filtered: list[dict[str, Any]] = []\n    for comment in comments:\n        if is_bot_user(comment.get(\"user\", {})):\n            continue\n        body = (comment.get(\"body\") or \"\").strip()\n        if is_codex_reply_body(body):\n            continue\n        if is_codex_review_body(body):\n            continue\n        if \"@codex review\" in body:\n            continue\n        created_time = comment_time(comment)\n        if (\n            latest_ack is not None\n            and created_time is not None\n            and created_time <= latest_ack\n        ):\n            continue\n        filtered.append(comment)\n    return filtered\n\n\ndef filter_codex_review_issue_comments(\n    comments: list[dict[str, Any]],\n) -> list[dict[str, Any]]:\n    latest_ack = latest_codex_issue_reply_time(comments)\n    filtered: list[dict[str, Any]] = []\n    for comment in comments:\n        body = (comment.get(\"body\") or \"\").strip()\n        if not is_codex_review_body(body):\n            continue\n        created_time = comment_time(comment)\n        if (\n            latest_ack is not None\n            and created_time is not None\n            and created_time <= latest_ack\n        ):\n            continue\n        filtered.append(comment)\n    return filtered\n\n\ndef thread_root_id(comment: dict[str, Any]) -> int | None:\n    return comment.get(\"in_reply_to_id\") or comment.get(\"id\")\n\n\ndef comment_time(comment: dict[str, Any]) -> datetime | None:\n    timestamp = comment.get(\"updated_at\") or comment.get(\"created_at\")\n    if not timestamp:\n        return None\n    return parse_time(timestamp)\n\n\ndef latest_codex_reply_by_thread(\n    comments: list[dict[str, Any]],\n) -> dict[int, datetime]:\n    latest: dict[int, datetime] = {}\n    for comment in comments:\n        body = (comment.get(\"body\") or \"\").strip()\n        if not is_codex_reply_body(body):\n            continue\n        thread_root = thread_root_id(comment)\n        created_time = comment_time(comment)\n        if thread_root is None or created_time is None:\n            continue\n        existing = latest.get(thread_root)\n        if existing is None or created_time > existing:\n            latest[thread_root] = created_time\n    return latest\n\n\ndef filter_human_review_comments(\n    comments: list[dict[str, Any]],\n) -> list[dict[str, Any]]:\n    latest_codex_reply = latest_codex_reply_by_thread(comments)\n    filtered: list[dict[str, Any]] = []\n    for comment in comments:\n        if is_bot_user(comment.get(\"user\", {})):\n            continue\n        body = (comment.get(\"body\") or \"\").strip()\n        if is_codex_reply_body(body):\n            continue\n        thread_root = thread_root_id(comment)\n        created_time = comment_time(comment)\n        last_codex_reply = None\n        if thread_root is not None:\n            last_codex_reply = latest_codex_reply.get(thread_root)\n        if last_codex_reply and created_time and created_time <= last_codex_reply:\n            continue\n        filtered.append(comment)\n    return filtered\n\n\ndef is_blocking_review(\n    review: dict[str, Any],\n    review_requested_at: datetime | None,\n) -> bool:\n    created_at = review.get(\"submitted_at\") or review.get(\"created_at\")\n    if not created_at:\n        return False\n    user_login = review.get(\"user\", {}).get(\"login\")\n    created_time = parse_time(created_at)\n    if (\n        user_login in CODEX_BOTS\n        and review_requested_at is not None\n        and created_time <= review_requested_at\n    ):\n        return False\n    body = (review.get(\"body\") or \"\").strip()\n    state = review.get(\"state\")\n    if user_login in CODEX_BOTS:\n        return state == \"CHANGES_REQUESTED\"\n    if body.startswith(\"[codex]\") or state in (\"APPROVED\", \"DISMISSED\"):\n        return False\n    blocking = False\n    if body or state == \"CHANGES_REQUESTED\":\n        blocking = True\n    elif state == \"COMMENTED\":\n        blocking = False\n    elif state:\n        blocking = state not in (\"APPROVED\", \"DISMISSED\")\n    return blocking\n\n\ndef review_timestamp(review: dict[str, Any]) -> datetime | None:\n    created_at = review.get(\"submitted_at\") or review.get(\"created_at\")\n    if not created_at:\n        return None\n    return parse_time(created_at)\n\n\ndef dedupe_reviews(reviews: list[dict[str, Any]]) -> list[dict[str, Any]]:\n    latest_by_user: dict[str, dict[str, Any]] = {}\n    for review in reviews:\n        user_login = review.get(\"user\", {}).get(\"login\")\n        if not user_login:\n            continue\n        timestamp = review_timestamp(review)\n        if user_login not in latest_by_user:\n            latest_by_user[user_login] = review\n            continue\n        existing = latest_by_user[user_login]\n        existing_timestamp = review_timestamp(existing)\n        if timestamp is None:\n            continue\n        if existing_timestamp is None or timestamp > existing_timestamp:\n            latest_by_user[user_login] = review\n    return list(latest_by_user.values())\n\n\ndef filter_blocking_reviews(\n    reviews: list[dict[str, Any]],\n    review_requested_at: datetime | None,\n) -> list[dict[str, Any]]:\n    return [\n        review\n        for review in dedupe_reviews(reviews)\n        if is_blocking_review(review, review_requested_at)\n    ]\n\n\ndef is_merge_conflicting(pr: PrInfo) -> bool:\n    return pr.mergeable == \"CONFLICTING\" or pr.merge_state == \"DIRTY\"\n\n\nasync def fetch_review_context(\n    pr_number: int,\n) -> tuple[\n    list[dict[str, Any]],\n    list[dict[str, Any]],\n    list[dict[str, Any]],\n    datetime | None,\n]:\n    issue_comments = await get_issue_comments(pr_number)\n    review_request_at = latest_review_request_at(issue_comments)\n    review_comments = await get_review_comments(pr_number)\n    reviews = await get_reviews(pr_number)\n    return issue_comments, review_comments, reviews, review_request_at\n\n\ndef raise_on_human_feedback(\n    issue_comments: list[dict[str, Any]],\n    review_comments: list[dict[str, Any]],\n    reviews: list[dict[str, Any]],\n    review_request_at: datetime | None,\n) -> None:\n    human_issue_comments = filter_human_issue_comments(issue_comments)\n    codex_review_comments = filter_codex_review_issue_comments(issue_comments)\n    human_review_comments = filter_human_review_comments(review_comments)\n    if human_issue_comments or human_review_comments or codex_review_comments:\n        print(\"Review comments detected. Address before merge.\")\n        print(\n            \"Reminder: decide whether feedback stays in scope; defer if needed \"\n            \"and note in your root-level update.\",\n        )\n        raise SystemExit(2)\n    blocking_reviews = filter_blocking_reviews(reviews, review_request_at)\n    if blocking_reviews:\n        print(\"Review states/comments detected. Address before merge.\")\n        print(\n            \"Reminder: keep PR title/description aligned with the full scope \"\n            \"when changes expand.\",\n        )\n        raise SystemExit(2)\n\n\nasync def wait_for_codex(pr_number: int, checks_done: asyncio.Event) -> None:\n    print(\"Waiting for review feedback...\", flush=True)\n    while True:\n        (\n            issue_comments,\n            review_comments,\n            reviews,\n            review_request_at,\n        ) = await fetch_review_context(pr_number)\n        bot_issue_comments = filter_codex_comments(issue_comments, review_request_at)\n        bot_review_comments = filter_codex_comments(review_comments, review_request_at)\n        bot_comments = bot_issue_comments + bot_review_comments\n        raise_on_human_feedback(\n            issue_comments,\n            review_comments,\n            reviews,\n            review_request_at,\n        )\n        if bot_comments:\n            latest = max(\n                bot_comments,\n                key=lambda comment: parse_time(comment[\"created_at\"]),\n            )\n            body = sanitize_terminal_output(latest.get(\"body\") or \"\").strip()\n            if body:\n                print(\"Codex left comments. Address feedback before merge.\")\n                print(body)\n                raise SystemExit(2)\n        if checks_done.is_set():\n            return\n        await asyncio.sleep(POLL_SECONDS)\n\n\nasync def wait_for_checks(head_sha: str, checks_done: asyncio.Event) -> None:\n    print(\"Waiting for CI checks...\", flush=True)\n    empty_seconds = 0\n    while True:\n        check_runs = await get_check_runs(head_sha)\n        if not check_runs:\n            empty_seconds += POLL_SECONDS\n            if empty_seconds >= CHECKS_APPEAR_TIMEOUT_SECONDS:\n                print(\n                    \"No checks detected after 120s; check CI configuration\",\n                )\n                raise SystemExit(3)\n            await asyncio.sleep(POLL_SECONDS)\n            continue\n        empty_seconds = 0\n        pending, failed, failures = summarize_checks(check_runs)\n        if failed:\n            print(\"Checks failed:\")\n            for failure in failures:\n                print(f\"- {failure}\")\n            raise SystemExit(3)\n        if not pending:\n            print(\"Checks passed\")\n            checks_done.set()\n            return\n        await asyncio.sleep(POLL_SECONDS)\n\n\nasync def watch_pr() -> None:\n    pr = await get_pr_info()\n    if is_merge_conflicting(pr):\n        print(\n            \"PR has merge conflicts. Resolve/rebase against main and push before \"\n            \"running land_watch again.\",\n        )\n        raise SystemExit(5)\n    head_sha = pr.head_sha\n    checks_done = asyncio.Event()\n    codex_task = asyncio.create_task(wait_for_codex(pr.number, checks_done))\n    checks_task = asyncio.create_task(wait_for_checks(head_sha, checks_done))\n\n    async def head_monitor() -> None:\n        while True:\n            current = await get_pr_info()\n            if is_merge_conflicting(current):\n                print(\n                    \"PR has merge conflicts. Resolve/rebase against main and push \"\n                    \"before running land_watch again.\",\n                )\n                raise SystemExit(5)\n            if current.head_sha != head_sha:\n                print(\"PR head updated; pull/amend/force-push to retrigger CI\")\n                raise SystemExit(4)\n            await asyncio.sleep(POLL_SECONDS)\n\n    monitor_task = asyncio.create_task(head_monitor())\n    success_task = asyncio.gather(codex_task, checks_task)\n\n    done, pending = await asyncio.wait(\n        [monitor_task, success_task],\n        return_when=asyncio.FIRST_COMPLETED,\n    )\n    for task in pending:\n        task.cancel()\n    for task in done:\n        exc = task.exception()\n        if exc:\n            raise exc\n\n\nif __name__ == \"__main__\":\n    try:\n        asyncio.run(watch_pr())\n    except SystemExit as exc:\n        raise SystemExit(exc.code) from None\n"
  },
  {
    "path": ".codex/skills/linear/SKILL.md",
    "content": "---\nname: linear\ndescription: |\n  Use Symphony's `linear_graphql` client tool for raw Linear GraphQL\n  operations such as comment editing and upload flows.\n---\n\n# Linear GraphQL\n\nUse this skill for raw Linear GraphQL work during Symphony app-server sessions.\n\n## Primary tool\n\nUse the `linear_graphql` client tool exposed by Symphony's app-server session.\nIt reuses Symphony's configured Linear auth for the session.\n\nTool input:\n\n```json\n{\n  \"query\": \"query or mutation document\",\n  \"variables\": {\n    \"optional\": \"graphql variables object\"\n  }\n}\n```\n\nTool behavior:\n\n- Send one GraphQL operation per tool call.\n- Treat a top-level `errors` array as a failed GraphQL operation even if the\n  tool call itself completed.\n- Keep queries/mutations narrowly scoped; ask only for the fields you need.\n\n## Discovering unfamiliar operations\n\nWhen you need an unfamiliar mutation, input type, or object field, use targeted\nintrospection through `linear_graphql`.\n\nList mutation names:\n\n```graphql\nquery ListMutations {\n  __type(name: \"Mutation\") {\n    fields {\n      name\n    }\n  }\n}\n```\n\nInspect a specific input object:\n\n```graphql\nquery CommentCreateInputShape {\n  __type(name: \"CommentCreateInput\") {\n    inputFields {\n      name\n      type {\n        kind\n        name\n        ofType {\n          kind\n          name\n        }\n      }\n    }\n  }\n}\n```\n\n## Common workflows\n\n### Query an issue by key, identifier, or id\n\nUse these progressively:\n\n- Start with `issue(id: $key)` when you have a ticket key such as `MT-686`.\n- Fall back to `issues(filter: ...)` when you need identifier search semantics.\n- Once you have the internal issue id, prefer `issue(id: $id)` for narrower reads.\n\nLookup by issue key:\n\n```graphql\nquery IssueByKey($key: String!) {\n  issue(id: $key) {\n    id\n    identifier\n    title\n    state {\n      id\n      name\n      type\n    }\n    project {\n      id\n      name\n    }\n    branchName\n    url\n    description\n    updatedAt\n    links {\n      nodes {\n        id\n        url\n        title\n      }\n    }\n  }\n}\n```\n\nLookup by identifier filter:\n\n```graphql\nquery IssueByIdentifier($identifier: String!) {\n  issues(filter: { identifier: { eq: $identifier } }, first: 1) {\n    nodes {\n      id\n      identifier\n      title\n      state {\n        id\n        name\n        type\n      }\n      project {\n        id\n        name\n      }\n      branchName\n      url\n      description\n      updatedAt\n    }\n  }\n}\n```\n\nResolve a key to an internal id:\n\n```graphql\nquery IssueByIdOrKey($id: String!) {\n  issue(id: $id) {\n    id\n    identifier\n    title\n  }\n}\n```\n\nRead the issue once the internal id is known:\n\n```graphql\nquery IssueDetails($id: String!) {\n  issue(id: $id) {\n    id\n    identifier\n    title\n    url\n    description\n    state {\n      id\n      name\n      type\n    }\n    project {\n      id\n      name\n    }\n    attachments {\n      nodes {\n        id\n        title\n        url\n        sourceType\n      }\n    }\n  }\n}\n```\n\n### Query team workflow states for an issue\n\nUse this before changing issue state when you need the exact `stateId`:\n\n```graphql\nquery IssueTeamStates($id: String!) {\n  issue(id: $id) {\n    id\n    team {\n      id\n      key\n      name\n      states {\n        nodes {\n          id\n          name\n          type\n        }\n      }\n    }\n  }\n}\n```\n\n### Edit an existing comment\n\nUse `commentUpdate` through `linear_graphql`:\n\n```graphql\nmutation UpdateComment($id: String!, $body: String!) {\n  commentUpdate(id: $id, input: { body: $body }) {\n    success\n    comment {\n      id\n      body\n    }\n  }\n}\n```\n\n### Create a comment\n\nUse `commentCreate` through `linear_graphql`:\n\n```graphql\nmutation CreateComment($issueId: String!, $body: String!) {\n  commentCreate(input: { issueId: $issueId, body: $body }) {\n    success\n    comment {\n      id\n      url\n    }\n  }\n}\n```\n\n### Move an issue to a different state\n\nUse `issueUpdate` with the destination `stateId`:\n\n```graphql\nmutation MoveIssueToState($id: String!, $stateId: String!) {\n  issueUpdate(id: $id, input: { stateId: $stateId }) {\n    success\n    issue {\n      id\n      identifier\n      state {\n        id\n        name\n      }\n    }\n  }\n}\n```\n\n### Attach a GitHub PR to an issue\n\nUse the GitHub-specific attachment mutation when linking a PR:\n\n```graphql\nmutation AttachGitHubPR($issueId: String!, $url: String!, $title: String) {\n  attachmentLinkGitHubPR(\n    issueId: $issueId\n    url: $url\n    title: $title\n    linkKind: links\n  ) {\n    success\n    attachment {\n      id\n      title\n      url\n    }\n  }\n}\n```\n\nIf you only need a plain URL attachment and do not care about GitHub-specific\nlink metadata, use:\n\n```graphql\nmutation AttachURL($issueId: String!, $url: String!, $title: String) {\n  attachmentLinkURL(issueId: $issueId, url: $url, title: $title) {\n    success\n    attachment {\n      id\n      title\n      url\n    }\n  }\n}\n```\n\n### Introspection patterns used during schema discovery\n\nUse these when the exact field or mutation shape is unclear:\n\n```graphql\nquery QueryFields {\n  __type(name: \"Query\") {\n    fields {\n      name\n    }\n  }\n}\n```\n\n```graphql\nquery IssueFieldArgs {\n  __type(name: \"Query\") {\n    fields {\n      name\n      args {\n        name\n        type {\n          kind\n          name\n          ofType {\n            kind\n            name\n            ofType {\n              kind\n              name\n            }\n          }\n        }\n      }\n    }\n  }\n}\n```\n\n### Upload a video to a comment\n\nDo this in three steps:\n\n1. Call `linear_graphql` with `fileUpload` to get `uploadUrl`, `assetUrl`, and\n   any required upload headers.\n2. Upload the local file bytes to `uploadUrl` with `curl -X PUT` and the exact\n   headers returned by `fileUpload`.\n3. Call `linear_graphql` again with `commentCreate` (or `commentUpdate`) and\n   include the resulting `assetUrl` in the comment body.\n\nUseful mutations:\n\n```graphql\nmutation FileUpload(\n  $filename: String!\n  $contentType: String!\n  $size: Int!\n  $makePublic: Boolean\n) {\n  fileUpload(\n    filename: $filename\n    contentType: $contentType\n    size: $size\n    makePublic: $makePublic\n  ) {\n    success\n    uploadFile {\n      uploadUrl\n      assetUrl\n      headers {\n        key\n        value\n      }\n    }\n  }\n}\n```\n\n## Usage rules\n\n- Use `linear_graphql` for comment edits, uploads, and ad-hoc Linear API\n  queries.\n- Prefer the narrowest issue lookup that matches what you already know:\n  key -> identifier search -> internal id.\n- For state transitions, fetch team states first and use the exact `stateId`\n  instead of hardcoding names inside mutations.\n- Prefer `attachmentLinkGitHubPR` over a generic URL attachment when linking a\n  GitHub PR to a Linear issue.\n- Do not introduce new raw-token shell helpers for GraphQL access.\n- If you need shell work for uploads, only use it for signed upload URLs\n  returned by `fileUpload`; those URLs already carry the needed authorization.\n"
  },
  {
    "path": ".codex/skills/pull/SKILL.md",
    "content": "---\nname: pull\ndescription:\n  Pull latest origin/main into the current local branch and resolve merge\n  conflicts (aka update-branch). Use when Codex needs to sync a feature branch\n  with origin, perform a merge-based update (not rebase), and guide conflict\n  resolution best practices.\n---\n\n# Pull\n\n## Workflow\n\n1. Verify git status is clean or commit/stash changes before merging.\n2. Ensure rerere is enabled locally:\n   - `git config rerere.enabled true`\n   - `git config rerere.autoupdate true`\n3. Confirm remotes and branches:\n   - Ensure the `origin` remote exists.\n   - Ensure the current branch is the one to receive the merge.\n4. Fetch latest refs:\n   - `git fetch origin`\n5. Sync the remote feature branch first:\n   - `git pull --ff-only origin $(git branch --show-current)`\n   - This pulls branch updates made remotely (for example, a GitHub auto-commit)\n     before merging `origin/main`.\n6. Merge in order:\n   - Prefer `git -c merge.conflictstyle=zdiff3 merge origin/main` for clearer\n     conflict context.\n7. If conflicts appear, resolve them (see conflict guidance below), then:\n   - `git add <files>`\n   - `git commit` (or `git merge --continue` if the merge is paused)\n8. Verify with project checks (follow repo policy in `AGENTS.md`).\n9. Summarize the merge:\n   - Call out the most challenging conflicts/files and how they were resolved.\n   - Note any assumptions or follow-ups.\n\n## Conflict Resolution Guidance (Best Practices)\n\n- Inspect context before editing:\n  - Use `git status` to list conflicted files.\n  - Use `git diff` or `git diff --merge` to see conflict hunks.\n  - Use `git diff :1:path/to/file :2:path/to/file` and\n    `git diff :1:path/to/file :3:path/to/file` to compare base vs ours/theirs\n    for a file-level view of intent.\n  - With `merge.conflictstyle=zdiff3`, conflict markers include:\n    - `<<<<<<<` ours, `|||||||` base, `=======` split, `>>>>>>>` theirs.\n    - Matching lines near the start/end are trimmed out of the conflict region,\n      so focus on the differing core.\n  - Summarize the intent of both changes, decide the semantically correct\n    outcome, then edit:\n    - State what each side is trying to achieve (bug fix, refactor, rename,\n      behavior change).\n    - Identify the shared goal, if any, and whether one side supersedes the\n      other.\n    - Decide the final behavior first; only then craft the code to match that\n      decision.\n    - Prefer preserving invariants, API contracts, and user-visible behavior\n      unless the conflict clearly indicates a deliberate change.\n  - Open files and understand intent on both sides before choosing a resolution.\n- Prefer minimal, intention-preserving edits:\n  - Keep behavior consistent with the branch’s purpose.\n  - Avoid accidental deletions or silent behavior changes.\n- Resolve one file at a time and rerun tests after each logical batch.\n- Use `ours/theirs` only when you are certain one side should win entirely.\n- For complex conflicts, search for related files or definitions to align with\n  the rest of the codebase.\n- For generated files, resolve non-generated conflicts first, then regenerate:\n  - Prefer resolving source files and handwritten logic before touching\n    generated artifacts.\n  - Run the CLI/tooling command that produced the generated file to recreate it\n    cleanly, then stage the regenerated output.\n- For import conflicts where intent is unclear, accept both sides first:\n  - Keep all candidate imports temporarily, finish the merge, then run lint/type\n    checks to remove unused or incorrect imports safely.\n- After resolving, ensure no conflict markers remain:\n  - `git diff --check`\n- When unsure, note assumptions and ask for confirmation before finalizing the\n  merge.\n\n## When To Ask The User (Keep To A Minimum)\n\nDo not ask for input unless there is no safe, reversible alternative. Prefer\nmaking a best-effort decision, documenting the rationale, and proceeding.\n\nAsk the user only when:\n\n- The correct resolution depends on product intent or behavior not inferable\n  from code, tests, or nearby documentation.\n- The conflict crosses a user-visible contract, API surface, or migration where\n  choosing incorrectly could break external consumers.\n- A conflict requires selecting between two mutually exclusive designs with\n  equivalent technical merit and no clear local signal.\n- The merge introduces data loss, schema changes, or irreversible side effects\n  without an obvious safe default.\n- The branch is not the intended target, or the remote/branch names do not exist\n  and cannot be determined locally.\n\nOtherwise, proceed with the merge, explain the decision briefly in notes, and\nleave a clear, reviewable commit history.\n"
  },
  {
    "path": ".codex/skills/push/SKILL.md",
    "content": "---\nname: push\ndescription:\n  Push current branch changes to origin and create or update the corresponding\n  pull request; use when asked to push, publish updates, or create pull request.\n---\n\n# Push\n\n## Prerequisites\n\n- `gh` CLI is installed and available in `PATH`.\n- `gh auth status` succeeds for GitHub operations in this repo.\n\n## Goals\n\n- Push current branch changes to `origin` safely.\n- Create a PR if none exists for the branch, otherwise update the existing PR.\n- Keep branch history clean when remote has moved.\n\n## Related Skills\n\n- `pull`: use this when push is rejected or sync is not clean (non-fast-forward,\n  merge conflict risk, or stale branch).\n\n## Steps\n\n1. Identify current branch and confirm remote state.\n2. Run local validation (`make -C elixir all`) before pushing.\n3. Push branch to `origin` with upstream tracking if needed, using whatever\n   remote URL is already configured.\n4. If push is not clean/rejected:\n   - If the failure is a non-fast-forward or sync problem, run the `pull`\n     skill to merge `origin/main`, resolve conflicts, and rerun validation.\n   - Push again; use `--force-with-lease` only when history was rewritten.\n   - If the failure is due to auth, permissions, or workflow restrictions on\n     the configured remote, stop and surface the exact error instead of\n     rewriting remotes or switching protocols as a workaround.\n\n5. Ensure a PR exists for the branch:\n   - If no PR exists, create one.\n   - If a PR exists and is open, update it.\n   - If branch is tied to a closed/merged PR, create a new branch + PR.\n   - Write a proper PR title that clearly describes the change outcome\n   - For branch updates, explicitly reconsider whether current PR title still\n     matches the latest scope; update it if it no longer does.\n6. Write/update PR body explicitly using `.github/pull_request_template.md`:\n   - Fill every section with concrete content for this change.\n   - Replace all placeholder comments (`<!-- ... -->`).\n   - Keep bullets/checkboxes where template expects them.\n   - If PR already exists, refresh body content so it reflects the total PR\n     scope (all intended work on the branch), not just the newest commits,\n     including newly added work, removed work, or changed approach.\n   - Do not reuse stale description text from earlier iterations.\n7. Validate PR body with `mix pr_body.check` and fix all reported issues.\n8. Reply with the PR URL from `gh pr view`.\n\n## Commands\n\n```sh\n# Identify branch\nbranch=$(git branch --show-current)\n\n# Minimal validation gate\nmake -C elixir all\n\n# Initial push: respect the current origin remote.\ngit push -u origin HEAD\n\n# If that failed because the remote moved, use the pull skill. After\n# pull-skill resolution and re-validation, retry the normal push:\ngit push -u origin HEAD\n\n# If the configured remote rejects the push for auth, permissions, or workflow\n# restrictions, stop and surface the exact error.\n\n# Only if history was rewritten locally:\ngit push --force-with-lease origin HEAD\n\n# Ensure a PR exists (create only if missing)\npr_state=$(gh pr view --json state -q .state 2>/dev/null || true)\nif [ \"$pr_state\" = \"MERGED\" ] || [ \"$pr_state\" = \"CLOSED\" ]; then\n  echo \"Current branch is tied to a closed PR; create a new branch + PR.\" >&2\n  exit 1\nfi\n\n# Write a clear, human-friendly title that summarizes the shipped change.\npr_title=\"<clear PR title written for this change>\"\nif [ -z \"$pr_state\" ]; then\n  gh pr create --title \"$pr_title\"\nelse\n  # Reconsider title on every branch update; edit if scope shifted.\n  gh pr edit --title \"$pr_title\"\nfi\n\n# Write/edit PR body to match .github/pull_request_template.md before validation.\n# Example workflow:\n# 1) open the template and draft body content for this PR\n# 2) gh pr edit --body-file /tmp/pr_body.md\n# 3) for branch updates, re-check that title/body still match current diff\n\ntmp_pr_body=$(mktemp)\ngh pr view --json body -q .body > \"$tmp_pr_body\"\n(cd elixir && mix pr_body.check --file \"$tmp_pr_body\")\nrm -f \"$tmp_pr_body\"\n\n# Show PR URL for the reply\ngh pr view --json url -q .url\n```\n\n## Notes\n\n- Do not use `--force`; only use `--force-with-lease` as the last resort.\n- Distinguish sync problems from remote auth/permission problems:\n  - Use the `pull` skill for non-fast-forward or stale-branch issues.\n  - Surface auth, permissions, or workflow restrictions directly instead of\n    changing remotes or protocols.\n"
  },
  {
    "path": ".codex/worktree_init.sh",
    "content": "#!/usr/bin/env bash\nset -eo pipefail\n\nscript_dir=\"$(cd \"$(dirname \"$0\")\" && pwd)\"\nrepo_root=\"$(cd \"$script_dir/..\" && pwd)\"\nproject_root=\"$repo_root/elixir\"\n\nif ! command -v mise >/dev/null 2>&1; then\n  echo \"mise is required. Install it from https://mise.jdx.dev/getting-started.html\" >&2\n  exit 1\nfi\n\ncd \"$project_root\"\nmise trust\n\nmake setup\n"
  },
  {
    "path": ".github/pull_request_template.md",
    "content": "#### Context\n\n<!-- Why is this change needed? Length <= 240 chars -->\n\n#### TL;DR\n\n*<!-- A short description of what we are changing. Use simple language. Assume reader is not familiar with this code. Length <= 120 chars -->*\n\n#### Summary\n\n- <!-- Details of the changes in bullet points -->\n- <!-- Keep them high level -->\n- <!-- Each item <= 120 chars -->\n\n#### Alternatives\n\n- <!-- What alternatives have been considered? Why not? -->\n\n#### Test Plan\n\n- [ ] `make -C elixir all`\n- [ ] <!-- Additional targeted checks (list below) -->\n"
  },
  {
    "path": ".github/workflows/make-all.yml",
    "content": "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    defaults:\n      run:\n        working-directory: elixir\n\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n\n      - name: Set up mise tools\n        uses: jdx/mise-action@v3\n        with:\n          install: true\n          cache: true\n          working_directory: elixir\n\n      - name: Cache deps and build\n        uses: actions/cache@v4\n        with:\n          path: |\n            elixir/deps\n            elixir/_build\n          key: ${{ runner.os }}-mix-${{ hashFiles('elixir/mix.lock') }}\n          restore-keys: |\n            ${{ runner.os }}-mix-\n\n      - name: Verify make all\n        run: make all\n"
  },
  {
    "path": ".github/workflows/pr-description-lint.yml",
    "content": "name: pr-description-lint\n\non:\n  pull_request:\n    types: [opened, edited, reopened, synchronize, ready_for_review]\n\njobs:\n  validate-pr-description:\n    runs-on: ubuntu-latest\n    defaults:\n      run:\n        working-directory: elixir\n\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n\n      - name: Set up mise tools\n        uses: jdx/mise-action@v3\n        with:\n          install: true\n          cache: true\n          working_directory: elixir\n\n      - name: Validate PR description format\n        env:\n          PR_BODY_JSON: ${{ toJson(github.event.pull_request.body) }}\n        run: |\n          mix local.hex --force\n          mix local.rebar --force\n          mix deps.get\n          printf '%s' \"$PR_BODY_JSON\" | jq -r '.' > /tmp/pr_body.md\n          mix pr_body.check --file /tmp/pr_body.md\n"
  },
  {
    "path": "LICENSE",
    "content": "                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "NOTICE",
    "content": "Copyright 2025 OpenAI\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\nDistributed under the License is distributed on an “AS IS” BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nLimitations under the License.\n"
  },
  {
    "path": "README.md",
    "content": "# Symphony\n\nSymphony turns project work into isolated, autonomous implementation runs, allowing teams to manage\nwork instead of supervising coding agents.\n\n[![Symphony demo video preview](.github/media/symphony-demo-poster.jpg)](.github/media/symphony-demo.mp4)\n\n_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._\n\n> [!WARNING]\n> Symphony is a low-key engineering preview for testing in trusted environments.\n\n## Running Symphony\n\n### Requirements\n\nSymphony works best in codebases that have adopted\n[harness engineering](https://openai.com/index/harness-engineering/). Symphony is the next step --\nmoving from managing coding agents to managing work that needs to get done.\n\n### Option 1. Make your own\n\nTell your favorite coding agent to build Symphony in a programming language of your choice:\n\n> Implement Symphony according to the following spec:\n> https://github.com/openai/symphony/blob/main/SPEC.md\n\n### Option 2. Use our experimental reference implementation\n\nCheck out [elixir/README.md](elixir/README.md) for instructions on how to set up your environment\nand run the Elixir-based Symphony implementation. You can also ask your favorite coding agent to\nhelp with the setup:\n\n> Set up Symphony for my repository based on\n> https://github.com/openai/symphony/blob/main/elixir/README.md\n\n---\n\n## License\n\nThis project is licensed under the [Apache License 2.0](LICENSE).\n"
  },
  {
    "path": "SPEC.md",
    "content": "# Symphony Service Specification\n\nStatus: Draft v1 (language-agnostic)\n\nPurpose: Define a service that orchestrates coding agents to get project work done.\n\n## 1. Problem Statement\n\nSymphony is a long-running automation service that continuously reads work from an issue tracker\n(Linear in this specification version), creates an isolated workspace for each issue, and runs a\ncoding agent session for that issue inside the workspace.\n\nThe service solves four operational problems:\n\n- It turns issue execution into a repeatable daemon workflow instead of manual scripts.\n- It isolates agent execution in per-issue workspaces so agent commands run only inside per-issue\n  workspace directories.\n- It keeps the workflow policy in-repo (`WORKFLOW.md`) so teams version the agent prompt and runtime\n  settings with their code.\n- It provides enough observability to operate and debug multiple concurrent agent runs.\n\nImplementations are expected to document their trust and safety posture explicitly. This\nspecification does not require a single approval, sandbox, or operator-confirmation policy; some\nimplementations may target trusted environments with a high-trust configuration, while others may\nrequire stricter approvals or sandboxing.\n\nImportant boundary:\n\n- Symphony is a scheduler/runner and tracker reader.\n- Ticket writes (state transitions, comments, PR links) are typically performed by the coding agent\n  using tools available in the workflow/runtime environment.\n- A successful run may end at a workflow-defined handoff state (for example `Human Review`), not\n  necessarily `Done`.\n\n## 2. Goals and Non-Goals\n\n### 2.1 Goals\n\n- Poll the issue tracker on a fixed cadence and dispatch work with bounded concurrency.\n- Maintain a single authoritative orchestrator state for dispatch, retries, and reconciliation.\n- Create deterministic per-issue workspaces and preserve them across runs.\n- Stop active runs when issue state changes make them ineligible.\n- Recover from transient failures with exponential backoff.\n- Load runtime behavior from a repository-owned `WORKFLOW.md` contract.\n- Expose operator-visible observability (at minimum structured logs).\n- Support restart recovery without requiring a persistent database.\n\n### 2.2 Non-Goals\n\n- Rich web UI or multi-tenant control plane.\n- Prescribing a specific dashboard or terminal UI implementation.\n- General-purpose workflow engine or distributed job scheduler.\n- Built-in business logic for how to edit tickets, PRs, or comments. (That logic lives in the\n  workflow prompt and agent tooling.)\n- Mandating strong sandbox controls beyond what the coding agent and host OS provide.\n- Mandating a single default approval, sandbox, or operator-confirmation posture for all\n  implementations.\n\n## 3. System Overview\n\n### 3.1 Main Components\n\n1. `Workflow Loader`\n   - Reads `WORKFLOW.md`.\n   - Parses YAML front matter and prompt body.\n   - Returns `{config, prompt_template}`.\n\n2. `Config Layer`\n   - Exposes typed getters for workflow config values.\n   - Applies defaults and environment variable indirection.\n   - Performs validation used by the orchestrator before dispatch.\n\n3. `Issue Tracker Client`\n   - Fetches candidate issues in active states.\n   - Fetches current states for specific issue IDs (reconciliation).\n   - Fetches terminal-state issues during startup cleanup.\n   - Normalizes tracker payloads into a stable issue model.\n\n4. `Orchestrator`\n   - Owns the poll tick.\n   - Owns the in-memory runtime state.\n   - Decides which issues to dispatch, retry, stop, or release.\n   - Tracks session metrics and retry queue state.\n\n5. `Workspace Manager`\n   - Maps issue identifiers to workspace paths.\n   - Ensures per-issue workspace directories exist.\n   - Runs workspace lifecycle hooks.\n   - Cleans workspaces for terminal issues.\n\n6. `Agent Runner`\n   - Creates workspace.\n   - Builds prompt from issue + workflow template.\n   - Launches the coding agent app-server client.\n   - Streams agent updates back to the orchestrator.\n\n7. `Status Surface` (optional)\n   - Presents human-readable runtime status (for example terminal output, dashboard, or other\n     operator-facing view).\n\n8. `Logging`\n   - Emits structured runtime logs to one or more configured sinks.\n\n### 3.2 Abstraction Levels\n\nSymphony is easiest to port when kept in these layers:\n\n1. `Policy Layer` (repo-defined)\n   - `WORKFLOW.md` prompt body.\n   - Team-specific rules for ticket handling, validation, and handoff.\n\n2. `Configuration Layer` (typed getters)\n   - Parses front matter into typed runtime settings.\n   - Handles defaults, environment tokens, and path normalization.\n\n3. `Coordination Layer` (orchestrator)\n   - Polling loop, issue eligibility, concurrency, retries, reconciliation.\n\n4. `Execution Layer` (workspace + agent subprocess)\n   - Filesystem lifecycle, workspace preparation, coding-agent protocol.\n\n5. `Integration Layer` (Linear adapter)\n   - API calls and normalization for tracker data.\n\n6. `Observability Layer` (logs + optional status surface)\n   - Operator visibility into orchestrator and agent behavior.\n\n### 3.3 External Dependencies\n\n- Issue tracker API (Linear for `tracker.kind: linear` in this specification version).\n- Local filesystem for workspaces and logs.\n- Optional workspace population tooling (for example Git CLI, if used).\n- Coding-agent executable that supports JSON-RPC-like app-server mode over stdio.\n- Host environment authentication for the issue tracker and coding agent.\n\n## 4. Core Domain Model\n\n### 4.1 Entities\n\n#### 4.1.1 Issue\n\nNormalized issue record used by orchestration, prompt rendering, and observability output.\n\nFields:\n\n- `id` (string)\n  - Stable tracker-internal ID.\n- `identifier` (string)\n  - Human-readable ticket key (example: `ABC-123`).\n- `title` (string)\n- `description` (string or null)\n- `priority` (integer or null)\n  - Lower numbers are higher priority in dispatch sorting.\n- `state` (string)\n  - Current tracker state name.\n- `branch_name` (string or null)\n  - Tracker-provided branch metadata if available.\n- `url` (string or null)\n- `labels` (list of strings)\n  - Normalized to lowercase.\n- `blocked_by` (list of blocker refs)\n  - Each blocker ref contains:\n    - `id` (string or null)\n    - `identifier` (string or null)\n    - `state` (string or null)\n- `created_at` (timestamp or null)\n- `updated_at` (timestamp or null)\n\n#### 4.1.2 Workflow Definition\n\nParsed `WORKFLOW.md` payload:\n\n- `config` (map)\n  - YAML front matter root object.\n- `prompt_template` (string)\n  - Markdown body after front matter, trimmed.\n\n#### 4.1.3 Service Config (Typed View)\n\nTyped runtime values derived from `WorkflowDefinition.config` plus environment resolution.\n\nExamples:\n\n- poll interval\n- workspace root\n- active and terminal issue states\n- concurrency limits\n- coding-agent executable/args/timeouts\n- workspace hooks\n\n#### 4.1.4 Workspace\n\nFilesystem workspace assigned to one issue identifier.\n\nFields (logical):\n\n- `path` (workspace path; current runtime typically uses absolute paths, but relative roots are\n  possible if configured without path separators)\n- `workspace_key` (sanitized issue identifier)\n- `created_now` (boolean, used to gate `after_create` hook)\n\n#### 4.1.5 Run Attempt\n\nOne execution attempt for one issue.\n\nFields (logical):\n\n- `issue_id`\n- `issue_identifier`\n- `attempt` (integer or null, `null` for first run, `>=1` for retries/continuation)\n- `workspace_path`\n- `started_at`\n- `status`\n- `error` (optional)\n\n#### 4.1.6 Live Session (Agent Session Metadata)\n\nState tracked while a coding-agent subprocess is running.\n\nFields:\n\n- `session_id` (string, `<thread_id>-<turn_id>`)\n- `thread_id` (string)\n- `turn_id` (string)\n- `codex_app_server_pid` (string or null)\n- `last_codex_event` (string/enum or null)\n- `last_codex_timestamp` (timestamp or null)\n- `last_codex_message` (summarized payload)\n- `codex_input_tokens` (integer)\n- `codex_output_tokens` (integer)\n- `codex_total_tokens` (integer)\n- `last_reported_input_tokens` (integer)\n- `last_reported_output_tokens` (integer)\n- `last_reported_total_tokens` (integer)\n- `turn_count` (integer)\n  - Number of coding-agent turns started within the current worker lifetime.\n\n#### 4.1.7 Retry Entry\n\nScheduled retry state for an issue.\n\nFields:\n\n- `issue_id`\n- `identifier` (best-effort human ID for status surfaces/logs)\n- `attempt` (integer, 1-based for retry queue)\n- `due_at_ms` (monotonic clock timestamp)\n- `timer_handle` (runtime-specific timer reference)\n- `error` (string or null)\n\n#### 4.1.8 Orchestrator Runtime State\n\nSingle authoritative in-memory state owned by the orchestrator.\n\nFields:\n\n- `poll_interval_ms` (current effective poll interval)\n- `max_concurrent_agents` (current effective global concurrency limit)\n- `running` (map `issue_id -> running entry`)\n- `claimed` (set of issue IDs reserved/running/retrying)\n- `retry_attempts` (map `issue_id -> RetryEntry`)\n- `completed` (set of issue IDs; bookkeeping only, not dispatch gating)\n- `codex_totals` (aggregate tokens + runtime seconds)\n- `codex_rate_limits` (latest rate-limit snapshot from agent events)\n\n### 4.2 Stable Identifiers and Normalization Rules\n\n- `Issue ID`\n  - Use for tracker lookups and internal map keys.\n- `Issue Identifier`\n  - Use for human-readable logs and workspace naming.\n- `Workspace Key`\n  - Derive from `issue.identifier` by replacing any character not in `[A-Za-z0-9._-]` with `_`.\n  - Use the sanitized value for the workspace directory name.\n- `Normalized Issue State`\n  - Compare states after `lowercase`.\n- `Session ID`\n  - Compose from coding-agent `thread_id` and `turn_id` as `<thread_id>-<turn_id>`.\n\n## 5. Workflow Specification (Repository Contract)\n\n### 5.1 File Discovery and Path Resolution\n\nWorkflow file path precedence:\n\n1. Explicit application/runtime setting (set by CLI startup path).\n2. Default: `WORKFLOW.md` in the current process working directory.\n\nLoader behavior:\n\n- If the file cannot be read, return `missing_workflow_file` error.\n- The workflow file is expected to be repository-owned and version-controlled.\n\n### 5.2 File Format\n\n`WORKFLOW.md` is a Markdown file with optional YAML front matter.\n\nDesign note:\n\n- `WORKFLOW.md` should be self-contained enough to describe and run different workflows (prompt,\n  runtime settings, hooks, and tracker selection/config) without requiring out-of-band\n  service-specific configuration.\n\nParsing rules:\n\n- If file starts with `---`, parse lines until the next `---` as YAML front matter.\n- Remaining lines become the prompt body.\n- If front matter is absent, treat the entire file as prompt body and use an empty config map.\n- YAML front matter must decode to a map/object; non-map YAML is an error.\n- Prompt body is trimmed before use.\n\nReturned workflow object:\n\n- `config`: front matter root object (not nested under a `config` key).\n- `prompt_template`: trimmed Markdown body.\n\n### 5.3 Front Matter Schema\n\nTop-level keys:\n\n- `tracker`\n- `polling`\n- `workspace`\n- `hooks`\n- `agent`\n- `codex`\n\nUnknown keys should be ignored for forward compatibility.\n\nNote:\n\n- The workflow front matter is extensible. Optional extensions may define additional top-level keys\n  (for example `server`) without changing the core schema above.\n- Extensions should document their field schema, defaults, validation rules, and whether changes\n  apply dynamically or require restart.\n- Common extension: `server.port` (integer) enables the optional HTTP server described in Section\n  13.7.\n\n#### 5.3.1 `tracker` (object)\n\nFields:\n\n- `kind` (string)\n  - Required for dispatch.\n  - Current supported value: `linear`\n- `endpoint` (string)\n  - Default for `tracker.kind == \"linear\"`: `https://api.linear.app/graphql`\n- `api_key` (string)\n  - May be a literal token or `$VAR_NAME`.\n  - Canonical environment variable for `tracker.kind == \"linear\"`: `LINEAR_API_KEY`.\n  - If `$VAR_NAME` resolves to an empty string, treat the key as missing.\n- `project_slug` (string)\n  - Required for dispatch when `tracker.kind == \"linear\"`.\n- `active_states` (list of strings)\n  - Default: `Todo`, `In Progress`\n- `terminal_states` (list of strings)\n  - Default: `Closed`, `Cancelled`, `Canceled`, `Duplicate`, `Done`\n\n#### 5.3.2 `polling` (object)\n\nFields:\n\n- `interval_ms` (integer or string integer)\n  - Default: `30000`\n  - Changes should be re-applied at runtime and affect future tick scheduling without restart.\n\n#### 5.3.3 `workspace` (object)\n\nFields:\n\n- `root` (path string or `$VAR`)\n  - Default: `<system-temp>/symphony_workspaces`\n  - `~` and strings containing path separators are expanded.\n  - Bare strings without path separators are preserved as-is (relative roots are allowed but\n    discouraged).\n\n#### 5.3.4 `hooks` (object)\n\nFields:\n\n- `after_create` (multiline shell script string, optional)\n  - Runs only when a workspace directory is newly created.\n  - Failure aborts workspace creation.\n- `before_run` (multiline shell script string, optional)\n  - Runs before each agent attempt after workspace preparation and before launching the coding\n    agent.\n  - Failure aborts the current attempt.\n- `after_run` (multiline shell script string, optional)\n  - Runs after each agent attempt (success, failure, timeout, or cancellation) once the workspace\n    exists.\n  - Failure is logged but ignored.\n- `before_remove` (multiline shell script string, optional)\n  - Runs before workspace deletion if the directory exists.\n  - Failure is logged but ignored; cleanup still proceeds.\n- `timeout_ms` (integer, optional)\n  - Default: `60000`\n  - Applies to all workspace hooks.\n  - Non-positive values should be treated as invalid and fall back to the default.\n  - Changes should be re-applied at runtime for future hook executions.\n\n#### 5.3.5 `agent` (object)\n\nFields:\n\n- `max_concurrent_agents` (integer or string integer)\n  - Default: `10`\n  - Changes should be re-applied at runtime and affect subsequent dispatch decisions.\n- `max_retry_backoff_ms` (integer or string integer)\n  - Default: `300000` (5 minutes)\n  - Changes should be re-applied at runtime and affect future retry scheduling.\n- `max_concurrent_agents_by_state` (map `state_name -> positive integer`)\n  - Default: empty map.\n  - State keys are normalized (`lowercase`) for lookup.\n  - Invalid entries (non-positive or non-numeric) are ignored.\n\n#### 5.3.6 `codex` (object)\n\nFields:\n\nFor Codex-owned config values such as `approval_policy`, `thread_sandbox`, and\n`turn_sandbox_policy`, supported values are defined by the targeted Codex app-server version.\nImplementors should treat them as pass-through Codex config values rather than relying on a\nhand-maintained enum in this spec. To inspect the installed Codex schema, run\n`codex app-server generate-json-schema --out <dir>` and inspect the relevant definitions referenced\nby `v2/ThreadStartParams.json` and `v2/TurnStartParams.json`. Implementations may validate these\nfields locally if they want stricter startup checks.\n\n- `command` (string shell command)\n  - Default: `codex app-server`\n  - The runtime launches this command via `bash -lc` in the workspace directory.\n  - The launched process must speak a compatible app-server protocol over stdio.\n- `approval_policy` (Codex `AskForApproval` value)\n  - Default: implementation-defined.\n- `thread_sandbox` (Codex `SandboxMode` value)\n  - Default: implementation-defined.\n- `turn_sandbox_policy` (Codex `SandboxPolicy` value)\n  - Default: implementation-defined.\n- `turn_timeout_ms` (integer)\n  - Default: `3600000` (1 hour)\n- `read_timeout_ms` (integer)\n  - Default: `5000`\n- `stall_timeout_ms` (integer)\n  - Default: `300000` (5 minutes)\n  - If `<= 0`, stall detection is disabled.\n\n### 5.4 Prompt Template Contract\n\nThe Markdown body of `WORKFLOW.md` is the per-issue prompt template.\n\nRendering requirements:\n\n- Use a strict template engine (Liquid-compatible semantics are sufficient).\n- Unknown variables must fail rendering.\n- Unknown filters must fail rendering.\n\nTemplate input variables:\n\n- `issue` (object)\n  - Includes all normalized issue fields, including labels and blockers.\n- `attempt` (integer or null)\n  - `null`/absent on first attempt.\n  - Integer on retry or continuation run.\n\nFallback prompt behavior:\n\n- If the workflow prompt body is empty, the runtime may use a minimal default prompt\n  (`You are working on an issue from Linear.`).\n- Workflow file read/parse failures are configuration/validation errors and should not silently fall\n  back to a prompt.\n\n### 5.5 Workflow Validation and Error Surface\n\nError classes:\n\n- `missing_workflow_file`\n- `workflow_parse_error`\n- `workflow_front_matter_not_a_map`\n- `template_parse_error` (during prompt rendering)\n- `template_render_error` (unknown variable/filter, invalid interpolation)\n\nDispatch gating behavior:\n\n- Workflow file read/YAML errors block new dispatches until fixed.\n- Template errors fail only the affected run attempt.\n\n## 6. Configuration Specification\n\n### 6.1 Source Precedence and Resolution Semantics\n\nConfiguration precedence:\n\n1. Workflow file path selection (runtime setting -> cwd default).\n2. YAML front matter values.\n3. Environment indirection via `$VAR_NAME` inside selected YAML values.\n4. Built-in defaults.\n\nValue coercion semantics:\n\n- Path/command fields support:\n  - `~` home expansion\n  - `$VAR` expansion for env-backed path values\n  - Apply expansion only to values intended to be local filesystem paths; do not rewrite URIs or\n    arbitrary shell command strings.\n\n### 6.2 Dynamic Reload Semantics\n\nDynamic reload is required:\n\n- The software should watch `WORKFLOW.md` for changes.\n- On change, it should re-read and re-apply workflow config and prompt template without restart.\n- The software should attempt to adjust live behavior to the new config (for example polling\n  cadence, concurrency limits, active/terminal states, codex settings, workspace paths/hooks, and\n  prompt content for future runs).\n- Reloaded config applies to future dispatch, retry scheduling, reconciliation decisions, hook\n  execution, and agent launches.\n- Implementations are not required to restart in-flight agent sessions automatically when config\n  changes.\n- Extensions that manage their own listeners/resources (for example an HTTP server port change) may\n  require restart unless the implementation explicitly supports live rebind.\n- Implementations should also re-validate/reload defensively during runtime operations (for example\n  before dispatch) in case filesystem watch events are missed.\n- Invalid reloads should not crash the service; keep operating with the last known good effective\n  configuration and emit an operator-visible error.\n\n### 6.3 Dispatch Preflight Validation\n\nThis validation is a scheduler preflight run before attempting to dispatch new work. It validates\nthe workflow/config needed to poll and launch workers, not a full audit of all possible workflow\nbehavior.\n\nStartup validation:\n\n- Validate configuration before starting the scheduling loop.\n- If startup validation fails, fail startup and emit an operator-visible error.\n\nPer-tick dispatch validation:\n\n- Re-validate before each dispatch cycle.\n- If validation fails, skip dispatch for that tick, keep reconciliation active, and emit an\n  operator-visible error.\n\nValidation checks:\n\n- Workflow file can be loaded and parsed.\n- `tracker.kind` is present and supported.\n- `tracker.api_key` is present after `$` resolution.\n- `tracker.project_slug` is present when required by the selected tracker kind.\n- `codex.command` is present and non-empty.\n\n### 6.4 Config Fields Summary (Cheat Sheet)\n\nThis section is intentionally redundant so a coding agent can implement the config layer quickly.\n\n- `tracker.kind`: string, required, currently `linear`\n- `tracker.endpoint`: string, default `https://api.linear.app/graphql` when `tracker.kind=linear`\n- `tracker.api_key`: string or `$VAR`, canonical env `LINEAR_API_KEY` when `tracker.kind=linear`\n- `tracker.project_slug`: string, required when `tracker.kind=linear`\n- `tracker.active_states`: list of strings, default `[\"Todo\", \"In Progress\"]`\n- `tracker.terminal_states`: list of strings, default `[\"Closed\", \"Cancelled\", \"Canceled\", \"Duplicate\", \"Done\"]`\n- `polling.interval_ms`: integer, default `30000`\n- `workspace.root`: path, default `<system-temp>/symphony_workspaces`\n- `worker.ssh_hosts` (extension): list of SSH host strings, optional; when omitted, work runs\n  locally\n- `worker.max_concurrent_agents_per_host` (extension): positive integer, optional; shared per-host\n  cap applied across configured SSH hosts\n- `hooks.after_create`: shell script or null\n- `hooks.before_run`: shell script or null\n- `hooks.after_run`: shell script or null\n- `hooks.before_remove`: shell script or null\n- `hooks.timeout_ms`: integer, default `60000`\n- `agent.max_concurrent_agents`: integer, default `10`\n- `agent.max_turns`: integer, default `20`\n- `agent.max_retry_backoff_ms`: integer, default `300000` (5m)\n- `agent.max_concurrent_agents_by_state`: map of positive integers, default `{}`\n- `codex.command`: shell command string, default `codex app-server`\n- `codex.approval_policy`: Codex `AskForApproval` value, default implementation-defined\n- `codex.thread_sandbox`: Codex `SandboxMode` value, default implementation-defined\n- `codex.turn_sandbox_policy`: Codex `SandboxPolicy` value, default implementation-defined\n- `codex.turn_timeout_ms`: integer, default `3600000`\n- `codex.read_timeout_ms`: integer, default `5000`\n- `codex.stall_timeout_ms`: integer, default `300000`\n- `server.port` (extension): integer, optional; enables the optional HTTP server, `0` may be used\n  for ephemeral local bind, and CLI `--port` overrides it\n\n## 7. Orchestration State Machine\n\nThe orchestrator is the only component that mutates scheduling state. All worker outcomes are\nreported back to it and converted into explicit state transitions.\n\n### 7.1 Issue Orchestration States\n\nThis is not the same as tracker states (`Todo`, `In Progress`, etc.). This is the service's internal\nclaim state.\n\n1. `Unclaimed`\n   - Issue is not running and has no retry scheduled.\n\n2. `Claimed`\n   - Orchestrator has reserved the issue to prevent duplicate dispatch.\n   - In practice, claimed issues are either `Running` or `RetryQueued`.\n\n3. `Running`\n   - Worker task exists and the issue is tracked in `running` map.\n\n4. `RetryQueued`\n   - Worker is not running, but a retry timer exists in `retry_attempts`.\n\n5. `Released`\n   - Claim removed because issue is terminal, non-active, missing, or retry path completed without\n     re-dispatch.\n\nImportant nuance:\n\n- A successful worker exit does not mean the issue is done forever.\n- The worker may continue through multiple back-to-back coding-agent turns before it exits.\n- After each normal turn completion, the worker re-checks the tracker issue state.\n- If the issue is still in an active state, the worker should start another turn on the same live\n  coding-agent thread in the same workspace, up to `agent.max_turns`.\n- The first turn should use the full rendered task prompt.\n- Continuation turns should send only continuation guidance to the existing thread, not resend the\n  original task prompt that is already present in thread history.\n- Once the worker exits normally, the orchestrator still schedules a short continuation retry\n  (about 1 second) so it can re-check whether the issue remains active and needs another worker\n  session.\n\n### 7.2 Run Attempt Lifecycle\n\nA run attempt transitions through these phases:\n\n1. `PreparingWorkspace`\n2. `BuildingPrompt`\n3. `LaunchingAgentProcess`\n4. `InitializingSession`\n5. `StreamingTurn`\n6. `Finishing`\n7. `Succeeded`\n8. `Failed`\n9. `TimedOut`\n10. `Stalled`\n11. `CanceledByReconciliation`\n\nDistinct terminal reasons are important because retry logic and logs differ.\n\n### 7.3 Transition Triggers\n\n- `Poll Tick`\n  - Reconcile active runs.\n  - Validate config.\n  - Fetch candidate issues.\n  - Dispatch until slots are exhausted.\n\n- `Worker Exit (normal)`\n  - Remove running entry.\n  - Update aggregate runtime totals.\n  - Schedule continuation retry (attempt `1`) after the worker exhausts or finishes its in-process\n    turn loop.\n\n- `Worker Exit (abnormal)`\n  - Remove running entry.\n  - Update aggregate runtime totals.\n  - Schedule exponential-backoff retry.\n\n- `Codex Update Event`\n  - Update live session fields, token counters, and rate limits.\n\n- `Retry Timer Fired`\n  - Re-fetch active candidates and attempt re-dispatch, or release claim if no longer eligible.\n\n- `Reconciliation State Refresh`\n  - Stop runs whose issue states are terminal or no longer active.\n\n- `Stall Timeout`\n  - Kill worker and schedule retry.\n\n### 7.4 Idempotency and Recovery Rules\n\n- The orchestrator serializes state mutations through one authority to avoid duplicate dispatch.\n- `claimed` and `running` checks are required before launching any worker.\n- Reconciliation runs before dispatch on every tick.\n- Restart recovery is tracker-driven and filesystem-driven (no durable orchestrator DB required).\n- Startup terminal cleanup removes stale workspaces for issues already in terminal states.\n\n## 8. Polling, Scheduling, and Reconciliation\n\n### 8.1 Poll Loop\n\nAt startup, the service validates config, performs startup cleanup, schedules an immediate tick, and\nthen repeats every `polling.interval_ms`.\n\nThe effective poll interval should be updated when workflow config changes are re-applied.\n\nTick sequence:\n\n1. Reconcile running issues.\n2. Run dispatch preflight validation.\n3. Fetch candidate issues from tracker using active states.\n4. Sort issues by dispatch priority.\n5. Dispatch eligible issues while slots remain.\n6. Notify observability/status consumers of state changes.\n\nIf per-tick validation fails, dispatch is skipped for that tick, but reconciliation still happens\nfirst.\n\n### 8.2 Candidate Selection Rules\n\nAn issue is dispatch-eligible only if all are true:\n\n- It has `id`, `identifier`, `title`, and `state`.\n- Its state is in `active_states` and not in `terminal_states`.\n- It is not already in `running`.\n- It is not already in `claimed`.\n- Global concurrency slots are available.\n- Per-state concurrency slots are available.\n- Blocker rule for `Todo` state passes:\n  - If the issue state is `Todo`, do not dispatch when any blocker is non-terminal.\n\nSorting order (stable intent):\n\n1. `priority` ascending (1..4 are preferred; null/unknown sorts last)\n2. `created_at` oldest first\n3. `identifier` lexicographic tie-breaker\n\n### 8.3 Concurrency Control\n\nGlobal limit:\n\n- `available_slots = max(max_concurrent_agents - running_count, 0)`\n\nPer-state limit:\n\n- `max_concurrent_agents_by_state[state]` if present (state key normalized)\n- otherwise fallback to global limit\n\nThe runtime counts issues by their current tracked state in the `running` map.\n\nOptional SSH host limit:\n\n- When `worker.max_concurrent_agents_per_host` is set, each configured SSH host may run at most\n  that many concurrent agents at once.\n- Hosts at that cap are skipped for new dispatch until capacity frees up.\n\n### 8.4 Retry and Backoff\n\nRetry entry creation:\n\n- Cancel any existing retry timer for the same issue.\n- Store `attempt`, `identifier`, `error`, `due_at_ms`, and new timer handle.\n\nBackoff formula:\n\n- Normal continuation retries after a clean worker exit use a short fixed delay of `1000` ms.\n- Failure-driven retries use `delay = min(10000 * 2^(attempt - 1), agent.max_retry_backoff_ms)`.\n- Power is capped by the configured max retry backoff (default `300000` / 5m).\n\nRetry handling behavior:\n\n1. Fetch active candidate issues (not all issues).\n2. Find the specific issue by `issue_id`.\n3. If not found, release claim.\n4. If found and still candidate-eligible:\n   - Dispatch if slots are available.\n   - Otherwise requeue with error `no available orchestrator slots`.\n5. If found but no longer active, release claim.\n\nNote:\n\n- Terminal-state workspace cleanup is handled by startup cleanup and active-run reconciliation\n  (including terminal transitions for currently running issues).\n- Retry handling mainly operates on active candidates and releases claims when the issue is absent,\n  rather than performing terminal cleanup itself.\n\n### 8.5 Active Run Reconciliation\n\nReconciliation runs every tick and has two parts.\n\nPart A: Stall detection\n\n- For each running issue, compute `elapsed_ms` since:\n  - `last_codex_timestamp` if any event has been seen, else\n  - `started_at`\n- If `elapsed_ms > codex.stall_timeout_ms`, terminate the worker and queue a retry.\n- If `stall_timeout_ms <= 0`, skip stall detection entirely.\n\nPart B: Tracker state refresh\n\n- Fetch current issue states for all running issue IDs.\n- For each running issue:\n  - If tracker state is terminal: terminate worker and clean workspace.\n  - If tracker state is still active: update the in-memory issue snapshot.\n  - If tracker state is neither active nor terminal: terminate worker without workspace cleanup.\n- If state refresh fails, keep workers running and try again on the next tick.\n\n### 8.6 Startup Terminal Workspace Cleanup\n\nWhen the service starts:\n\n1. Query tracker for issues in terminal states.\n2. For each returned issue identifier, remove the corresponding workspace directory.\n3. If the terminal-issues fetch fails, log a warning and continue startup.\n\nThis prevents stale terminal workspaces from accumulating after restarts.\n\n## 9. Workspace Management and Safety\n\n### 9.1 Workspace Layout\n\nWorkspace root:\n\n- `workspace.root` (normalized path; the current config layer expands path-like values and preserves\n  bare relative names)\n\nPer-issue workspace path:\n\n- `<workspace.root>/<sanitized_issue_identifier>`\n\nWorkspace persistence:\n\n- Workspaces are reused across runs for the same issue.\n- Successful runs do not auto-delete workspaces.\n\n### 9.2 Workspace Creation and Reuse\n\nInput: `issue.identifier`\n\nAlgorithm summary:\n\n1. Sanitize identifier to `workspace_key`.\n2. Compute workspace path under workspace root.\n3. Ensure the workspace path exists as a directory.\n4. Mark `created_now=true` only if the directory was created during this call; otherwise\n   `created_now=false`.\n5. If `created_now=true`, run `after_create` hook if configured.\n\nNotes:\n\n- This section does not assume any specific repository/VCS workflow.\n- Workspace preparation beyond directory creation (for example dependency bootstrap, checkout/sync,\n  code generation) is implementation-defined and is typically handled via hooks.\n\n### 9.3 Optional Workspace Population (Implementation-Defined)\n\nThe spec does not require any built-in VCS or repository bootstrap behavior.\n\nImplementations may populate or synchronize the workspace using implementation-defined logic and/or\nhooks (for example `after_create` and/or `before_run`).\n\nFailure handling:\n\n- Workspace population/synchronization failures return an error for the current attempt.\n- If failure happens while creating a brand-new workspace, implementations may remove the partially\n  prepared directory.\n- Reused workspaces should not be destructively reset on population failure unless that policy is\n  explicitly chosen and documented.\n\n### 9.4 Workspace Hooks\n\nSupported hooks:\n\n- `hooks.after_create`\n- `hooks.before_run`\n- `hooks.after_run`\n- `hooks.before_remove`\n\nExecution contract:\n\n- Execute in a local shell context appropriate to the host OS, with the workspace directory as\n  `cwd`.\n- On POSIX systems, `sh -lc <script>` (or a stricter equivalent such as `bash -lc <script>`) is a\n  conforming default.\n- Hook timeout uses `hooks.timeout_ms`; default: `60000 ms`.\n- Log hook start, failures, and timeouts.\n\nFailure semantics:\n\n- `after_create` failure or timeout is fatal to workspace creation.\n- `before_run` failure or timeout is fatal to the current run attempt.\n- `after_run` failure or timeout is logged and ignored.\n- `before_remove` failure or timeout is logged and ignored.\n\n### 9.5 Safety Invariants\n\nThis is the most important portability constraint.\n\nInvariant 1: Run the coding agent only in the per-issue workspace path.\n\n- Before launching the coding-agent subprocess, validate:\n  - `cwd == workspace_path`\n\nInvariant 2: Workspace path must stay inside workspace root.\n\n- Normalize both paths to absolute.\n- Require `workspace_path` to have `workspace_root` as a prefix directory.\n- Reject any path outside the workspace root.\n\nInvariant 3: Workspace key is sanitized.\n\n- Only `[A-Za-z0-9._-]` allowed in workspace directory names.\n- Replace all other characters with `_`.\n\n## 10. Agent Runner Protocol (Coding Agent Integration)\n\nThis section defines the language-neutral contract for integrating a coding agent app-server.\n\nCompatibility profile:\n\n- The normative contract is message ordering, required behaviors, and the logical fields that must\n  be extracted (for example session IDs, completion state, approval handling, and usage/rate-limit\n  telemetry).\n- Exact JSON field names may vary slightly across compatible app-server versions.\n- Implementations should tolerate equivalent payload shapes when they carry the same logical\n  meaning, especially for nested IDs, approval requests, user-input-required signals, and\n  token/rate-limit metadata.\n\n### 10.1 Launch Contract\n\nSubprocess launch parameters:\n\n- Command: `codex.command`\n- Invocation: `bash -lc <codex.command>`\n- Working directory: workspace path\n- Stdout/stderr: separate streams\n- Framing: line-delimited protocol messages on stdout (JSON-RPC-like JSON per line)\n\nNotes:\n\n- The default command is `codex app-server`.\n- Approval policy, cwd, and prompt are expressed in the protocol messages in Section 10.2.\n\nRecommended additional process settings:\n\n- Max line size: 10 MB (for safe buffering)\n\n### 10.2 Session Startup Handshake\n\nReference: https://developers.openai.com/codex/app-server/\n\nThe client must send these protocol messages in order:\n\nIllustrative startup transcript (equivalent payload shapes are acceptable if they preserve the same\nsemantics):\n\n```json\n{\"id\":1,\"method\":\"initialize\",\"params\":{\"clientInfo\":{\"name\":\"symphony\",\"version\":\"1.0\"},\"capabilities\":{}}}\n{\"method\":\"initialized\",\"params\":{}}\n{\"id\":2,\"method\":\"thread/start\",\"params\":{\"approvalPolicy\":\"<implementation-defined>\",\"sandbox\":\"<implementation-defined>\",\"cwd\":\"/abs/workspace\"}}\n{\"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>\"}}}\n```\n\n1. `initialize` request\n   - Params include:\n     - `clientInfo` object (for example `{name, version}`)\n     - `capabilities` object (may be empty)\n   - If the targeted Codex app-server requires capability negotiation for dynamic tools, include the\n     necessary capability flag(s) here.\n   - Wait for response (`read_timeout_ms`)\n2. `initialized` notification\n3. `thread/start` request\n   - Params include:\n     - `approvalPolicy` = implementation-defined session approval policy value\n     - `sandbox` = implementation-defined session sandbox value\n     - `cwd` = absolute workspace path\n     - If optional client-side tools are implemented, include their advertised tool specs using the\n       protocol mechanism supported by the targeted Codex app-server version.\n4. `turn/start` request\n   - Params include:\n     - `threadId`\n     - `input` = single text item containing rendered prompt for the first turn, or continuation\n       guidance for later turns on the same thread\n     - `cwd`\n     - `title` = `<issue.identifier>: <issue.title>`\n     - `approvalPolicy` = implementation-defined turn approval policy value\n     - `sandboxPolicy` = implementation-defined object-form sandbox policy payload when required by\n       the targeted app-server version\n\nSession identifiers:\n\n- Read `thread_id` from `thread/start` result `result.thread.id`\n- Read `turn_id` from each `turn/start` result `result.turn.id`\n- Emit `session_id = \"<thread_id>-<turn_id>\"`\n- Reuse the same `thread_id` for all continuation turns inside one worker run\n\n### 10.3 Streaming Turn Processing\n\nThe client reads line-delimited messages until the turn terminates.\n\nCompletion conditions:\n\n- `turn/completed` -> success\n- `turn/failed` -> failure\n- `turn/cancelled` -> failure\n- turn timeout (`turn_timeout_ms`) -> failure\n- subprocess exit -> failure\n\nContinuation processing:\n\n- If the worker decides to continue after a successful turn, it should issue another `turn/start`\n  on the same live `threadId`.\n- The app-server subprocess should remain alive across those continuation turns and be stopped only\n  when the worker run is ending.\n\nLine handling requirements:\n\n- Read protocol messages from stdout only.\n- Buffer partial stdout lines until newline arrives.\n- Attempt JSON parse on complete stdout lines.\n- Stderr is not part of the protocol stream:\n  - ignore it or log it as diagnostics\n  - do not attempt protocol JSON parsing on stderr\n\n### 10.4 Emitted Runtime Events (Upstream to Orchestrator)\n\nThe app-server client emits structured events to the orchestrator callback. Each event should\ninclude:\n\n- `event` (enum/string)\n- `timestamp` (UTC timestamp)\n- `codex_app_server_pid` (if available)\n- optional `usage` map (token counts)\n- payload fields as needed\n\nImportant emitted events may include:\n\n- `session_started`\n- `startup_failed`\n- `turn_completed`\n- `turn_failed`\n- `turn_cancelled`\n- `turn_ended_with_error`\n- `turn_input_required`\n- `approval_auto_approved`\n- `unsupported_tool_call`\n- `notification`\n- `other_message`\n- `malformed`\n\n### 10.5 Approval, Tool Calls, and User Input Policy\n\nApproval, sandbox, and user-input behavior is implementation-defined.\n\nPolicy requirements:\n\n- Each implementation should document its chosen approval, sandbox, and operator-confirmation\n  posture.\n- Approval requests and user-input-required events must not leave a run stalled indefinitely. An\n  implementation should either satisfy them, surface them to an operator, auto-resolve them, or\n  fail the run according to its documented policy.\n\nExample high-trust behavior:\n\n- Auto-approve command execution approvals for the session.\n- Auto-approve file-change approvals for the session.\n- Treat user-input-required turns as hard failure.\n\nUnsupported dynamic tool calls:\n\n- Supported dynamic tool calls that are explicitly implemented and advertised by the runtime should\n  be handled according to their extension contract.\n- If the agent requests a dynamic tool call (`item/tool/call`) that is not supported, return a tool\n  failure response and continue the session.\n- This prevents the session from stalling on unsupported tool execution paths.\n\nOptional client-side tool extension:\n\n- An implementation may expose a limited set of client-side tools to the app-server session.\n- Current optional standardized tool: `linear_graphql`.\n- If implemented, supported tools should be advertised to the app-server session during startup\n  using the protocol mechanism supported by the targeted Codex app-server version.\n- Unsupported tool names should still return a failure result and continue the session.\n\n`linear_graphql` extension contract:\n\n- Purpose: execute a raw GraphQL query or mutation against Linear using Symphony's configured\n  tracker auth for the current session.\n- Availability: only meaningful when `tracker.kind == \"linear\"` and valid Linear auth is configured.\n- Preferred input shape:\n\n  ```json\n  {\n    \"query\": \"single GraphQL query or mutation document\",\n    \"variables\": {\n      \"optional\": \"graphql variables object\"\n    }\n  }\n  ```\n\n- `query` must be a non-empty string.\n- `query` must contain exactly one GraphQL operation.\n- `variables` is optional and, when present, must be a JSON object.\n- Implementations may additionally accept a raw GraphQL query string as shorthand input.\n- Execute one GraphQL operation per tool call.\n- If the provided document contains multiple operations, reject the tool call as invalid input.\n- `operationName` selection is intentionally out of scope for this extension.\n- Reuse the configured Linear endpoint and auth from the active Symphony workflow/runtime config; do\n  not require the coding agent to read raw tokens from disk.\n- Tool result semantics:\n  - transport success + no top-level GraphQL `errors` -> `success=true`\n  - top-level GraphQL `errors` present -> `success=false`, but preserve the GraphQL response body\n    for debugging\n  - invalid input, missing auth, or transport failure -> `success=false` with an error payload\n- Return the GraphQL response or error payload as structured tool output that the model can inspect\n  in-session.\n\nIllustrative responses (equivalent payload shapes are acceptable if they preserve the same outcome):\n\n```json\n{\"id\":\"<approval-id>\",\"result\":{\"approved\":true}}\n{\"id\":\"<tool-call-id>\",\"result\":{\"success\":false,\"error\":\"unsupported_tool_call\"}}\n```\n\nHard failure on user input requirement:\n\n- If the agent requests user input, fail the run attempt immediately.\n- The client detects this via:\n  - explicit method (`item/tool/requestUserInput`), or\n  - turn methods/flags indicating input is required.\n\n### 10.6 Timeouts and Error Mapping\n\nTimeouts:\n\n- `codex.read_timeout_ms`: request/response timeout during startup and sync requests\n- `codex.turn_timeout_ms`: total turn stream timeout\n- `codex.stall_timeout_ms`: enforced by orchestrator based on event inactivity\n\nError mapping (recommended normalized categories):\n\n- `codex_not_found`\n- `invalid_workspace_cwd`\n- `response_timeout`\n- `turn_timeout`\n- `port_exit`\n- `response_error`\n- `turn_failed`\n- `turn_cancelled`\n- `turn_input_required`\n\n### 10.7 Agent Runner Contract\n\nThe `Agent Runner` wraps workspace + prompt + app-server client.\n\nBehavior:\n\n1. Create/reuse workspace for issue.\n2. Build prompt from workflow template.\n3. Start app-server session.\n4. Forward app-server events to orchestrator.\n5. On any error, fail the worker attempt (the orchestrator will retry).\n\nNote:\n\n- Workspaces are intentionally preserved after successful runs.\n\n## 11. Issue Tracker Integration Contract (Linear-Compatible)\n\n### 11.1 Required Operations\n\nAn implementation must support these tracker adapter operations:\n\n1. `fetch_candidate_issues()`\n   - Return issues in configured active states for a configured project.\n\n2. `fetch_issues_by_states(state_names)`\n   - Used for startup terminal cleanup.\n\n3. `fetch_issue_states_by_ids(issue_ids)`\n   - Used for active-run reconciliation.\n\n### 11.2 Query Semantics (Linear)\n\nLinear-specific requirements for `tracker.kind == \"linear\"`:\n\n- `tracker.kind == \"linear\"`\n- GraphQL endpoint (default `https://api.linear.app/graphql`)\n- Auth token sent in `Authorization` header\n- `tracker.project_slug` maps to Linear project `slugId`\n- Candidate issue query filters project using `project: { slugId: { eq: $projectSlug } }`\n- Issue-state refresh query uses GraphQL issue IDs with variable type `[ID!]`\n- Pagination required for candidate issues\n- Page size default: `50`\n- Network timeout: `30000 ms`\n\nImportant:\n\n- Linear GraphQL schema details can drift. Keep query construction isolated and test the exact query\n  fields/types required by this specification.\n\nA non-Linear implementation may change transport details, but the normalized outputs must match the\ndomain model in Section 4.\n\n### 11.3 Normalization Rules\n\nCandidate issue normalization should produce fields listed in Section 4.1.1.\n\nAdditional normalization details:\n\n- `labels` -> lowercase strings\n- `blocked_by` -> derived from inverse relations where relation type is `blocks`\n- `priority` -> integer only (non-integers become null)\n- `created_at` and `updated_at` -> parse ISO-8601 timestamps\n\n### 11.4 Error Handling Contract\n\nRecommended error categories:\n\n- `unsupported_tracker_kind`\n- `missing_tracker_api_key`\n- `missing_tracker_project_slug`\n- `linear_api_request` (transport failures)\n- `linear_api_status` (non-200 HTTP)\n- `linear_graphql_errors`\n- `linear_unknown_payload`\n- `linear_missing_end_cursor` (pagination integrity error)\n\nOrchestrator behavior on tracker errors:\n\n- Candidate fetch failure: log and skip dispatch for this tick.\n- Running-state refresh failure: log and keep active workers running.\n- Startup terminal cleanup failure: log warning and continue startup.\n\n### 11.5 Tracker Writes (Important Boundary)\n\nSymphony does not require first-class tracker write APIs in the orchestrator.\n\n- Ticket mutations (state transitions, comments, PR metadata) are typically handled by the coding\n  agent using tools defined by the workflow prompt.\n- The service remains a scheduler/runner and tracker reader.\n- Workflow-specific success often means \"reached the next handoff state\" (for example\n  `Human Review`) rather than tracker terminal state `Done`.\n- If the optional `linear_graphql` client-side tool extension is implemented, it is still part of\n  the agent toolchain rather than orchestrator business logic.\n\n## 12. Prompt Construction and Context Assembly\n\n### 12.1 Inputs\n\nInputs to prompt rendering:\n\n- `workflow.prompt_template`\n- normalized `issue` object\n- optional `attempt` integer (retry/continuation metadata)\n\n### 12.2 Rendering Rules\n\n- Render with strict variable checking.\n- Render with strict filter checking.\n- Convert issue object keys to strings for template compatibility.\n- Preserve nested arrays/maps (labels, blockers) so templates can iterate.\n\n### 12.3 Retry/Continuation Semantics\n\n`attempt` should be passed to the template because the workflow prompt may provide different\ninstructions for:\n\n- first run (`attempt` null or absent)\n- continuation run after a successful prior session\n- retry after error/timeout/stall\n\n### 12.4 Failure Semantics\n\nIf prompt rendering fails:\n\n- Fail the run attempt immediately.\n- Let the orchestrator treat it like any other worker failure and decide retry behavior.\n\n## 13. Logging, Status, and Observability\n\n### 13.1 Logging Conventions\n\nRequired context fields for issue-related logs:\n\n- `issue_id`\n- `issue_identifier`\n\nRequired context for coding-agent session lifecycle logs:\n\n- `session_id`\n\nMessage formatting requirements:\n\n- Use stable `key=value` phrasing.\n- Include action outcome (`completed`, `failed`, `retrying`, etc.).\n- Include concise failure reason when present.\n- Avoid logging large raw payloads unless necessary.\n\n### 13.2 Logging Outputs and Sinks\n\nThe spec does not prescribe where logs must go (stderr, file, remote sink, etc.).\n\nRequirements:\n\n- Operators must be able to see startup/validation/dispatch failures without attaching a debugger.\n- Implementations may write to one or more sinks.\n- If a configured log sink fails, the service should continue running when possible and emit an\n  operator-visible warning through any remaining sink.\n\n### 13.3 Runtime Snapshot / Monitoring Interface (Optional but Recommended)\n\nIf the implementation exposes a synchronous runtime snapshot (for dashboards or monitoring), it\nshould return:\n\n- `running` (list of running session rows)\n- each running row should include `turn_count`\n- `retrying` (list of retry queue rows)\n- `codex_totals`\n  - `input_tokens`\n  - `output_tokens`\n  - `total_tokens`\n  - `seconds_running` (aggregate runtime seconds as of snapshot time, including active sessions)\n- `rate_limits` (latest coding-agent rate limit payload, if available)\n\nRecommended snapshot error modes:\n\n- `timeout`\n- `unavailable`\n\n### 13.4 Optional Human-Readable Status Surface\n\nA human-readable status surface (terminal output, dashboard, etc.) is optional and\nimplementation-defined.\n\nIf present, it should draw from orchestrator state/metrics only and must not be required for\ncorrectness.\n\n### 13.5 Session Metrics and Token Accounting\n\nToken accounting rules:\n\n- Agent events may include token counts in multiple payload shapes.\n- Prefer absolute thread totals when available, such as:\n  - `thread/tokenUsage/updated` payloads\n  - `total_token_usage` within token-count wrapper events\n- Ignore delta-style payloads such as `last_token_usage` for dashboard/API totals.\n- Extract input/output/total token counts leniently from common field names within the selected\n  payload.\n- For absolute totals, track deltas relative to last reported totals to avoid double-counting.\n- Do not treat generic `usage` maps as cumulative totals unless the event type defines them that\n  way.\n- Accumulate aggregate totals in orchestrator state.\n\nRuntime accounting:\n\n- Runtime should be reported as a live aggregate at snapshot/render time.\n- Implementations may maintain a cumulative counter for ended sessions and add active-session\n  elapsed time derived from `running` entries (for example `started_at`) when producing a\n  snapshot/status view.\n- Add run duration seconds to the cumulative ended-session runtime when a session ends (normal exit\n  or cancellation/termination).\n- Continuous background ticking of runtime totals is not required.\n\nRate-limit tracking:\n\n- Track the latest rate-limit payload seen in any agent update.\n- Any human-readable presentation of rate-limit data is implementation-defined.\n\n### 13.6 Humanized Agent Event Summaries (Optional)\n\nHumanized summaries of raw agent protocol events are optional.\n\nIf implemented:\n\n- Treat them as observability-only output.\n- Do not make orchestrator logic depend on humanized strings.\n\n### 13.7 Optional HTTP Server Extension\n\nThis section defines an optional HTTP interface for observability and operational control.\n\nIf implemented:\n\n- The HTTP server is an extension and is not required for conformance.\n- The implementation may serve server-rendered HTML or a client-side application for the dashboard.\n- The dashboard/API must be observability/control surfaces only and must not become required for\n  orchestrator correctness.\n\nEnablement (extension):\n\n- Start the HTTP server when a CLI `--port` argument is provided.\n- Start the HTTP server when `server.port` is present in `WORKFLOW.md` front matter.\n- `server.port` is extension configuration and is intentionally not part of the core front-matter\n  schema in Section 5.3.\n- Precedence: CLI `--port` overrides `server.port` when both are present.\n- `server.port` must be an integer. Positive values bind that port. `0` may be used to request an\n  ephemeral port for local development and tests.\n- Implementations should bind loopback by default (`127.0.0.1` or host equivalent) unless explicitly\n  configured otherwise.\n- Changes to HTTP listener settings (for example `server.port`) do not need to hot-rebind;\n  restart-required behavior is conformant.\n\n#### 13.7.1 Human-Readable Dashboard (`/`)\n\n- Host a human-readable dashboard at `/`.\n- The returned document should depict the current state of the system (for example active sessions,\n  retry delays, token consumption, runtime totals, recent events, and health/error indicators).\n- It is up to the implementation whether this is server-generated HTML or a client-side app that\n  consumes the JSON API below.\n\n#### 13.7.2 JSON REST API (`/api/v1/*`)\n\nProvide a JSON REST API under `/api/v1/*` for current runtime state and operational debugging.\n\nMinimum endpoints:\n\n- `GET /api/v1/state`\n  - Returns a summary view of the current system state (running sessions, retry queue/delays,\n    aggregate token/runtime totals, latest rate limits, and any additional tracked summary fields).\n  - Suggested response shape:\n\n    ```json\n    {\n      \"generated_at\": \"2026-02-24T20:15:30Z\",\n      \"counts\": {\n        \"running\": 2,\n        \"retrying\": 1\n      },\n      \"running\": [\n        {\n          \"issue_id\": \"abc123\",\n          \"issue_identifier\": \"MT-649\",\n          \"state\": \"In Progress\",\n          \"session_id\": \"thread-1-turn-1\",\n          \"turn_count\": 7,\n          \"last_event\": \"turn_completed\",\n          \"last_message\": \"\",\n          \"started_at\": \"2026-02-24T20:10:12Z\",\n          \"last_event_at\": \"2026-02-24T20:14:59Z\",\n          \"tokens\": {\n            \"input_tokens\": 1200,\n            \"output_tokens\": 800,\n            \"total_tokens\": 2000\n          }\n        }\n      ],\n      \"retrying\": [\n        {\n          \"issue_id\": \"def456\",\n          \"issue_identifier\": \"MT-650\",\n          \"attempt\": 3,\n          \"due_at\": \"2026-02-24T20:16:00Z\",\n          \"error\": \"no available orchestrator slots\"\n        }\n      ],\n      \"codex_totals\": {\n        \"input_tokens\": 5000,\n        \"output_tokens\": 2400,\n        \"total_tokens\": 7400,\n        \"seconds_running\": 1834.2\n      },\n      \"rate_limits\": null\n    }\n    ```\n\n- `GET /api/v1/<issue_identifier>`\n  - Returns issue-specific runtime/debug details for the identified issue, including any information\n    the implementation tracks that is useful for debugging.\n  - Suggested response shape:\n\n    ```json\n    {\n      \"issue_identifier\": \"MT-649\",\n      \"issue_id\": \"abc123\",\n      \"status\": \"running\",\n      \"workspace\": {\n        \"path\": \"/tmp/symphony_workspaces/MT-649\"\n      },\n      \"attempts\": {\n        \"restart_count\": 1,\n        \"current_retry_attempt\": 2\n      },\n      \"running\": {\n        \"session_id\": \"thread-1-turn-1\",\n        \"turn_count\": 7,\n        \"state\": \"In Progress\",\n        \"started_at\": \"2026-02-24T20:10:12Z\",\n        \"last_event\": \"notification\",\n        \"last_message\": \"Working on tests\",\n        \"last_event_at\": \"2026-02-24T20:14:59Z\",\n        \"tokens\": {\n          \"input_tokens\": 1200,\n          \"output_tokens\": 800,\n          \"total_tokens\": 2000\n        }\n      },\n      \"retry\": null,\n      \"logs\": {\n        \"codex_session_logs\": [\n          {\n            \"label\": \"latest\",\n            \"path\": \"/var/log/symphony/codex/MT-649/latest.log\",\n            \"url\": null\n          }\n        ]\n      },\n      \"recent_events\": [\n        {\n          \"at\": \"2026-02-24T20:14:59Z\",\n          \"event\": \"notification\",\n          \"message\": \"Working on tests\"\n        }\n      ],\n      \"last_error\": null,\n      \"tracked\": {}\n    }\n    ```\n\n  - If the issue is unknown to the current in-memory state, return `404` with an error response (for\n    example `{\\\"error\\\":{\\\"code\\\":\\\"issue_not_found\\\",\\\"message\\\":\\\"...\\\"}}`).\n\n- `POST /api/v1/refresh`\n  - Queues an immediate tracker poll + reconciliation cycle (best-effort trigger; implementations\n    may coalesce repeated requests).\n  - Suggested request body: empty body or `{}`.\n  - Suggested response (`202 Accepted`) shape:\n\n    ```json\n    {\n      \"queued\": true,\n      \"coalesced\": false,\n      \"requested_at\": \"2026-02-24T20:15:30Z\",\n      \"operations\": [\"poll\", \"reconcile\"]\n    }\n    ```\n\nAPI design notes:\n\n- The JSON shapes above are the recommended baseline for interoperability and debugging ergonomics.\n- Implementations may add fields, but should avoid breaking existing fields within a version.\n- Endpoints should be read-only except for operational triggers like `/refresh`.\n- Unsupported methods on defined routes should return `405 Method Not Allowed`.\n- API errors should use a JSON envelope such as `{\"error\":{\"code\":\"...\",\"message\":\"...\"}}`.\n- If the dashboard is a client-side app, it should consume this API rather than duplicating state\n  logic.\n\n## 14. Failure Model and Recovery Strategy\n\n### 14.1 Failure Classes\n\n1. `Workflow/Config Failures`\n   - Missing `WORKFLOW.md`\n   - Invalid YAML front matter\n   - Unsupported tracker kind or missing tracker credentials/project slug\n   - Missing coding-agent executable\n\n2. `Workspace Failures`\n   - Workspace directory creation failure\n   - Workspace population/synchronization failure (implementation-defined; may come from hooks)\n   - Invalid workspace path configuration\n   - Hook timeout/failure\n\n3. `Agent Session Failures`\n   - Startup handshake failure\n   - Turn failed/cancelled\n   - Turn timeout\n   - User input requested (hard fail)\n   - Subprocess exit\n   - Stalled session (no activity)\n\n4. `Tracker Failures`\n   - API transport errors\n   - Non-200 status\n   - GraphQL errors\n   - malformed payloads\n\n5. `Observability Failures`\n   - Snapshot timeout\n   - Dashboard render errors\n   - Log sink configuration failure\n\n### 14.2 Recovery Behavior\n\n- Dispatch validation failures:\n  - Skip new dispatches.\n  - Keep service alive.\n  - Continue reconciliation where possible.\n\n- Worker failures:\n  - Convert to retries with exponential backoff.\n\n- Tracker candidate-fetch failures:\n  - Skip this tick.\n  - Try again on next tick.\n\n- Reconciliation state-refresh failures:\n  - Keep current workers.\n  - Retry on next tick.\n\n- Dashboard/log failures:\n  - Do not crash the orchestrator.\n\n### 14.3 Partial State Recovery (Restart)\n\nCurrent design is intentionally in-memory for scheduler state.\n\nAfter restart:\n\n- No retry timers are restored from prior process memory.\n- No running sessions are assumed recoverable.\n- Service recovers by:\n  - startup terminal workspace cleanup\n  - fresh polling of active issues\n  - re-dispatching eligible work\n\n### 14.4 Operator Intervention Points\n\nOperators can control behavior by:\n\n- Editing `WORKFLOW.md` (prompt and most runtime settings).\n- `WORKFLOW.md` changes should be detected and re-applied automatically without restart.\n- Changing issue states in the tracker:\n  - terminal state -> running session is stopped and workspace cleaned when reconciled\n  - non-active state -> running session is stopped without cleanup\n- Restarting the service for process recovery or deployment (not as the normal path for applying\n  workflow config changes).\n\n## 15. Security and Operational Safety\n\n### 15.1 Trust Boundary Assumption\n\nEach implementation defines its own trust boundary.\n\nOperational safety requirements:\n\n- Implementations should state clearly whether they are intended for trusted environments, more\n  restrictive environments, or both.\n- Implementations should state clearly whether they rely on auto-approved actions, operator\n  approvals, stricter sandboxing, or some combination of those controls.\n- Workspace isolation and path validation are important baseline controls, but they are not a\n  substitute for whatever approval and sandbox policy an implementation chooses.\n\n### 15.2 Filesystem Safety Requirements\n\nMandatory:\n\n- Workspace path must remain under configured workspace root.\n- Coding-agent cwd must be the per-issue workspace path for the current run.\n- Workspace directory names must use sanitized identifiers.\n\nRecommended additional hardening for ports:\n\n- Run under a dedicated OS user.\n- Restrict workspace root permissions.\n- Mount workspace root on a dedicated volume if possible.\n\n### 15.3 Secret Handling\n\n- Support `$VAR` indirection in workflow config.\n- Do not log API tokens or secret env values.\n- Validate presence of secrets without printing them.\n\n### 15.4 Hook Script Safety\n\nWorkspace hooks are arbitrary shell scripts from `WORKFLOW.md`.\n\nImplications:\n\n- Hooks are fully trusted configuration.\n- Hooks run inside the workspace directory.\n- Hook output should be truncated in logs.\n- Hook timeouts are required to avoid hanging the orchestrator.\n\n### 15.5 Harness Hardening Guidance\n\nRunning Codex agents against repositories, issue trackers, and other inputs that may contain\nsensitive data or externally-controlled content can be dangerous. A permissive deployment can lead\nto data leaks, destructive mutations, or full machine compromise if the agent is induced to execute\nharmful commands or use overly-powerful integrations.\n\nImplementations should explicitly evaluate their own risk profile and harden the execution harness\nwhere appropriate. This specification intentionally does not mandate a single hardening posture, but\nports should not assume that tracker data, repository contents, prompt inputs, or tool arguments are\nfully trustworthy just because they originate inside a normal workflow.\n\nPossible hardening measures include:\n\n- Tightening Codex approval and sandbox settings described elsewhere in this specification instead\n  of running with a maximally permissive configuration.\n- Adding external isolation layers such as OS/container/VM sandboxing, network restrictions, or\n  separate credentials beyond the built-in Codex policy controls.\n- Filtering which Linear issues, projects, teams, labels, or other tracker sources are eligible for\n  dispatch so untrusted or out-of-scope tasks do not automatically reach the agent.\n- Narrowing the optional `linear_graphql` tool so it can only read or mutate data inside the\n  intended project scope, rather than exposing general workspace-wide tracker access.\n- Reducing the set of client-side tools, credentials, filesystem paths, and network destinations\n  available to the agent to the minimum needed for the workflow.\n\nThe correct controls are deployment-specific, but implementations should document them clearly and\ntreat harness hardening as part of the core safety model rather than an optional afterthought.\n\n## 16. Reference Algorithms (Language-Agnostic)\n\n### 16.1 Service Startup\n\n```text\nfunction start_service():\n  configure_logging()\n  start_observability_outputs()\n  start_workflow_watch(on_change=reload_and_reapply_workflow)\n\n  state = {\n    poll_interval_ms: get_config_poll_interval_ms(),\n    max_concurrent_agents: get_config_max_concurrent_agents(),\n    running: {},\n    claimed: set(),\n    retry_attempts: {},\n    completed: set(),\n    codex_totals: {input_tokens: 0, output_tokens: 0, total_tokens: 0, seconds_running: 0},\n    codex_rate_limits: null\n  }\n\n  validation = validate_dispatch_config()\n  if validation is not ok:\n    log_validation_error(validation)\n    fail_startup(validation)\n\n  startup_terminal_workspace_cleanup()\n  schedule_tick(delay_ms=0)\n\n  event_loop(state)\n```\n\n### 16.2 Poll-and-Dispatch Tick\n\n```text\non_tick(state):\n  state = reconcile_running_issues(state)\n\n  validation = validate_dispatch_config()\n  if validation is not ok:\n    log_validation_error(validation)\n    notify_observers()\n    schedule_tick(state.poll_interval_ms)\n    return state\n\n  issues = tracker.fetch_candidate_issues()\n  if issues failed:\n    log_tracker_error()\n    notify_observers()\n    schedule_tick(state.poll_interval_ms)\n    return state\n\n  for issue in sort_for_dispatch(issues):\n    if no_available_slots(state):\n      break\n\n    if should_dispatch(issue, state):\n      state = dispatch_issue(issue, state, attempt=null)\n\n  notify_observers()\n  schedule_tick(state.poll_interval_ms)\n  return state\n```\n\n### 16.3 Reconcile Active Runs\n\n```text\nfunction reconcile_running_issues(state):\n  state = reconcile_stalled_runs(state)\n\n  running_ids = keys(state.running)\n  if running_ids is empty:\n    return state\n\n  refreshed = tracker.fetch_issue_states_by_ids(running_ids)\n  if refreshed failed:\n    log_debug(\"keep workers running\")\n    return state\n\n  for issue in refreshed:\n    if issue.state in terminal_states:\n      state = terminate_running_issue(state, issue.id, cleanup_workspace=true)\n    else if issue.state in active_states:\n      state.running[issue.id].issue = issue\n    else:\n      state = terminate_running_issue(state, issue.id, cleanup_workspace=false)\n\n  return state\n```\n\n### 16.4 Dispatch One Issue\n\n```text\nfunction dispatch_issue(issue, state, attempt):\n  worker = spawn_worker(\n    fn -> run_agent_attempt(issue, attempt, parent_orchestrator_pid) end\n  )\n\n  if worker spawn failed:\n    return schedule_retry(state, issue.id, next_attempt(attempt), {\n      identifier: issue.identifier,\n      error: \"failed to spawn agent\"\n    })\n\n  state.running[issue.id] = {\n    worker_handle,\n    monitor_handle,\n    identifier: issue.identifier,\n    issue,\n    session_id: null,\n    codex_app_server_pid: null,\n    last_codex_message: null,\n    last_codex_event: null,\n    last_codex_timestamp: null,\n    codex_input_tokens: 0,\n    codex_output_tokens: 0,\n    codex_total_tokens: 0,\n    last_reported_input_tokens: 0,\n    last_reported_output_tokens: 0,\n    last_reported_total_tokens: 0,\n    retry_attempt: normalize_attempt(attempt),\n    started_at: now_utc()\n  }\n\n  state.claimed.add(issue.id)\n  state.retry_attempts.remove(issue.id)\n  return state\n```\n\n### 16.5 Worker Attempt (Workspace + Prompt + Agent)\n\n```text\nfunction run_agent_attempt(issue, attempt, orchestrator_channel):\n  workspace = workspace_manager.create_for_issue(issue.identifier)\n  if workspace failed:\n    fail_worker(\"workspace error\")\n\n  if run_hook(\"before_run\", workspace.path) failed:\n    fail_worker(\"before_run hook error\")\n\n  session = app_server.start_session(workspace=workspace.path)\n  if session failed:\n    run_hook_best_effort(\"after_run\", workspace.path)\n    fail_worker(\"agent session startup error\")\n\n  max_turns = config.agent.max_turns\n  turn_number = 1\n\n  while true:\n    prompt = build_turn_prompt(workflow_template, issue, attempt, turn_number, max_turns)\n    if prompt failed:\n      app_server.stop_session(session)\n      run_hook_best_effort(\"after_run\", workspace.path)\n      fail_worker(\"prompt error\")\n\n    turn_result = app_server.run_turn(\n      session=session,\n      prompt=prompt,\n      issue=issue,\n      on_message=(msg) -> send(orchestrator_channel, {codex_update, issue.id, msg})\n    )\n\n    if turn_result failed:\n      app_server.stop_session(session)\n      run_hook_best_effort(\"after_run\", workspace.path)\n      fail_worker(\"agent turn error\")\n\n    refreshed_issue = tracker.fetch_issue_states_by_ids([issue.id])\n    if refreshed_issue failed:\n      app_server.stop_session(session)\n      run_hook_best_effort(\"after_run\", workspace.path)\n      fail_worker(\"issue state refresh error\")\n\n    issue = refreshed_issue[0] or issue\n\n    if issue.state is not active:\n      break\n\n    if turn_number >= max_turns:\n      break\n\n    turn_number = turn_number + 1\n\n  app_server.stop_session(session)\n  run_hook_best_effort(\"after_run\", workspace.path)\n\n  exit_normal()\n```\n\n### 16.6 Worker Exit and Retry Handling\n\n```text\non_worker_exit(issue_id, reason, state):\n  running_entry = state.running.remove(issue_id)\n  state = add_runtime_seconds_to_totals(state, running_entry)\n\n  if reason == normal:\n    state.completed.add(issue_id)  # bookkeeping only\n    state = schedule_retry(state, issue_id, 1, {\n      identifier: running_entry.identifier,\n      delay_type: continuation\n    })\n  else:\n    state = schedule_retry(state, issue_id, next_attempt_from(running_entry), {\n      identifier: running_entry.identifier,\n      error: format(\"worker exited: %reason\")\n    })\n\n  notify_observers()\n  return state\n```\n\n```text\non_retry_timer(issue_id, state):\n  retry_entry = state.retry_attempts.pop(issue_id)\n  if missing:\n    return state\n\n  candidates = tracker.fetch_candidate_issues()\n  if fetch failed:\n    return schedule_retry(state, issue_id, retry_entry.attempt + 1, {\n      identifier: retry_entry.identifier,\n      error: \"retry poll failed\"\n    })\n\n  issue = find_by_id(candidates, issue_id)\n  if issue is null:\n    state.claimed.remove(issue_id)\n    return state\n\n  if available_slots(state) == 0:\n    return schedule_retry(state, issue_id, retry_entry.attempt + 1, {\n      identifier: issue.identifier,\n      error: \"no available orchestrator slots\"\n    })\n\n  return dispatch_issue(issue, state, attempt=retry_entry.attempt)\n```\n\n## 17. Test and Validation Matrix\n\nA conforming implementation should include tests that cover the behaviors defined in this\nspecification.\n\nValidation profiles:\n\n- `Core Conformance`: deterministic tests required for all conforming implementations.\n- `Extension Conformance`: required only for optional features that an implementation chooses to\n  ship.\n- `Real Integration Profile`: environment-dependent smoke/integration checks recommended before\n  production use.\n\nUnless otherwise noted, Sections 17.1 through 17.7 are `Core Conformance`. Bullets that begin with\n`If ... is implemented` are `Extension Conformance`.\n\n### 17.1 Workflow and Config Parsing\n\n- Workflow file path precedence:\n  - explicit runtime path is used when provided\n  - cwd default is `WORKFLOW.md` when no explicit runtime path is provided\n- Workflow file changes are detected and trigger re-read/re-apply without restart\n- Invalid workflow reload keeps last known good effective configuration and emits an\n  operator-visible error\n- Missing `WORKFLOW.md` returns typed error\n- Invalid YAML front matter returns typed error\n- Front matter non-map returns typed error\n- Config defaults apply when optional values are missing\n- `tracker.kind` validation enforces currently supported kind (`linear`)\n- `tracker.api_key` works (including `$VAR` indirection)\n- `$VAR` resolution works for tracker API key and path values\n- `~` path expansion works\n- `codex.command` is preserved as a shell command string\n- Per-state concurrency override map normalizes state names and ignores invalid values\n- Prompt template renders `issue` and `attempt`\n- Prompt rendering fails on unknown variables (strict mode)\n\n### 17.2 Workspace Manager and Safety\n\n- Deterministic workspace path per issue identifier\n- Missing workspace directory is created\n- Existing workspace directory is reused\n- Existing non-directory path at workspace location is handled safely (replace or fail per\n  implementation policy)\n- Optional workspace population/synchronization errors are surfaced\n- Temporary artifacts (`tmp`, `.elixir_ls`) are removed during prep\n- `after_create` hook runs only on new workspace creation\n- `before_run` hook runs before each attempt and failure/timeouts abort the current attempt\n- `after_run` hook runs after each attempt and failure/timeouts are logged and ignored\n- `before_remove` hook runs on cleanup and failures/timeouts are ignored\n- Workspace path sanitization and root containment invariants are enforced before agent launch\n- Agent launch uses the per-issue workspace path as cwd and rejects out-of-root paths\n\n### 17.3 Issue Tracker Client\n\n- Candidate issue fetch uses active states and project slug\n- Linear query uses the specified project filter field (`slugId`)\n- Empty `fetch_issues_by_states([])` returns empty without API call\n- Pagination preserves order across multiple pages\n- Blockers are normalized from inverse relations of type `blocks`\n- Labels are normalized to lowercase\n- Issue state refresh by ID returns minimal normalized issues\n- Issue state refresh query uses GraphQL ID typing (`[ID!]`) as specified in Section 11.2\n- Error mapping for request errors, non-200, GraphQL errors, malformed payloads\n\n### 17.4 Orchestrator Dispatch, Reconciliation, and Retry\n\n- Dispatch sort order is priority then oldest creation time\n- `Todo` issue with non-terminal blockers is not eligible\n- `Todo` issue with terminal blockers is eligible\n- Active-state issue refresh updates running entry state\n- Non-active state stops running agent without workspace cleanup\n- Terminal state stops running agent and cleans workspace\n- Reconciliation with no running issues is a no-op\n- Normal worker exit schedules a short continuation retry (attempt 1)\n- Abnormal worker exit increments retries with 10s-based exponential backoff\n- Retry backoff cap uses configured `agent.max_retry_backoff_ms`\n- Retry queue entries include attempt, due time, identifier, and error\n- Stall detection kills stalled sessions and schedules retry\n- Slot exhaustion requeues retries with explicit error reason\n- If a snapshot API is implemented, it returns running rows, retry rows, token totals, and rate\n  limits\n- If a snapshot API is implemented, timeout/unavailable cases are surfaced\n\n### 17.5 Coding-Agent App-Server Client\n\n- Launch command uses workspace cwd and invokes `bash -lc <codex.command>`\n- Startup handshake sends `initialize`, `initialized`, `thread/start`, `turn/start`\n- `initialize` includes client identity/capabilities payload required by the targeted Codex\n  app-server protocol\n- Policy-related startup payloads use the implementation's documented approval/sandbox settings\n- `thread/start` and `turn/start` parse nested IDs and emit `session_started`\n- Request/response read timeout is enforced\n- Turn timeout is enforced\n- Partial JSON lines are buffered until newline\n- Stdout and stderr are handled separately; protocol JSON is parsed from stdout only\n- Non-JSON stderr lines are logged but do not crash parsing\n- Command/file-change approvals are handled according to the implementation's documented policy\n- Unsupported dynamic tool calls are rejected without stalling the session\n- User input requests are handled according to the implementation's documented policy and do not\n  stall indefinitely\n- Usage and rate-limit payloads are extracted from nested payload shapes\n- Compatible payload variants for approvals, user-input-required signals, and usage/rate-limit\n  telemetry are accepted when they preserve the same logical meaning\n- If optional client-side tools are implemented, the startup handshake advertises the supported tool\n  specs required for discovery by the targeted app-server version\n- If the optional `linear_graphql` client-side tool extension is implemented:\n  - the tool is advertised to the session\n  - valid `query` / `variables` inputs execute against configured Linear auth\n  - top-level GraphQL `errors` produce `success=false` while preserving the GraphQL body\n  - invalid arguments, missing auth, and transport failures return structured failure payloads\n  - unsupported tool names still fail without stalling the session\n\n### 17.6 Observability\n\n- Validation failures are operator-visible\n- Structured logging includes issue/session context fields\n- Logging sink failures do not crash orchestration\n- Token/rate-limit aggregation remains correct across repeated agent updates\n- If a human-readable status surface is implemented, it is driven from orchestrator state and does\n  not affect correctness\n- If humanized event summaries are implemented, they cover key wrapper/agent event classes without\n  changing orchestrator behavior\n\n### 17.7 CLI and Host Lifecycle\n\n- CLI accepts an optional positional workflow path argument (`path-to-WORKFLOW.md`)\n- CLI uses `./WORKFLOW.md` when no workflow path argument is provided\n- CLI errors on nonexistent explicit workflow path or missing default `./WORKFLOW.md`\n- CLI surfaces startup failure cleanly\n- CLI exits with success when application starts and shuts down normally\n- CLI exits nonzero when startup fails or the host process exits abnormally\n\n### 17.8 Real Integration Profile (Recommended)\n\nThese checks are recommended for production readiness and may be skipped in CI when credentials,\nnetwork access, or external service permissions are unavailable.\n\n- A real tracker smoke test can be run with valid credentials supplied by `LINEAR_API_KEY` or a\n  documented local bootstrap mechanism (for example `~/.linear_api_key`).\n- Real integration tests should use isolated test identifiers/workspaces and clean up tracker\n  artifacts when practical.\n- A skipped real-integration test should be reported as skipped, not silently treated as passed.\n- If a real-integration profile is explicitly enabled in CI or release validation, failures should\n  fail that job.\n\n## 18. Implementation Checklist (Definition of Done)\n\nUse the same validation profiles as Section 17:\n\n- Section 18.1 = `Core Conformance`\n- Section 18.2 = `Extension Conformance`\n- Section 18.3 = `Real Integration Profile`\n\n### 18.1 Required for Conformance\n\n- Workflow path selection supports explicit runtime path and cwd default\n- `WORKFLOW.md` loader with YAML front matter + prompt body split\n- Typed config layer with defaults and `$` resolution\n- Dynamic `WORKFLOW.md` watch/reload/re-apply for config and prompt\n- Polling orchestrator with single-authority mutable state\n- Issue tracker client with candidate fetch + state refresh + terminal fetch\n- Workspace manager with sanitized per-issue workspaces\n- Workspace lifecycle hooks (`after_create`, `before_run`, `after_run`, `before_remove`)\n- Hook timeout config (`hooks.timeout_ms`, default `60000`)\n- Coding-agent app-server subprocess client with JSON line protocol\n- Codex launch command config (`codex.command`, default `codex app-server`)\n- Strict prompt rendering with `issue` and `attempt` variables\n- Exponential retry queue with continuation retries after normal exit\n- Configurable retry backoff cap (`agent.max_retry_backoff_ms`, default 5m)\n- Reconciliation that stops runs on terminal/non-active tracker states\n- Workspace cleanup for terminal issues (startup sweep + active transition)\n- Structured logs with `issue_id`, `issue_identifier`, and `session_id`\n- Operator-visible observability (structured logs; optional snapshot/status surface)\n\n### 18.2 Recommended Extensions (Not Required for Conformance)\n\n- Optional HTTP server honors CLI `--port` over `server.port`, uses a safe default bind host, and\n  exposes the baseline endpoints/error semantics in Section 13.7 if shipped.\n- Optional `linear_graphql` client-side tool extension exposes raw Linear GraphQL access through the\n  app-server session using configured Symphony auth.\n- TODO: Persist retry queue and session metadata across process restarts.\n- TODO: Make observability settings configurable in workflow front matter without prescribing UI\n  implementation details.\n- TODO: Add first-class tracker write APIs (comments/state transitions) in the orchestrator instead\n  of only via agent tools.\n- TODO: Add pluggable issue tracker adapters beyond Linear.\n\n### 18.3 Operational Validation Before Production (Recommended)\n\n- Run the `Real Integration Profile` from Section 17.8 with valid credentials and network access.\n- Verify hook execution and workflow path resolution on the target host OS/shell environment.\n- If the optional HTTP server is shipped, verify the configured port behavior and loopback/default\n  bind expectations on the target environment.\n\n## Appendix A. SSH Worker Extension (Optional)\n\nThis appendix describes a common extension profile in which Symphony keeps one central\norchestrator but executes worker runs on one or more remote hosts over SSH.\n\n### A.1 Execution Model\n\n- The orchestrator remains the single source of truth for polling, claims, retries, and\n  reconciliation.\n- `worker.ssh_hosts` provides the candidate SSH destinations for remote execution.\n- Each worker run is assigned to one host at a time, and that host becomes part of the run's\n  effective execution identity along with the issue workspace.\n- `workspace.root` is interpreted on the remote host, not on the orchestrator host.\n- The coding-agent app-server is launched over SSH stdio instead of as a local subprocess, so the\n  orchestrator still owns the session lifecycle even though commands execute remotely.\n- Continuation turns inside one worker lifetime should stay on the same host and workspace.\n- A remote host should satisfy the same basic contract as a local worker environment: reachable\n  shell, writable workspace root, coding-agent executable, and any required auth or repository\n  prerequisites.\n\n### A.2 Scheduling Notes\n\n- SSH hosts may be treated as a pool for dispatch.\n- Implementations may prefer the previously used host on retries when that host is still\n  available.\n- `worker.max_concurrent_agents_per_host` is an optional shared per-host cap across configured SSH\n  hosts.\n- When all SSH hosts are at capacity, dispatch should wait rather than silently falling back to a\n  different execution mode.\n- Implementations may fail over to another host when the original host is unavailable before work\n  has meaningfully started.\n- Once a run has already produced side effects, a transparent rerun on another host should be\n  treated as a new attempt, not as invisible failover.\n\n### A.3 Problems to Consider\n\n- Remote environment drift:\n  - Each host needs the expected shell environment, coding-agent executable, auth, and repository\n    prerequisites.\n- Workspace locality:\n  - Workspaces are usually host-local, so moving an issue to a different host is typically a cold\n    restart unless shared storage exists.\n- Path and command safety:\n  - Remote path resolution, shell quoting, and workspace-boundary checks matter more once execution\n    crosses a machine boundary.\n- Startup and failover semantics:\n  - Implementations should distinguish host-connectivity/startup failures from in-workspace agent\n    failures so the same ticket is not accidentally re-executed on multiple hosts.\n- Host health and saturation:\n  - A dead or overloaded host should reduce available capacity, not cause duplicate execution or an\n    accidental fallback to local work.\n- Cleanup and observability:\n  - Operators need to know which host owns a run, where its workspace lives, and whether cleanup\n    happened on the right machine.\n"
  },
  {
    "path": "elixir/.formatter.exs",
    "content": "# Used by \"mix format\"\n[\n  inputs: [\"{mix,.formatter}.exs\", \"{config,lib,test}/**/*.{ex,exs}\"],\n  line_length: 200\n]\n"
  },
  {
    "path": "elixir/.gitattributes",
    "content": "test/fixtures/status_dashboard_snapshots/* linguist-generated=true\n"
  },
  {
    "path": "elixir/.gitignore",
    "content": "# The directory Mix will write compiled artifacts to.\n/_build/\n\n# If you run \"mix test --cover\", coverage assets end up here.\n/cover/\n\n# The directory Mix downloads your dependencies sources to.\n/deps/\n\n# Where third-party dependencies like ExDoc output generated docs.\n/doc/\n\n# Temporary files, for example, from tests.\n/tmp/\n\n# Generated browser assets.\n/priv/static/assets/\n\n# Local runtime logs.\n/log/\n/logs/\n\n# If the VM crashes, it generates a dump, let's ignore it too.\nerl_crash.dump\n\n# Elixir language server and tooling.\n/.elixir_ls/\n/.fetch/\n\n# Editor / OS temporary files.\n.DS_Store\n*.swp\n*.swo\n*~\n\n# IDE folders.\n.idea/\n.vscode/\n/bin/\n\n# Local environment and auth artifacts.\n.env\n.env.*\n.secrets\n.credentials\nstatus.txt\n.codex/original-user-prompt.txt\n\n# Also ignore archive artifacts (built via \"mix archive.build\").\n*.ez\n\n# Ignore package tarball (built via \"mix hex.build\").\nsymphony_elixir-*.tar\n"
  },
  {
    "path": "elixir/AGENTS.md",
    "content": "# Symphony Elixir\n\nThis directory contains the Elixir agent orchestration service that polls Linear, creates per-issue workspaces, and runs Codex in app-server mode.\n\n## Environment\n\n- Elixir: `1.19.x` (OTP 28) via `mise`.\n- Install deps: `mix setup`.\n- Main quality gate: `make all` (format check, lint, coverage, dialyzer).\n\n\n## Codebase-Specific Conventions\n\n- Runtime config is loaded from `WORKFLOW.md` front matter via `SymphonyElixir.Workflow` and `SymphonyElixir.Config`.\n- Keep the implementation aligned with [`../SPEC.md`](../SPEC.md) where practical.\n  - The implementation may be a superset of the spec.\n  - The implementation must not conflict with the spec.\n  - If implementation changes meaningfully alter the intended behavior, update the spec in the same\n    change where practical so the spec stays current.\n- Prefer adding config access through `SymphonyElixir.Config` instead of ad-hoc env reads.\n- Workspace safety is critical:\n  - Never run Codex turn cwd in source repo.\n  - Workspaces must stay under configured workspace root.\n- Orchestrator behavior is stateful and concurrency-sensitive; preserve retry, reconciliation, and cleanup semantics.\n- Follow `docs/logging.md` for logging conventions and required issue/session context fields.\n\n## Tests and Validation\n\nRun targeted tests while iterating, then run full gates before handoff.\n\n```bash\nmake all\n```\n\n## Required Rules\n\n- Public functions (`def`) in `lib/` must have an adjacent `@spec`.\n- `defp` specs are optional.\n- `@impl` callback implementations are exempt from local `@spec` requirement.\n- Keep changes narrowly scoped; avoid unrelated refactors.\n- Follow existing module/style patterns in `lib/symphony_elixir/*`.\n\nValidation command:\n\n```bash\nmix specs.check\n```\n\n## PR Requirements\n\n- PR body must follow `../.github/pull_request_template.md` exactly.\n- Validate PR body locally when needed:\n\n```bash\nmix pr_body.check --file /path/to/pr_body.md\n```\n\n## Docs Update Policy\n\nIf behavior/config changes, update docs in the same PR:\n\n- `../README.md` for project concept and goals.\n- `README.md` for Elixir implementation and run instructions.\n- `WORKFLOW.md` for workflow/config contract changes.\n"
  },
  {
    "path": "elixir/Makefile",
    "content": ".PHONY: help all setup deps build fmt fmt-check lint test coverage ci dialyzer e2e\n\nMIX ?= mix\n\nhelp:\n\t@echo \"Targets: setup, deps, fmt, fmt-check, lint, test, coverage, dialyzer, e2e, ci\"\n\nsetup:\n\t$(MIX) setup\n\ndeps:\n\t$(MIX) deps.get\n\nbuild:\n\t$(MIX) build\n\nfmt:\n\t$(MIX) format\n\nfmt-check:\n\t$(MIX) format --check-formatted\n\nlint:\n\t$(MIX) lint\n\ncoverage:\n\t$(MIX) test --cover\n\ntest:\n\t$(MIX) test\n\ndialyzer:\n\t$(MIX) deps.get\n\t$(MIX) dialyzer --format short\n\ne2e:\n\tSYMPHONY_RUN_LIVE_E2E=1 $(MIX) test test/symphony_elixir/live_e2e_test.exs\n\nci:\n\t$(MAKE) setup\n\t$(MAKE) build\n\t$(MAKE) fmt-check\n\t$(MAKE) lint\n\t$(MAKE) coverage\n\t$(MAKE) dialyzer\n\nall: ci\n"
  },
  {
    "path": "elixir/README.md",
    "content": "# Symphony Elixir\n\nThis directory contains the current Elixir/OTP implementation of Symphony, based on\n[`SPEC.md`](../SPEC.md) at the repository root.\n\n> [!WARNING]\n> Symphony Elixir is prototype software intended for evaluation only and is presented as-is.\n> We recommend implementing your own hardened version based on `SPEC.md`.\n\n## Screenshot\n\n![Symphony Elixir screenshot](../.github/media/elixir-screenshot.png)\n\n## How it works\n\n1. Polls Linear for candidate work\n2. Creates a workspace per issue\n3. Launches Codex in [App Server mode](https://developers.openai.com/codex/app-server/) inside the\n   workspace\n4. Sends a workflow prompt to Codex\n5. Keeps Codex working on the issue until the work is done\n\nDuring app-server sessions, Symphony also serves a client-side `linear_graphql` tool so that repo\nskills can make raw Linear GraphQL calls.\n\nIf a claimed issue moves to a terminal state (`Done`, `Closed`, `Cancelled`, or `Duplicate`),\nSymphony stops the active agent for that issue and cleans up matching workspaces.\n\n## How to use it\n\n1. Make sure your codebase is set up to work well with agents: see\n   [Harness engineering](https://openai.com/index/harness-engineering/).\n2. Get a new personal token in Linear via Settings → Security & access → Personal API keys, and\n   set it as the `LINEAR_API_KEY` environment variable.\n3. Copy this directory's `WORKFLOW.md` to your repo.\n4. Optionally copy the `commit`, `push`, `pull`, `land`, and `linear` skills to your repo.\n   - The `linear` skill expects Symphony's `linear_graphql` app-server tool for raw Linear GraphQL\n     operations such as comment editing or upload flows.\n5. Customize the copied `WORKFLOW.md` file for your project.\n   - To get your project's slug, right-click the project and copy its URL. The slug is part of the\n     URL.\n   - When creating a workflow based on this repo, note that it depends on non-standard Linear\n     issue statuses: \"Rework\", \"Human Review\", and \"Merging\". You can customize them in\n     Team Settings → Workflow in Linear.\n6. Follow the instructions below to install the required runtime dependencies and start the service.\n\n## Prerequisites\n\nWe recommend using [mise](https://mise.jdx.dev/) to manage Elixir/Erlang versions.\n\n```bash\nmise install\nmise exec -- elixir --version\n```\n\n## Run\n\n```bash\ngit clone https://github.com/openai/symphony\ncd symphony/elixir\nmise trust\nmise install\nmise exec -- mix setup\nmise exec -- mix build\nmise exec -- ./bin/symphony ./WORKFLOW.md\n```\n\n## Configuration\n\nPass a custom workflow file path to `./bin/symphony` when starting the service:\n\n```bash\n./bin/symphony /path/to/custom/WORKFLOW.md\n```\n\nIf no path is passed, Symphony defaults to `./WORKFLOW.md`.\n\nOptional flags:\n\n- `--logs-root` tells Symphony to write logs under a different directory (default: `./log`)\n- `--port` also starts the Phoenix observability service (default: disabled)\n\nThe `WORKFLOW.md` file uses YAML front matter for configuration, plus a Markdown body used as the\nCodex session prompt.\n\nMinimal example:\n\n```md\n---\ntracker:\n  kind: linear\n  project_slug: \"...\"\nworkspace:\n  root: ~/code/workspaces\nhooks:\n  after_create: |\n    git clone git@github.com:your-org/your-repo.git .\nagent:\n  max_concurrent_agents: 10\n  max_turns: 20\ncodex:\n  command: codex app-server\n---\n\nYou are working on a Linear issue {{ issue.identifier }}.\n\nTitle: {{ issue.title }} Body: {{ issue.description }}\n```\n\nNotes:\n\n- If a value is missing, defaults are used.\n- Safer Codex defaults are used when policy fields are omitted:\n  - `codex.approval_policy` defaults to `{\"reject\":{\"sandbox_approval\":true,\"rules\":true,\"mcp_elicitations\":true}}`\n  - `codex.thread_sandbox` defaults to `workspace-write`\n  - `codex.turn_sandbox_policy` defaults to a `workspaceWrite` policy rooted at the current issue workspace\n- 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.\n- Supported `codex.thread_sandbox` values: `read-only`, `workspace-write`, `danger-full-access`.\n- When `codex.turn_sandbox_policy` is set explicitly, Symphony passes the map through to Codex\n  unchanged. Compatibility then depends on the targeted Codex app-server version rather than local\n  Symphony validation.\n- `agent.max_turns` caps how many back-to-back Codex turns Symphony will run in a single agent\n  invocation when a turn completes normally but the issue is still in an active state. Default: `20`.\n- If the Markdown body is blank, Symphony uses a default prompt template that includes the issue\n  identifier, title, and body.\n- Use `hooks.after_create` to bootstrap a fresh workspace. For a Git-backed repo, you can run\n  `git clone ... .` there, along with any other setup commands you need.\n- If a hook needs `mise exec` inside a freshly cloned workspace, trust the repo config and fetch\n  the project dependencies in `hooks.after_create` before invoking `mise` later from other hooks.\n- `tracker.api_key` reads from `LINEAR_API_KEY` when unset or when value is `$LINEAR_API_KEY`.\n- For path values, `~` is expanded to the home directory.\n- For env-backed path values, use `$VAR`. `workspace.root` resolves `$VAR` before path handling,\n  while `codex.command` stays a shell command string and any `$VAR` expansion there happens in the\n  launched shell.\n\n```yaml\ntracker:\n  api_key: $LINEAR_API_KEY\nworkspace:\n  root: $SYMPHONY_WORKSPACE_ROOT\nhooks:\n  after_create: |\n    git clone --depth 1 \"$SOURCE_REPO_URL\" .\ncodex:\n  command: \"$CODEX_BIN app-server --model gpt-5.3-codex\"\n```\n\n- If `WORKFLOW.md` is missing or has invalid YAML at startup, Symphony does not boot.\n- If a later reload fails, Symphony keeps running with the last known good workflow and logs the\n  reload error until the file is fixed.\n- `server.port` or CLI `--port` enables the optional Phoenix LiveView dashboard and JSON API at\n  `/`, `/api/v1/state`, `/api/v1/<issue_identifier>`, and `/api/v1/refresh`.\n\n## Web dashboard\n\nThe observability UI now runs on a minimal Phoenix stack:\n\n- LiveView for the dashboard at `/`\n- JSON API for operational debugging under `/api/v1/*`\n- Bandit as the HTTP server\n- Phoenix dependency static assets for the LiveView client bootstrap\n\n## Project Layout\n\n- `lib/`: application code and Mix tasks\n- `test/`: ExUnit coverage for runtime behavior\n- `WORKFLOW.md`: in-repo workflow contract used by local runs\n- `../.codex/`: repository-local Codex skills and setup helpers\n\n## Testing\n\n```bash\nmake all\n```\n\nRun the real external end-to-end test only when you want Symphony to create disposable Linear\nresources and launch a real `codex app-server` session:\n\n```bash\ncd elixir\nexport LINEAR_API_KEY=...\nmake e2e\n```\n\nOptional environment variables:\n\n- `SYMPHONY_LIVE_LINEAR_TEAM_KEY` defaults to `SYME2E`\n- `SYMPHONY_LIVE_SSH_WORKER_HOSTS` uses those SSH hosts when set, as a comma-separated list\n\n`make e2e` runs two live scenarios:\n- one with a local worker\n- one with SSH workers\n\nIf `SYMPHONY_LIVE_SSH_WORKER_HOSTS` is unset, the SSH scenario uses `docker compose` to start two\ndisposable SSH workers on `localhost:<port>`. The live test generates a temporary SSH keypair,\nmounts the host `~/.codex/auth.json` into each worker, verifies that Symphony can talk to them\nover real SSH, then runs the same orchestration flow against those worker addresses. This keeps\nthe transport representative without depending on long-lived external machines.\n\nSet `SYMPHONY_LIVE_SSH_WORKER_HOSTS` if you want `make e2e` to target real SSH hosts instead.\n\nThe live test creates a temporary Linear project and issue, writes a temporary `WORKFLOW.md`, runs\na real agent turn, verifies the workspace side effect, requires Codex to comment on and close the\nLinear issue, then marks the project completed so the run remains visible in Linear.\n\n## FAQ\n\n### Why Elixir?\n\nElixir is built on Erlang/BEAM/OTP, which is great for supervising long-running processes. It has an\nactive ecosystem of tools and libraries. It also supports hot code reloading without stopping\nactively running subagents, which is very useful during development.\n\n### What's the easiest way to set this up for my own codebase?\n\nLaunch `codex` in your repo, give it the URL to the Symphony repo, and ask it to set things up for\nyou.\n\n## License\n\nThis project is licensed under the [Apache License 2.0](../LICENSE).\n"
  },
  {
    "path": "elixir/WORKFLOW.md",
    "content": "---\ntracker:\n  kind: linear\n  project_slug: \"symphony-0c79b11b75ea\"\n  active_states:\n    - Todo\n    - In Progress\n    - Merging\n    - Rework\n  terminal_states:\n    - Closed\n    - Cancelled\n    - Canceled\n    - Duplicate\n    - Done\npolling:\n  interval_ms: 5000\nworkspace:\n  root: ~/code/symphony-workspaces\nhooks:\n  after_create: |\n    git clone --depth 1 https://github.com/openai/symphony .\n    if command -v mise >/dev/null 2>&1; then\n      cd elixir && mise trust && mise exec -- mix deps.get\n    fi\n  before_remove: |\n    cd elixir && mise exec -- mix workspace.before_remove\nagent:\n  max_concurrent_agents: 10\n  max_turns: 20\ncodex:\n  command: codex --config shell_environment_policy.inherit=all --config model_reasoning_effort=xhigh --model gpt-5.3-codex app-server\n  approval_policy: never\n  thread_sandbox: workspace-write\n  turn_sandbox_policy:\n    type: workspaceWrite\n---\n\nYou are working on a Linear ticket `{{ issue.identifier }}`\n\n{% if attempt %}\nContinuation context:\n\n- This is retry attempt #{{ attempt }} because the ticket is still in an active state.\n- Resume from the current workspace state instead of restarting from scratch.\n- Do not repeat already-completed investigation or validation unless needed for new code changes.\n- Do not end the turn while the issue remains in an active state unless you are blocked by missing required permissions/secrets.\n  {% endif %}\n\nIssue context:\nIdentifier: {{ issue.identifier }}\nTitle: {{ issue.title }}\nCurrent status: {{ issue.state }}\nLabels: {{ issue.labels }}\nURL: {{ issue.url }}\n\nDescription:\n{% if issue.description %}\n{{ issue.description }}\n{% else %}\nNo description provided.\n{% endif %}\n\nInstructions:\n\n1. This is an unattended orchestration session. Never ask a human to perform follow-up actions.\n2. 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.\n3. Final message must report completed actions and blockers only. Do not include \"next steps for user\".\n\nWork only in the provided repository copy. Do not touch any other path.\n\n## Prerequisite: Linear MCP or `linear_graphql` tool is available\n\nThe 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.\n\n## Default posture\n\n- Start by determining the ticket's current status, then follow the matching flow for that status.\n- Start every task by opening the tracking workpad comment and bringing it up to date before doing new implementation work.\n- Spend extra effort up front on planning and verification design before implementation.\n- Reproduce first: always confirm the current behavior/issue signal before changing code so the fix target is explicit.\n- Keep ticket metadata current (state, checklist, acceptance criteria, links).\n- Treat a single persistent Linear comment as the source of truth for progress.\n- Use that single workpad comment for all progress and handoff notes; do not post separate \"done\"/summary comments.\n- 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.\n- When meaningful out-of-scope improvements are discovered during execution,\n  file a separate Linear issue instead of expanding scope. The follow-up issue\n  must include a clear title, description, and acceptance criteria, be placed in\n  `Backlog`, be assigned to the same project as the current issue, link the\n  current issue as `related`, and use `blockedBy` when the follow-up depends on\n  the current issue.\n- Move status only when the matching quality bar is met.\n- Operate autonomously end-to-end unless blocked by missing requirements, secrets, or permissions.\n- Use the blocked-access escape hatch only for true external blockers (missing required tools/auth) after exhausting documented fallbacks.\n\n## Related skills\n\n- `linear`: interact with Linear.\n- `commit`: produce clean, logical commits during implementation.\n- `push`: keep remote branch current and publish updates.\n- `pull`: keep branch updated with latest `origin/main` before handoff.\n- `land`: when ticket reaches `Merging`, explicitly open and follow `.codex/skills/land/SKILL.md`, which includes the `land` loop.\n\n## Status map\n\n- `Backlog` -> out of scope for this workflow; do not modify.\n- `Todo` -> queued; immediately transition to `In Progress` before active work.\n  - 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`).\n- `In Progress` -> implementation actively underway.\n- `Human Review` -> PR is attached and validated; waiting on human approval.\n- `Merging` -> approved by human; execute the `land` skill flow (do not call `gh pr merge` directly).\n- `Rework` -> reviewer requested changes; planning + implementation required.\n- `Done` -> terminal state; no further action required.\n\n## Step 0: Determine current ticket state and route\n\n1. Fetch the issue by explicit ticket ID.\n2. Read the current state.\n3. Route to the matching flow:\n   - `Backlog` -> do not modify issue content/state; stop and wait for human to move it to `Todo`.\n   - `Todo` -> immediately move to `In Progress`, then ensure bootstrap workpad comment exists (create if missing), then start execution flow.\n     - If PR is already attached, start by reviewing all open PR comments and deciding required changes vs explicit pushback responses.\n   - `In Progress` -> continue execution flow from current scratchpad comment.\n   - `Human Review` -> wait and poll for decision/review updates.\n   - `Merging` -> on entry, open and follow `.codex/skills/land/SKILL.md`; do not call `gh pr merge` directly.\n   - `Rework` -> run rework flow.\n   - `Done` -> do nothing and shut down.\n4. Check whether a PR already exists for the current branch and whether it is closed.\n   - If a branch PR exists and is `CLOSED` or `MERGED`, treat prior branch work as non-reusable for this run.\n   - Create a fresh branch from `origin/main` and restart execution flow as a new attempt.\n5. For `Todo` tickets, do startup sequencing in this exact order:\n   - `update_issue(..., state: \"In Progress\")`\n   - find/create `## Codex Workpad` bootstrap comment\n   - only then begin analysis/planning/implementation work.\n6. Add a short comment if state and issue content are inconsistent, then proceed with the safest flow.\n\n## Step 1: Start/continue execution (Todo or In Progress)\n\n1.  Find or create a single persistent scratchpad comment for the issue:\n    - Search existing comments for a marker header: `## Codex Workpad`.\n    - Ignore resolved comments while searching; only active/unresolved comments are eligible to be reused as the live workpad.\n    - If found, reuse that comment; do not create a new workpad comment.\n    - If not found, create one workpad comment and use it for all updates.\n    - Persist the workpad comment ID and only write progress updates to that ID.\n2.  If arriving from `Todo`, do not delay on additional status transitions: the issue should already be `In Progress` before this step begins.\n3.  Immediately reconcile the workpad before new edits:\n    - Check off items that are already done.\n    - Expand/fix the plan so it is comprehensive for current scope.\n    - Ensure `Acceptance Criteria` and `Validation` are current and still make sense for the task.\n4.  Start work by writing/updating a hierarchical plan in the workpad comment.\n5.  Ensure the workpad includes a compact environment stamp at the top as a code fence line:\n    - Format: `<host>:<abs-workdir>@<short-sha>`\n    - Example: `devbox-01:/home/dev-user/code/symphony-workspaces/MT-32@7bdde33bc`\n    - Do not include metadata already inferable from Linear issue fields (`issue ID`, `status`, `branch`, `PR link`).\n6.  Add explicit acceptance criteria and TODOs in checklist form in the same comment.\n    - If changes are user-facing, include a UI walkthrough acceptance criterion that describes the end-to-end user path to validate.\n    - 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).\n    - 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).\n7.  Run a principal-style self-review of the plan and refine it in the comment.\n8.  Before implementing, capture a concrete reproduction signal and record it in the workpad `Notes` section (command/output, screenshot, or deterministic UI behavior).\n9.  Run the `pull` skill to sync with latest `origin/main` before any code edits, then record the pull/sync result in the workpad `Notes`.\n    - Include a `pull skill evidence` note with:\n      - merge source(s),\n      - result (`clean` or `conflicts resolved`),\n      - resulting `HEAD` short SHA.\n10. Compact context and proceed to execution.\n\n## PR feedback sweep protocol (required)\n\nWhen a ticket has an attached PR, run this protocol before moving to `Human Review`:\n\n1. Identify the PR number from issue links/attachments.\n2. Gather feedback from all channels:\n   - Top-level PR comments (`gh pr view --comments`).\n   - Inline review comments (`gh api repos/<owner>/<repo>/pulls/<pr>/comments`).\n   - Review summaries/states (`gh pr view --json reviews`).\n3. Treat every actionable reviewer comment (human or bot), including inline review comments, as blocking until one of these is true:\n   - code/test/docs updated to address it, or\n   - explicit, justified pushback reply is posted on that thread.\n4. Update the workpad plan/checklist to include each feedback item and its resolution status.\n5. Re-run validation after feedback-driven changes and push updates.\n6. Repeat this sweep until there are no outstanding actionable comments.\n\n## Blocked-access escape hatch (required behavior)\n\nUse this only when completion is blocked by missing required tools or missing auth/permissions that cannot be resolved in-session.\n\n- GitHub is **not** a valid blocker by default. Always try fallback strategies first (alternate remote/auth mode, then continue publish/review flow).\n- Do not move to `Human Review` for GitHub access/auth until all fallback strategies have been attempted and documented in the workpad.\n- 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:\n  - what is missing,\n  - why it blocks required acceptance/validation,\n  - exact human action needed to unblock.\n- Keep the brief concise and action-oriented; do not add extra top-level comments outside the workpad.\n\n## Step 2: Execution phase (Todo -> In Progress -> Human Review)\n\n1.  Determine current repo state (`branch`, `git status`, `HEAD`) and verify the kickoff `pull` sync result is already recorded in the workpad before implementation continues.\n2.  If current issue state is `Todo`, move it to `In Progress`; otherwise leave the current state unchanged.\n3.  Load the existing workpad comment and treat it as the active execution checklist.\n    - Edit it liberally whenever reality changes (scope, risks, validation approach, discovered tasks).\n4.  Implement against the hierarchical TODOs and keep the comment current:\n    - Check off completed items.\n    - Add newly discovered items in the appropriate section.\n    - Keep parent/child structure intact as scope evolves.\n    - Update the workpad immediately after each meaningful milestone (for example: reproduction complete, code change landed, validation run, review feedback addressed).\n    - Never leave completed work unchecked in the plan.\n    - 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.\n5.  Run validation/tests required for the scope.\n    - Mandatory gate: execute all ticket-provided `Validation`/`Test Plan`/ `Testing` requirements when present; treat unmet items as incomplete work.\n    - Prefer a targeted proof that directly demonstrates the behavior you changed.\n    - 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.\n    - Revert every temporary proof edit before commit/push.\n    - Document these temporary proof steps and outcomes in the workpad `Validation`/`Notes` sections so reviewers can follow the evidence.\n    - If app-touching, run `launch-app` validation and capture/upload media via `github-pr-media` before handoff.\n6.  Re-check all acceptance criteria and close any gaps.\n7.  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.\n8.  Attach PR URL to the issue (prefer attachment; use the workpad comment only if attachment is unavailable).\n    - Ensure the GitHub PR has label `symphony` (add it if missing).\n9.  Merge latest `origin/main` into branch, resolve conflicts, and rerun checks.\n10. Update the workpad comment with final checklist status and validation notes.\n    - Mark completed plan/acceptance/validation checklist items as checked.\n    - Add final handoff notes (commit + validation summary) in the same workpad comment.\n    - Do not include PR URL in the workpad comment; keep PR linkage on the issue via attachment/link fields.\n    - Add a short `### Confusions` section at the bottom when any part of task execution was unclear/confusing, with concise bullets.\n    - Do not post any additional completion summary comment.\n11. Before moving to `Human Review`, poll PR feedback and checks:\n    - Read the PR `Manual QA Plan` comment (when present) and use it to sharpen UI/runtime test coverage for the current change.\n    - Run the full PR feedback sweep protocol.\n    - Confirm PR checks are passing (green) after the latest changes.\n    - Confirm every required ticket-provided validation/test-plan item is explicitly marked complete in the workpad.\n    - Repeat this check-address-verify loop until no outstanding comments remain and checks are fully passing.\n    - Re-open and refresh the workpad before state transition so `Plan`, `Acceptance Criteria`, and `Validation` exactly match completed work.\n12. Only then move issue to `Human Review`.\n    - 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.\n13. For `Todo` tickets that already had a PR attached at kickoff:\n    - Ensure all existing PR feedback was reviewed and resolved, including inline review comments (code changes or explicit, justified pushback response).\n    - Ensure branch was pushed with any required updates.\n    - Then move to `Human Review`.\n\n## Step 3: Human Review and merge handling\n\n1. When the issue is in `Human Review`, do not code or change ticket content.\n2. Poll for updates as needed, including GitHub PR review comments from humans and bots.\n3. If review feedback requires changes, move the issue to `Rework` and follow the rework flow.\n4. If approved, human moves the issue to `Merging`.\n5. 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.\n6. After merge is complete, move the issue to `Done`.\n\n## Step 4: Rework handling\n\n1. Treat `Rework` as a full approach reset, not incremental patching.\n2. Re-read the full issue body and all human comments; explicitly identify what will be done differently this attempt.\n3. Close the existing PR tied to the issue.\n4. Remove the existing `## Codex Workpad` comment from the issue.\n5. Create a fresh branch from `origin/main`.\n6. Start over from the normal kickoff flow:\n   - If current issue state is `Todo`, move it to `In Progress`; otherwise keep the current state.\n   - Create a new bootstrap `## Codex Workpad` comment.\n   - Build a fresh plan/checklist and execute end-to-end.\n\n## Completion bar before Human Review\n\n- Step 1/2 checklist is fully complete and accurately reflected in the single workpad comment.\n- Acceptance criteria and required ticket-provided validation items are complete.\n- Validation/tests are green for the latest commit.\n- PR feedback sweep is complete and no actionable comments remain.\n- PR checks are green, branch is pushed, and PR is linked on the issue.\n- Required PR metadata is present (`symphony` label).\n- If app-touching, runtime validation/media requirements from `App runtime validation (required)` are complete.\n\n## Guardrails\n\n- If the branch PR is already closed/merged, do not reuse that branch or prior implementation state for continuation.\n- For closed/merged branch PRs, create a new branch from `origin/main` and restart from reproduction/planning as if starting fresh.\n- If issue state is `Backlog`, do not modify it; wait for human to move to `Todo`.\n- Do not edit the issue body/description for planning or progress tracking.\n- Use exactly one persistent workpad comment (`## Codex Workpad`) per issue.\n- If comment editing is unavailable in-session, use the update script. Only report blocked if both MCP editing and script-based editing are unavailable.\n- Temporary proof edits are allowed only for local verification and must be reverted before commit.\n- If out-of-scope improvements are found, create a separate Backlog issue rather\n  than expanding current scope, and include a clear\n  title/description/acceptance criteria, same-project assignment, a `related`\n  link to the current issue, and `blockedBy` when the follow-up depends on the\n  current issue.\n- Do not move to `Human Review` unless the `Completion bar before Human Review` is satisfied.\n- In `Human Review`, do not make changes; wait and poll.\n- If state is terminal (`Done`), do nothing and shut down.\n- Keep issue text concise, specific, and reviewer-oriented.\n- If blocked and no workpad exists yet, add one blocker comment describing blocker, impact, and next unblock action.\n\n## Workpad template\n\nUse this exact structure for the persistent workpad comment and keep it updated in place throughout execution:\n\n````md\n## Codex Workpad\n\n```text\n<hostname>:<abs-path>@<short-sha>\n```\n\n### Plan\n\n- [ ] 1\\. Parent task\n  - [ ] 1.1 Child task\n  - [ ] 1.2 Child task\n- [ ] 2\\. Parent task\n\n### Acceptance Criteria\n\n- [ ] Criterion 1\n- [ ] Criterion 2\n\n### Validation\n\n- [ ] targeted tests: `<command>`\n\n### Notes\n\n- <short progress note with timestamp>\n\n### Confusions\n\n- <only include when something was confusing during execution>\n````\n"
  },
  {
    "path": "elixir/config/config.exs",
    "content": "import Config\n\nconfig :phoenix, :json_library, Jason\n\nconfig :symphony_elixir, SymphonyElixirWeb.Endpoint,\n  adapter: Bandit.PhoenixAdapter,\n  url: [host: \"localhost\"],\n  render_errors: [\n    formats: [html: SymphonyElixirWeb.ErrorHTML, json: SymphonyElixirWeb.ErrorJSON],\n    layout: false\n  ],\n  pubsub_server: SymphonyElixir.PubSub,\n  live_view: [signing_salt: \"symphony-live-view\"],\n  secret_key_base: String.duplicate(\"s\", 64),\n  check_origin: false,\n  server: false\n"
  },
  {
    "path": "elixir/docs/logging.md",
    "content": "# Logging Best Practices\n\nThis guide defines logging conventions for Symphony so Codex can diagnose failures quickly.\n\n## Goals\n\n- Make logs searchable by issue and session.\n- Capture enough execution context to identify root cause without reruns.\n- Keep messages stable so dashboards/alerts are reliable.\n\n## Required Context Fields\n\nWhen logging issue-related work, include both identifiers:\n\n- `issue_id`: Linear internal UUID (stable foreign key).\n- `issue_identifier`: human ticket key (for example `MT-620`).\n\nWhen logging Codex execution lifecycle events, include:\n\n- `session_id`: combined Codex thread/turn identifier.\n\n## Message Design\n\n- Use explicit `key=value` pairs in message text for high-signal fields.\n- Prefer deterministic wording for recurring lifecycle events.\n- Include the action outcome (`completed`, `failed`, `retrying`) and the reason/error when available.\n- Avoid logging large payloads unless required for debugging.\n\n## Scope Guidance\n\n- `AgentRunner`: log start/completion/failure with issue context, plus `session_id` when known.\n- `Orchestrator`: log dispatch, retry, terminal/non-active transitions, and worker exits with issue context. Include `session_id` whenever running-entry data has it.\n- `Codex.AppServer`: log session start/completion/error with issue context and `session_id`.\n\n## Checklist For New Logs\n\n- Is this event tied to a Linear issue? Include `issue_id` and `issue_identifier`.\n- Is this event tied to a Codex session? Include `session_id`.\n- Is the failure reason present and concise?\n- Is the message format consistent with existing lifecycle logs?\n"
  },
  {
    "path": "elixir/docs/token_accounting.md",
    "content": "# Codex Token Accounting\n\nThis document explains how Codex reports token usage through the app-server protocol and how Symphony should account for it.\n\nIt is based on the current Codex source in `codex-rs`, especially:\n\n- `app-server/README.md`\n- `protocol/src/protocol.rs`\n- `app-server/src/bespoke_event_handling.rs`\n- `app-server-protocol/src/protocol/v2.rs`\n- `exec/src/event_processor_with_jsonl_output.rs`\n- `state/src/extract.rs`\n\n## Short Version\n\n- `last_token_usage` means \"the latest increment\".\n- `total_token_usage` means \"the cumulative total so far\".\n- `thread/tokenUsage/updated` is the live streaming notification for token usage.\n- `turn/completed` carries final turn state, and turn-level usage is exposed separately from the live thread token stream.\n- Generic `usage` fields are event-specific. Do not assume every `usage` payload is a cumulative thread total.\n\n## Primary Source Semantics\n\nCodex defines `TokenUsageInfo` like this:\n\n```rust\npub struct TokenUsageInfo {\n    pub total_token_usage: TokenUsage,\n    pub last_token_usage: TokenUsage,\n    pub model_context_window: Option<i64>,\n}\n```\n\nThe important behavior is in `append_last_usage`:\n\n```rust\npub fn append_last_usage(&mut self, last: &TokenUsage) {\n    self.total_token_usage.add_assign(last);\n    self.last_token_usage = last.clone();\n}\n```\n\nThat gives the core semantics:\n\n- `last_token_usage`: the newest chunk of usage that was just added\n- `total_token_usage`: the accumulated total after adding that chunk\n\nThis is the most important accounting rule in the Codex source.\n\n## Event Types\n\n### `codex/event/token_count`\n\nCodex core emits token count events containing `TokenUsageInfo`.\n\nThese events can carry:\n\n- `info.total_token_usage`\n- `info.last_token_usage`\n- `info.model_context_window`\n\nSymphony sees these events wrapped inside the app-server message stream.\n\nMeaning:\n\n- `total_token_usage` is an absolute cumulative snapshot\n- `last_token_usage` is the delta that produced that snapshot\n\n### `thread/tokenUsage/updated`\n\nThe app-server converts token count events into a dedicated thread-scoped notification:\n\n```rust\nlet notification = ThreadTokenUsageUpdatedNotification {\n    thread_id: conversation_id.to_string(),\n    turn_id,\n    token_usage,\n};\n```\n\n`ThreadTokenUsage` is defined as:\n\n```rust\npub struct ThreadTokenUsage {\n    pub total: TokenUsageBreakdown,\n    pub last: TokenUsageBreakdown,\n    pub model_context_window: Option<i64>,\n}\n```\n\nAnd it is populated directly from `TokenUsageInfo`:\n\n```rust\nimpl From<CoreTokenUsageInfo> for ThreadTokenUsage {\n    fn from(value: CoreTokenUsageInfo) -> Self {\n        Self {\n            total: value.total_token_usage.into(),\n            last: value.last_token_usage.into(),\n            model_context_window: value.model_context_window,\n        }\n    }\n}\n```\n\nMeaning:\n\n- `thread/tokenUsage/updated` is the canonical live notification for token usage\n- `tokenUsage.total` is an absolute thread total\n- `tokenUsage.last` is the latest increment that produced that total\n\nThe app-server README is explicit: token usage streams separately via `thread/tokenUsage/updated`.\n\n### `turn/completed`\n\nThe app-server README says `turn/completed` carries final turn state and token usage.\n\nThere are two important details:\n\n1. The app-server protocol `turn/completed` notification contains a final `turn` object.\n2. The `exec` event processor also emits a turn-completed event that includes a `usage` struct.\n\nIn the `exec` event processor, the turn-completed usage is built from the most recent captured `total_token_usage`:\n\n```rust\nif let Some(info) = &ev.info {\n    self.last_total_token_usage = Some(info.total_token_usage.clone());\n}\n```\n\nThen on turn completion:\n\n```rust\nlet usage = if let Some(u) = &self.last_total_token_usage {\n    Usage {\n        input_tokens: u.input_tokens,\n        cached_input_tokens: u.cached_input_tokens,\n        output_tokens: u.output_tokens,\n    }\n}\n```\n\nImportant consequence:\n\n- a turn-completed `usage` payload is not the same schema as `ThreadTokenUsage`\n- it should be interpreted in the context of the specific event that emitted it\n- it must not be blindly mixed with `thread/tokenUsage/updated` accounting\n\n### Generic `usage`\n\nCodex uses the word `usage` in multiple places.\n\nThat does not mean all `usage` maps have the same semantics.\n\nExamples:\n\n- `thread/tokenUsage/updated.tokenUsage.total`: absolute cumulative thread total\n- `thread/tokenUsage/updated.tokenUsage.last`: latest delta\n- turn-completed `usage`: event-specific completion usage payload\n\nRule:\n\n- never classify a `usage` map by name alone\n- classify it by event type and payload path\n\n## What The Metrics Mean\n\n### Absolute totals\n\nThese are safe high-water-mark style counters:\n\n- `info.total_token_usage`\n- `tokenUsage.total` on `thread/tokenUsage/updated`\n\nUse these when you want:\n\n- live dashboard totals\n- stable per-thread accumulation\n- recovery after missed intermediate events\n\n### Deltas\n\nThese are incremental additions:\n\n- `info.last_token_usage`\n- `tokenUsage.last` on `thread/tokenUsage/updated`\n\nUse these only when:\n\n- no absolute total is available\n- you are explicitly handling additive updates\n\n### Context window\n\n`model_context_window` is not spend. It is the model's context limit.\n\nCodex also has logic that can \"fill to context window\", which sets:\n\n- `total_token_usage.total_tokens = context_window`\n- `last_token_usage.total_tokens = delta`\n\nSo `total_tokens` can reflect context-window normalization behavior, not just a raw upstream token report.\n\nFor Symphony, `model_context_window` should be displayed or logged separately from spend.\n\n## Recommended Accounting Strategy For Symphony\n\nTrack usage per active Codex thread.\n\nFor each thread, keep:\n\n- `absolute_total`: latest accepted absolute total snapshot\n- `accumulated_total`: the total you expose in UI/API\n- `last_seen_turn_id`\n\n### Preferred source order\n\nWhen a token-related event arrives, use this precedence:\n\n1. `thread/tokenUsage/updated.tokenUsage.total`\n2. `TokenCountEvent.info.total_token_usage`\n\nIgnore these for accounting:\n\n- `thread/tokenUsage/updated.tokenUsage.last`\n- `TokenCountEvent.info.last_token_usage`\n- generic `usage` maps\n- turn-completed `usage`\n\nDo not treat generic `params.usage` as equivalent to a cumulative thread total unless the event type makes that meaning explicit.\n\n### Algorithm\n\n#### If an absolute total is present\n\n- Treat it as a thread-level snapshot.\n- If it is greater than or equal to the stored `absolute_total`, replace the stored absolute total.\n- Set exposed totals from that absolute snapshot.\n- Do not add the corresponding delta again.\n\n#### If no absolute total is present\n\n- Ignore the event for accounting.\n- Keep the last accepted absolute high-water mark unchanged.\n\n### Why this matters\n\nIf 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.\n\n## What Symphony Should And Should Not Do\n\n### Do\n\n- Prefer `thread/tokenUsage/updated` for live reporting.\n- Treat `tokenUsage.total` as authoritative for thread totals.\n- Key accounting by `thread_id`, not just issue id.\n- Expect one thread to span multiple turns when Symphony reuses a live Codex thread.\n\n### Do not\n\n- Do not treat every `usage` map as absolute.\n- Do not count `tokenUsage.last` or `last_token_usage` into dashboard totals.\n- Do not add turn-completed `usage` on top of already-counted live thread totals unless you can prove it represents missing spend.\n- Do not reset accounting just because a new turn starts on the same thread.\n\n## Practical Interpretation For Symphony Logs\n\nWhen reading raw app-server events:\n\n- `codex/event/token_count`\n  - useful if you are inspecting nested `info.total_token_usage`\n- `thread/tokenUsage/updated`\n  - best source for live dashboard and API totals\n- `turn/completed`\n  - best used as end-of-turn state, not as an unconditional additive token event\n\n## Why `total_token_usage` Is The Durable Choice\n\nCodex itself consistently prefers cumulative totals when it needs durable state:\n\n- the state extractor stores `info.total_token_usage.total_tokens`\n- the exec event processor caches the last `total_token_usage` and uses that on turn completion\n\nThat is a strong signal for Symphony:\n\n- use absolute totals as the main accounting surface\n- ignore last/delta values for totals\n\n## Recommended Symphony Documentation Contract\n\nIf Symphony documents token reporting externally, the contract should be:\n\n- Live token totals come from Codex thread-scoped cumulative usage.\n- Incremental usage may also be emitted, but Symphony does not use it for totals.\n- Turn-completed usage is event-specific and should not be assumed to be a fresh additive increment.\n- Reporting is thread-based, and multiple turns can occur on one thread.\n\n## Implementation Checklist\n\n- Prefer `thread/tokenUsage/updated.tokenUsage.total`\n- Fallback to `info.total_token_usage`\n- Ignore `last` for totals\n- Key totals by `thread_id`\n- Do not classify generic `usage` by field name alone\n- Do not double-count turn-completed usage after live updates\n"
  },
  {
    "path": "elixir/lib/mix/tasks/pr_body.check.ex",
    "content": "defmodule Mix.Tasks.PrBody.Check do\n  use Mix.Task\n\n  @shortdoc \"Validate PR body format against the repository PR template\"\n\n  @moduledoc \"\"\"\n  Validates a PR description markdown file against the structure and expectations\n  implied by the repository pull request template.\n\n  Usage:\n\n      mix pr_body.check --file /path/to/pr_body.md\n  \"\"\"\n\n  @template_paths [\n    \".github/pull_request_template.md\",\n    \"../.github/pull_request_template.md\"\n  ]\n\n  @impl Mix.Task\n  def run(args) do\n    {opts, _argv, invalid} = OptionParser.parse(args, strict: [file: :string, help: :boolean], aliases: [h: :help])\n\n    cond do\n      opts[:help] ->\n        Mix.shell().info(@moduledoc)\n\n      invalid != [] ->\n        Mix.raise(\"Invalid option(s): #{inspect(invalid)}\")\n\n      true ->\n        file_path = required_opt(opts, :file)\n\n        with {:ok, template_path, template} <- read_template(),\n             {:ok, body} <- read_file(file_path),\n             {:ok, headings} <- extract_template_headings(template, template_path),\n             :ok <- lint_and_print(template_path, template, body, headings) do\n          Mix.shell().info(\"PR body format OK\")\n        else\n          {:error, message} -> Mix.raise(message)\n        end\n    end\n  end\n\n  defp read_template do\n    case Enum.find_value(@template_paths, &read_template_candidate/1) do\n      {:ok, _path, _template} = result ->\n        result\n\n      nil ->\n        joined_paths = Enum.join(@template_paths, \", \")\n        {:error, \"Unable to read PR template from any of: #{joined_paths}\"}\n    end\n  end\n\n  defp read_template_candidate(path) do\n    case File.read(path) do\n      {:ok, content} -> {:ok, path, content}\n      {:error, _reason} -> nil\n    end\n  end\n\n  defp required_opt(opts, key) do\n    case opts[key] do\n      nil -> Mix.raise(\"Missing required option --#{key}\")\n      value -> value\n    end\n  end\n\n  defp read_file(path) do\n    case File.read(path) do\n      {:ok, content} -> {:ok, content}\n      {:error, reason} -> {:error, \"Unable to read #{path}: #{inspect(reason)}\"}\n    end\n  end\n\n  defp extract_template_headings(template, template_path) do\n    headings =\n      Regex.scan(~r/^\\#{4,6}\\s+.+$/m, template)\n      |> Enum.map(&hd/1)\n\n    if headings == [] do\n      {:error, \"No markdown headings found in #{template_path}\"}\n    else\n      {:ok, headings}\n    end\n  end\n\n  defp lint_and_print(template_path, template, body, headings) do\n    errors = lint(template, body, headings)\n\n    if errors == [] do\n      :ok\n    else\n      Enum.each(errors, fn err -> Mix.shell().error(\"ERROR: #{err}\") end)\n\n      {:error, \"PR body format invalid. Read `#{template_path}` and follow it precisely.\"}\n    end\n  end\n\n  defp lint(template, body, headings) do\n    []\n    |> check_required_headings(body, headings)\n    |> check_order(body, headings)\n    |> check_no_placeholders(body)\n    |> check_sections_from_template(template, body, headings)\n  end\n\n  defp check_required_headings(errors, body, headings) do\n    missing = Enum.filter(headings, fn heading -> heading_position(body, heading) == :nomatch end)\n    errors ++ Enum.map(missing, fn heading -> \"Missing required heading: #{heading}\" end)\n  end\n\n  defp check_order(errors, body, headings) do\n    positions =\n      headings\n      |> Enum.map(&heading_position(body, &1))\n      |> Enum.reject(&(&1 == :nomatch))\n\n    if positions == Enum.sort(positions), do: errors, else: errors ++ [\"Required headings are out of order.\"]\n  end\n\n  defp check_no_placeholders(errors, body) do\n    if String.contains?(body, \"<!--\") do\n      errors ++ [\"PR description still contains template placeholder comments (<!-- ... -->).\"]\n    else\n      errors\n    end\n  end\n\n  defp check_sections_from_template(errors, template, body, headings) do\n    Enum.reduce(headings, errors, fn heading, acc ->\n      template_section = capture_heading_section(template, heading, headings)\n      body_section = capture_heading_section(body, heading, headings)\n\n      cond do\n        is_nil(body_section) ->\n          acc\n\n        String.trim(body_section) == \"\" ->\n          acc ++ [\"Section cannot be empty: #{heading}\"]\n\n        true ->\n          acc\n          |> maybe_require_bullets(heading, template_section, body_section)\n          |> maybe_require_checkboxes(heading, template_section, body_section)\n      end\n    end)\n  end\n\n  defp maybe_require_bullets(errors, heading, template_section, body_section) do\n    requires_bullets = Regex.match?(~r/^- /m, template_section || \"\")\n\n    if requires_bullets and not Regex.match?(~r/^- /m, body_section) do\n      errors ++ [\"Section must include at least one bullet item: #{heading}\"]\n    else\n      errors\n    end\n  end\n\n  defp maybe_require_checkboxes(errors, heading, template_section, body_section) do\n    requires_checkboxes = Regex.match?(~r/^- \\[ \\] /m, template_section || \"\")\n\n    if requires_checkboxes and not Regex.match?(~r/^- \\[[ xX]\\] /m, body_section) do\n      errors ++ [\"Section must include at least one checkbox item: #{heading}\"]\n    else\n      errors\n    end\n  end\n\n  defp heading_position(body, heading) do\n    case :binary.match(body, heading) do\n      {idx, _len} -> idx\n      :nomatch -> :nomatch\n    end\n  end\n\n  defp capture_heading_section(doc, heading, headings) do\n    with {heading_idx, _} <- :binary.match(doc, heading),\n         section_start <- heading_idx + byte_size(heading),\n         true <- section_start + 2 <= byte_size(doc),\n         \"\\n\\n\" <- binary_part(doc, section_start, 2) do\n      extract_section_content(doc, section_start + 2, heading, headings)\n    else\n      :nomatch -> nil\n      false -> \"\"\n      _ -> nil\n    end\n  end\n\n  defp extract_section_content(doc, content_start, heading, headings) do\n    content = binary_part(doc, content_start, byte_size(doc) - content_start)\n\n    case next_heading_offset(content, heading, headings) do\n      nil -> content\n      offset -> binary_part(content, 0, offset)\n    end\n  end\n\n  defp next_heading_offset(content, heading, headings) do\n    headings_after(heading, headings)\n    |> Enum.map(fn marker -> :binary.match(content, marker) end)\n    |> Enum.filter(&(&1 != :nomatch))\n    |> Enum.map(fn {idx, _} -> idx end)\n    |> case do\n      [] -> nil\n      indexes -> Enum.min(indexes)\n    end\n  end\n\n  defp headings_after(current_heading, headings) do\n    headings\n    |> Enum.filter(&(&1 != current_heading))\n    |> Enum.map(&(\"\\n\" <> &1))\n  end\nend\n"
  },
  {
    "path": "elixir/lib/mix/tasks/specs.check.ex",
    "content": "defmodule Mix.Tasks.Specs.Check do\n  use Mix.Task\n\n  alias SymphonyElixir.SpecsCheck\n\n  @moduledoc \"\"\"\n  Enforces adjacent `@spec` declarations for public APIs in `lib/`.\n  \"\"\"\n  @shortdoc \"Fails when public functions in lib/ are missing adjacent @specs\"\n\n  @switches [paths: :keep, exemptions_file: :string]\n  @default_paths [\"lib\"]\n\n  @impl Mix.Task\n  def run(args) do\n    {opts, _argv, _invalid} = OptionParser.parse(args, strict: @switches)\n\n    paths = Keyword.get_values(opts, :paths)\n    scanned_paths = if paths == [], do: @default_paths, else: paths\n\n    exemptions =\n      case Keyword.get(opts, :exemptions_file) do\n        nil -> MapSet.new()\n        path -> load_exemptions(path)\n      end\n\n    findings = SpecsCheck.missing_public_specs(scanned_paths, exemptions: exemptions)\n\n    if findings == [] do\n      Mix.shell().info(\"specs.check: all public functions have @spec or exemption\")\n      :ok\n    else\n      Enum.each(findings, fn finding ->\n        Mix.shell().error(\"#{finding.file}:#{finding.line} missing @spec for #{SpecsCheck.finding_identifier(finding)}\")\n      end)\n\n      Mix.raise(\"specs.check failed with #{length(findings)} missing @spec declaration(s)\")\n    end\n  end\n\n  defp load_exemptions(path) do\n    if File.exists?(path) do\n      path\n      |> File.read!()\n      |> String.split(\"\\n\")\n      |> Enum.map(&String.trim/1)\n      |> Enum.reject(&(&1 == \"\" or String.starts_with?(&1, \"#\")))\n      |> MapSet.new()\n    else\n      MapSet.new()\n    end\n  end\nend\n"
  },
  {
    "path": "elixir/lib/mix/tasks/workspace.before_remove.ex",
    "content": "defmodule Mix.Tasks.Workspace.BeforeRemove do\n  use Mix.Task\n\n  @shortdoc \"Close open GitHub PRs for the current branch before workspace removal\"\n\n  @moduledoc \"\"\"\n  Closes open pull requests for the current Git branch.\n\n  This task is intended for use from the `before_remove` workspace hook.\n\n  Usage:\n\n      mix workspace.before_remove\n      mix workspace.before_remove --branch feature/my-branch\n      mix workspace.before_remove --repo openai/symphony\n  \"\"\"\n\n  @default_repo \"openai/symphony\"\n\n  @impl Mix.Task\n  def run(args) do\n    {opts, _argv, invalid} =\n      OptionParser.parse(args,\n        strict: [branch: :string, help: :boolean, repo: :string],\n        aliases: [h: :help]\n      )\n\n    cond do\n      opts[:help] ->\n        Mix.shell().info(@moduledoc)\n\n      invalid != [] ->\n        Mix.raise(\"Invalid option(s): #{inspect(invalid)}\")\n\n      true ->\n        repo = opts[:repo] || @default_repo\n        branch = opts[:branch] || current_branch()\n\n        maybe_close_open_pull_requests(repo, branch)\n    end\n  end\n\n  defp maybe_close_open_pull_requests(_repo, nil), do: :ok\n\n  defp maybe_close_open_pull_requests(repo, branch) do\n    if gh_available?() and gh_authenticated?() do\n      repo\n      |> list_open_pull_request_numbers(branch)\n      |> Enum.each(&close_pull_request(repo, branch, &1))\n    end\n\n    :ok\n  end\n\n  defp gh_available? do\n    not is_nil(System.find_executable(\"gh\"))\n  end\n\n  defp gh_authenticated? do\n    match?({:ok, _output}, run_command(\"gh\", [\"auth\", \"status\"]))\n  end\n\n  defp list_open_pull_request_numbers(repo, branch) do\n    case run_command(\"gh\", [\n           \"pr\",\n           \"list\",\n           \"--repo\",\n           repo,\n           \"--head\",\n           branch,\n           \"--state\",\n           \"open\",\n           \"--json\",\n           \"number\",\n           \"--jq\",\n           \".[].number\"\n         ]) do\n      {:ok, output} ->\n        output\n        |> String.split(\"\\n\", trim: true)\n        |> Enum.reject(&(&1 == \"\"))\n\n      {:error, _reason} ->\n        []\n    end\n  end\n\n  defp close_pull_request(repo, branch, pr_number) do\n    case run_command(\"gh\", [\n           \"pr\",\n           \"close\",\n           pr_number,\n           \"--repo\",\n           repo,\n           \"--comment\",\n           closing_comment(branch)\n         ]) do\n      {:ok, _output} ->\n        Mix.shell().info(\"Closed PR ##{pr_number} for branch #{branch}\")\n\n      {:error, {status, output}} ->\n        trimmed_output = String.trim(output)\n\n        Mix.shell().error(\"Failed to close PR ##{pr_number} for branch #{branch}: exit #{status}#{format_output(trimmed_output)}\")\n    end\n  end\n\n  defp closing_comment(branch) do\n    \"Closing because the Linear issue for branch #{branch} entered a terminal state without merge.\"\n  end\n\n  defp format_output(\"\"), do: \"\"\n  defp format_output(output), do: \" output=#{inspect(output)}\"\n\n  defp current_branch do\n    case run_command(\"git\", [\"branch\", \"--show-current\"]) do\n      {:ok, output} ->\n        case String.trim(output) do\n          \"\" -> nil\n          branch -> branch\n        end\n\n      {:error, _reason} ->\n        nil\n    end\n  end\n\n  defp run_command(command, args) do\n    case System.find_executable(command) do\n      nil ->\n        {:error, {:enoent, \"\"}}\n\n      path ->\n        case System.cmd(path, args, stderr_to_stdout: true) do\n          {output, 0} -> {:ok, output}\n          {output, status} -> {:error, {status, output}}\n        end\n    end\n  end\nend\n"
  },
  {
    "path": "elixir/lib/symphony_elixir/agent_runner.ex",
    "content": "defmodule SymphonyElixir.AgentRunner do\n  @moduledoc \"\"\"\n  Executes a single Linear issue in its workspace with Codex.\n  \"\"\"\n\n  require Logger\n  alias SymphonyElixir.Codex.AppServer\n  alias SymphonyElixir.{Config, Linear.Issue, PromptBuilder, Tracker, Workspace}\n\n  @type worker_host :: String.t() | nil\n\n  @spec run(map(), pid() | nil, keyword()) :: :ok | no_return()\n  def run(issue, codex_update_recipient \\\\ nil, opts \\\\ []) do\n    # The orchestrator owns host retries so one worker lifetime never hops machines.\n    worker_host = selected_worker_host(Keyword.get(opts, :worker_host), Config.settings!().worker.ssh_hosts)\n\n    Logger.info(\"Starting agent run for #{issue_context(issue)} worker_host=#{worker_host_for_log(worker_host)}\")\n\n    case run_on_worker_host(issue, codex_update_recipient, opts, worker_host) do\n      :ok ->\n        :ok\n\n      {:error, reason} ->\n        Logger.error(\"Agent run failed for #{issue_context(issue)}: #{inspect(reason)}\")\n        raise RuntimeError, \"Agent run failed for #{issue_context(issue)}: #{inspect(reason)}\"\n    end\n  end\n\n  defp run_on_worker_host(issue, codex_update_recipient, opts, worker_host) do\n    Logger.info(\"Starting worker attempt for #{issue_context(issue)} worker_host=#{worker_host_for_log(worker_host)}\")\n\n    case Workspace.create_for_issue(issue, worker_host) do\n      {:ok, workspace} ->\n        send_worker_runtime_info(codex_update_recipient, issue, worker_host, workspace)\n\n        try do\n          with :ok <- Workspace.run_before_run_hook(workspace, issue, worker_host) do\n            run_codex_turns(workspace, issue, codex_update_recipient, opts, worker_host)\n          end\n        after\n          Workspace.run_after_run_hook(workspace, issue, worker_host)\n        end\n\n      {:error, reason} ->\n        {:error, reason}\n    end\n  end\n\n  defp codex_message_handler(recipient, issue) do\n    fn message ->\n      send_codex_update(recipient, issue, message)\n    end\n  end\n\n  defp send_codex_update(recipient, %Issue{id: issue_id}, message)\n       when is_binary(issue_id) and is_pid(recipient) do\n    send(recipient, {:codex_worker_update, issue_id, message})\n    :ok\n  end\n\n  defp send_codex_update(_recipient, _issue, _message), do: :ok\n\n  defp send_worker_runtime_info(recipient, %Issue{id: issue_id}, worker_host, workspace)\n       when is_binary(issue_id) and is_pid(recipient) and is_binary(workspace) do\n    send(\n      recipient,\n      {:worker_runtime_info, issue_id,\n       %{\n         worker_host: worker_host,\n         workspace_path: workspace\n       }}\n    )\n\n    :ok\n  end\n\n  defp send_worker_runtime_info(_recipient, _issue, _worker_host, _workspace), do: :ok\n\n  defp run_codex_turns(workspace, issue, codex_update_recipient, opts, worker_host) do\n    max_turns = Keyword.get(opts, :max_turns, Config.settings!().agent.max_turns)\n    issue_state_fetcher = Keyword.get(opts, :issue_state_fetcher, &Tracker.fetch_issue_states_by_ids/1)\n\n    with {:ok, session} <- AppServer.start_session(workspace, worker_host: worker_host) do\n      try do\n        do_run_codex_turns(session, workspace, issue, codex_update_recipient, opts, issue_state_fetcher, 1, max_turns)\n      after\n        AppServer.stop_session(session)\n      end\n    end\n  end\n\n  defp do_run_codex_turns(app_session, workspace, issue, codex_update_recipient, opts, issue_state_fetcher, turn_number, max_turns) do\n    prompt = build_turn_prompt(issue, opts, turn_number, max_turns)\n\n    with {:ok, turn_session} <-\n           AppServer.run_turn(\n             app_session,\n             prompt,\n             issue,\n             on_message: codex_message_handler(codex_update_recipient, issue)\n           ) do\n      Logger.info(\"Completed agent run for #{issue_context(issue)} session_id=#{turn_session[:session_id]} workspace=#{workspace} turn=#{turn_number}/#{max_turns}\")\n\n      case continue_with_issue?(issue, issue_state_fetcher) do\n        {:continue, refreshed_issue} when turn_number < max_turns ->\n          Logger.info(\"Continuing agent run for #{issue_context(refreshed_issue)} after normal turn completion turn=#{turn_number}/#{max_turns}\")\n\n          do_run_codex_turns(\n            app_session,\n            workspace,\n            refreshed_issue,\n            codex_update_recipient,\n            opts,\n            issue_state_fetcher,\n            turn_number + 1,\n            max_turns\n          )\n\n        {:continue, refreshed_issue} ->\n          Logger.info(\"Reached agent.max_turns for #{issue_context(refreshed_issue)} with issue still active; returning control to orchestrator\")\n\n          :ok\n\n        {:done, _refreshed_issue} ->\n          :ok\n\n        {:error, reason} ->\n          {:error, reason}\n      end\n    end\n  end\n\n  defp build_turn_prompt(issue, opts, 1, _max_turns), do: PromptBuilder.build_prompt(issue, opts)\n\n  defp build_turn_prompt(_issue, _opts, turn_number, max_turns) do\n    \"\"\"\n    Continuation guidance:\n\n    - The previous Codex turn completed normally, but the Linear issue is still in an active state.\n    - This is continuation turn ##{turn_number} of #{max_turns} for the current agent run.\n    - Resume from the current workspace and workpad state instead of restarting from scratch.\n    - The original task instructions and prior turn context are already present in this thread, so do not restate them before acting.\n    - Focus on the remaining ticket work and do not end the turn while the issue stays active unless you are truly blocked.\n    \"\"\"\n  end\n\n  defp continue_with_issue?(%Issue{id: issue_id} = issue, issue_state_fetcher) when is_binary(issue_id) do\n    case issue_state_fetcher.([issue_id]) do\n      {:ok, [%Issue{} = refreshed_issue | _]} ->\n        if active_issue_state?(refreshed_issue.state) do\n          {:continue, refreshed_issue}\n        else\n          {:done, refreshed_issue}\n        end\n\n      {:ok, []} ->\n        {:done, issue}\n\n      {:error, reason} ->\n        {:error, {:issue_state_refresh_failed, reason}}\n    end\n  end\n\n  defp continue_with_issue?(issue, _issue_state_fetcher), do: {:done, issue}\n\n  defp active_issue_state?(state_name) when is_binary(state_name) do\n    normalized_state = normalize_issue_state(state_name)\n\n    Config.settings!().tracker.active_states\n    |> Enum.any?(fn active_state -> normalize_issue_state(active_state) == normalized_state end)\n  end\n\n  defp active_issue_state?(_state_name), do: false\n\n  defp selected_worker_host(nil, []), do: nil\n\n  defp selected_worker_host(preferred_host, configured_hosts) when is_list(configured_hosts) do\n    hosts =\n      configured_hosts\n      |> Enum.map(&String.trim/1)\n      |> Enum.reject(&(&1 == \"\"))\n      |> Enum.uniq()\n\n    case preferred_host do\n      host when is_binary(host) and host != \"\" -> host\n      _ when hosts == [] -> nil\n      _ -> List.first(hosts)\n    end\n  end\n\n  defp worker_host_for_log(nil), do: \"local\"\n  defp worker_host_for_log(worker_host), do: worker_host\n\n  defp normalize_issue_state(state_name) when is_binary(state_name) do\n    state_name\n    |> String.trim()\n    |> String.downcase()\n  end\n\n  defp issue_context(%Issue{id: issue_id, identifier: identifier}) do\n    \"issue_id=#{issue_id} issue_identifier=#{identifier}\"\n  end\nend\n"
  },
  {
    "path": "elixir/lib/symphony_elixir/cli.ex",
    "content": "defmodule SymphonyElixir.CLI do\n  @moduledoc \"\"\"\n  Escript entrypoint for running Symphony with an explicit WORKFLOW.md path.\n  \"\"\"\n\n  alias SymphonyElixir.LogFile\n\n  @acknowledgement_switch :i_understand_that_this_will_be_running_without_the_usual_guardrails\n  @switches [{@acknowledgement_switch, :boolean}, logs_root: :string, port: :integer]\n\n  @type ensure_started_result :: {:ok, [atom()]} | {:error, term()}\n  @type deps :: %{\n          file_regular?: (String.t() -> boolean()),\n          set_workflow_file_path: (String.t() -> :ok | {:error, term()}),\n          set_logs_root: (String.t() -> :ok | {:error, term()}),\n          set_server_port_override: (non_neg_integer() | nil -> :ok | {:error, term()}),\n          ensure_all_started: (-> ensure_started_result())\n        }\n\n  @spec main([String.t()]) :: no_return()\n  def main(args) do\n    case evaluate(args) do\n      :ok ->\n        wait_for_shutdown()\n\n      {:error, message} ->\n        IO.puts(:stderr, message)\n        System.halt(1)\n    end\n  end\n\n  @spec evaluate([String.t()], deps()) :: :ok | {:error, String.t()}\n  def evaluate(args, deps \\\\ runtime_deps()) do\n    case OptionParser.parse(args, strict: @switches) do\n      {opts, [], []} ->\n        with :ok <- require_guardrails_acknowledgement(opts),\n             :ok <- maybe_set_logs_root(opts, deps),\n             :ok <- maybe_set_server_port(opts, deps) do\n          run(Path.expand(\"WORKFLOW.md\"), deps)\n        end\n\n      {opts, [workflow_path], []} ->\n        with :ok <- require_guardrails_acknowledgement(opts),\n             :ok <- maybe_set_logs_root(opts, deps),\n             :ok <- maybe_set_server_port(opts, deps) do\n          run(workflow_path, deps)\n        end\n\n      _ ->\n        {:error, usage_message()}\n    end\n  end\n\n  @spec run(String.t(), deps()) :: :ok | {:error, String.t()}\n  def run(workflow_path, deps) do\n    expanded_path = Path.expand(workflow_path)\n\n    if deps.file_regular?.(expanded_path) do\n      :ok = deps.set_workflow_file_path.(expanded_path)\n\n      case deps.ensure_all_started.() do\n        {:ok, _started_apps} ->\n          :ok\n\n        {:error, reason} ->\n          {:error, \"Failed to start Symphony with workflow #{expanded_path}: #{inspect(reason)}\"}\n      end\n    else\n      {:error, \"Workflow file not found: #{expanded_path}\"}\n    end\n  end\n\n  @spec usage_message() :: String.t()\n  defp usage_message do\n    \"Usage: symphony [--logs-root <path>] [--port <port>] [path-to-WORKFLOW.md]\"\n  end\n\n  @spec runtime_deps() :: deps()\n  defp runtime_deps do\n    %{\n      file_regular?: &File.regular?/1,\n      set_workflow_file_path: &SymphonyElixir.Workflow.set_workflow_file_path/1,\n      set_logs_root: &set_logs_root/1,\n      set_server_port_override: &set_server_port_override/1,\n      ensure_all_started: fn -> Application.ensure_all_started(:symphony_elixir) end\n    }\n  end\n\n  defp maybe_set_logs_root(opts, deps) do\n    case Keyword.get_values(opts, :logs_root) do\n      [] ->\n        :ok\n\n      values ->\n        logs_root = values |> List.last() |> String.trim()\n\n        if logs_root == \"\" do\n          {:error, usage_message()}\n        else\n          :ok = deps.set_logs_root.(Path.expand(logs_root))\n        end\n    end\n  end\n\n  defp require_guardrails_acknowledgement(opts) do\n    if Keyword.get(opts, @acknowledgement_switch, false) do\n      :ok\n    else\n      {:error, acknowledgement_banner()}\n    end\n  end\n\n  @spec acknowledgement_banner() :: String.t()\n  defp acknowledgement_banner do\n    lines = [\n      \"This Symphony implementation is a low key engineering preview.\",\n      \"Codex will run without any guardrails.\",\n      \"SymphonyElixir is not a supported product and is presented as-is.\",\n      \"To proceed, start with `--i-understand-that-this-will-be-running-without-the-usual-guardrails` CLI argument\"\n    ]\n\n    width = Enum.max(Enum.map(lines, &String.length/1))\n    border = String.duplicate(\"─\", width + 2)\n    top = \"╭\" <> border <> \"╮\"\n    bottom = \"╰\" <> border <> \"╯\"\n    spacer = \"│ \" <> String.duplicate(\" \", width) <> \" │\"\n\n    content =\n      [\n        top,\n        spacer\n        | Enum.map(lines, fn line ->\n            \"│ \" <> String.pad_trailing(line, width) <> \" │\"\n          end)\n      ] ++ [spacer, bottom]\n\n    [\n      IO.ANSI.red(),\n      IO.ANSI.bright(),\n      Enum.join(content, \"\\n\"),\n      IO.ANSI.reset()\n    ]\n    |> IO.iodata_to_binary()\n  end\n\n  defp set_logs_root(logs_root) do\n    Application.put_env(:symphony_elixir, :log_file, LogFile.default_log_file(logs_root))\n    :ok\n  end\n\n  defp maybe_set_server_port(opts, deps) do\n    case Keyword.get_values(opts, :port) do\n      [] ->\n        :ok\n\n      values ->\n        port = List.last(values)\n\n        if is_integer(port) and port >= 0 do\n          :ok = deps.set_server_port_override.(port)\n        else\n          {:error, usage_message()}\n        end\n    end\n  end\n\n  defp set_server_port_override(port) when is_integer(port) and port >= 0 do\n    Application.put_env(:symphony_elixir, :server_port_override, port)\n    :ok\n  end\n\n  @spec wait_for_shutdown() :: no_return()\n  defp wait_for_shutdown do\n    case Process.whereis(SymphonyElixir.Supervisor) do\n      nil ->\n        IO.puts(:stderr, \"Symphony supervisor is not running\")\n        System.halt(1)\n\n      pid ->\n        ref = Process.monitor(pid)\n\n        receive do\n          {:DOWN, ^ref, :process, ^pid, reason} ->\n            case reason do\n              :normal -> System.halt(0)\n              _ -> System.halt(1)\n            end\n        end\n    end\n  end\nend\n"
  },
  {
    "path": "elixir/lib/symphony_elixir/codex/app_server.ex",
    "content": "defmodule SymphonyElixir.Codex.AppServer do\n  @moduledoc \"\"\"\n  Minimal client for the Codex app-server JSON-RPC 2.0 stream over stdio.\n  \"\"\"\n\n  require Logger\n  alias SymphonyElixir.{Codex.DynamicTool, Config, PathSafety, SSH}\n\n  @initialize_id 1\n  @thread_start_id 2\n  @turn_start_id 3\n  @port_line_bytes 1_048_576\n  @max_stream_log_bytes 1_000\n  @non_interactive_tool_input_answer \"This is a non-interactive session. Operator input is unavailable.\"\n\n  @type session :: %{\n          port: port(),\n          metadata: map(),\n          approval_policy: String.t() | map(),\n          auto_approve_requests: boolean(),\n          thread_sandbox: String.t(),\n          turn_sandbox_policy: map(),\n          thread_id: String.t(),\n          workspace: Path.t(),\n          worker_host: String.t() | nil\n        }\n\n  @spec run(Path.t(), String.t(), map(), keyword()) :: {:ok, map()} | {:error, term()}\n  def run(workspace, prompt, issue, opts \\\\ []) do\n    with {:ok, session} <- start_session(workspace, opts) do\n      try do\n        run_turn(session, prompt, issue, opts)\n      after\n        stop_session(session)\n      end\n    end\n  end\n\n  @spec start_session(Path.t(), keyword()) :: {:ok, session()} | {:error, term()}\n  def start_session(workspace, opts \\\\ []) do\n    worker_host = Keyword.get(opts, :worker_host)\n\n    with {:ok, expanded_workspace} <- validate_workspace_cwd(workspace, worker_host),\n         {:ok, port} <- start_port(expanded_workspace, worker_host) do\n      metadata = port_metadata(port, worker_host)\n\n      with {:ok, session_policies} <- session_policies(expanded_workspace, worker_host),\n           {:ok, thread_id} <- do_start_session(port, expanded_workspace, session_policies) do\n        {:ok,\n         %{\n           port: port,\n           metadata: metadata,\n           approval_policy: session_policies.approval_policy,\n           auto_approve_requests: session_policies.approval_policy == \"never\",\n           thread_sandbox: session_policies.thread_sandbox,\n           turn_sandbox_policy: session_policies.turn_sandbox_policy,\n           thread_id: thread_id,\n           workspace: expanded_workspace,\n           worker_host: worker_host\n         }}\n      else\n        {:error, reason} ->\n          stop_port(port)\n          {:error, reason}\n      end\n    end\n  end\n\n  @spec run_turn(session(), String.t(), map(), keyword()) :: {:ok, map()} | {:error, term()}\n  def run_turn(\n        %{\n          port: port,\n          metadata: metadata,\n          approval_policy: approval_policy,\n          auto_approve_requests: auto_approve_requests,\n          turn_sandbox_policy: turn_sandbox_policy,\n          thread_id: thread_id,\n          workspace: workspace\n        },\n        prompt,\n        issue,\n        opts \\\\ []\n      ) do\n    on_message = Keyword.get(opts, :on_message, &default_on_message/1)\n\n    tool_executor =\n      Keyword.get(opts, :tool_executor, fn tool, arguments ->\n        DynamicTool.execute(tool, arguments)\n      end)\n\n    case start_turn(port, thread_id, prompt, issue, workspace, approval_policy, turn_sandbox_policy) do\n      {:ok, turn_id} ->\n        session_id = \"#{thread_id}-#{turn_id}\"\n        Logger.info(\"Codex session started for #{issue_context(issue)} session_id=#{session_id}\")\n\n        emit_message(\n          on_message,\n          :session_started,\n          %{\n            session_id: session_id,\n            thread_id: thread_id,\n            turn_id: turn_id\n          },\n          metadata\n        )\n\n        case await_turn_completion(port, on_message, tool_executor, auto_approve_requests) do\n          {:ok, result} ->\n            Logger.info(\"Codex session completed for #{issue_context(issue)} session_id=#{session_id}\")\n\n            {:ok,\n             %{\n               result: result,\n               session_id: session_id,\n               thread_id: thread_id,\n               turn_id: turn_id\n             }}\n\n          {:error, reason} ->\n            Logger.warning(\"Codex session ended with error for #{issue_context(issue)} session_id=#{session_id}: #{inspect(reason)}\")\n\n            emit_message(\n              on_message,\n              :turn_ended_with_error,\n              %{\n                session_id: session_id,\n                reason: reason\n              },\n              metadata\n            )\n\n            {:error, reason}\n        end\n\n      {:error, reason} ->\n        Logger.error(\"Codex session failed for #{issue_context(issue)}: #{inspect(reason)}\")\n        emit_message(on_message, :startup_failed, %{reason: reason}, metadata)\n        {:error, reason}\n    end\n  end\n\n  @spec stop_session(session()) :: :ok\n  def stop_session(%{port: port}) when is_port(port) do\n    stop_port(port)\n  end\n\n  defp validate_workspace_cwd(workspace, nil) when is_binary(workspace) do\n    expanded_workspace = Path.expand(workspace)\n    expanded_root = Path.expand(Config.settings!().workspace.root)\n    expanded_root_prefix = expanded_root <> \"/\"\n\n    with {:ok, canonical_workspace} <- PathSafety.canonicalize(expanded_workspace),\n         {:ok, canonical_root} <- PathSafety.canonicalize(expanded_root) do\n      canonical_root_prefix = canonical_root <> \"/\"\n\n      cond do\n        canonical_workspace == canonical_root ->\n          {:error, {:invalid_workspace_cwd, :workspace_root, canonical_workspace}}\n\n        String.starts_with?(canonical_workspace <> \"/\", canonical_root_prefix) ->\n          {:ok, canonical_workspace}\n\n        String.starts_with?(expanded_workspace <> \"/\", expanded_root_prefix) ->\n          {:error, {:invalid_workspace_cwd, :symlink_escape, expanded_workspace, canonical_root}}\n\n        true ->\n          {:error, {:invalid_workspace_cwd, :outside_workspace_root, canonical_workspace, canonical_root}}\n      end\n    else\n      {:error, {:path_canonicalize_failed, path, reason}} ->\n        {:error, {:invalid_workspace_cwd, :path_unreadable, path, reason}}\n    end\n  end\n\n  defp validate_workspace_cwd(workspace, worker_host)\n       when is_binary(workspace) and is_binary(worker_host) do\n    cond do\n      String.trim(workspace) == \"\" ->\n        {:error, {:invalid_workspace_cwd, :empty_remote_workspace, worker_host}}\n\n      String.contains?(workspace, [\"\\n\", \"\\r\", <<0>>]) ->\n        {:error, {:invalid_workspace_cwd, :invalid_remote_workspace, worker_host, workspace}}\n\n      true ->\n        {:ok, workspace}\n    end\n  end\n\n  defp start_port(workspace, nil) do\n    executable = System.find_executable(\"bash\")\n\n    if is_nil(executable) do\n      {:error, :bash_not_found}\n    else\n      port =\n        Port.open(\n          {:spawn_executable, String.to_charlist(executable)},\n          [\n            :binary,\n            :exit_status,\n            :stderr_to_stdout,\n            args: [~c\"-lc\", String.to_charlist(Config.settings!().codex.command)],\n            cd: String.to_charlist(workspace),\n            line: @port_line_bytes\n          ]\n        )\n\n      {:ok, port}\n    end\n  end\n\n  defp start_port(workspace, worker_host) when is_binary(worker_host) do\n    remote_command = remote_launch_command(workspace)\n    SSH.start_port(worker_host, remote_command, line: @port_line_bytes)\n  end\n\n  defp remote_launch_command(workspace) when is_binary(workspace) do\n    [\n      \"cd #{shell_escape(workspace)}\",\n      \"exec #{Config.settings!().codex.command}\"\n    ]\n    |> Enum.join(\" && \")\n  end\n\n  defp port_metadata(port, worker_host) when is_port(port) do\n    base_metadata =\n      case :erlang.port_info(port, :os_pid) do\n        {:os_pid, os_pid} ->\n          %{codex_app_server_pid: to_string(os_pid)}\n\n        _ ->\n          %{}\n      end\n\n    case worker_host do\n      host when is_binary(host) -> Map.put(base_metadata, :worker_host, host)\n      _ -> base_metadata\n    end\n  end\n\n  defp send_initialize(port) do\n    payload = %{\n      \"method\" => \"initialize\",\n      \"id\" => @initialize_id,\n      \"params\" => %{\n        \"capabilities\" => %{\n          \"experimentalApi\" => true\n        },\n        \"clientInfo\" => %{\n          \"name\" => \"symphony-orchestrator\",\n          \"title\" => \"Symphony Orchestrator\",\n          \"version\" => \"0.1.0\"\n        }\n      }\n    }\n\n    send_message(port, payload)\n\n    with {:ok, _} <- await_response(port, @initialize_id) do\n      send_message(port, %{\"method\" => \"initialized\", \"params\" => %{}})\n      :ok\n    end\n  end\n\n  defp session_policies(workspace, nil) do\n    Config.codex_runtime_settings(workspace)\n  end\n\n  defp session_policies(workspace, worker_host) when is_binary(worker_host) do\n    Config.codex_runtime_settings(workspace, remote: true)\n  end\n\n  defp do_start_session(port, workspace, session_policies) do\n    case send_initialize(port) do\n      :ok -> start_thread(port, workspace, session_policies)\n      {:error, reason} -> {:error, reason}\n    end\n  end\n\n  defp start_thread(port, workspace, %{approval_policy: approval_policy, thread_sandbox: thread_sandbox}) do\n    send_message(port, %{\n      \"method\" => \"thread/start\",\n      \"id\" => @thread_start_id,\n      \"params\" => %{\n        \"approvalPolicy\" => approval_policy,\n        \"sandbox\" => thread_sandbox,\n        \"cwd\" => workspace,\n        \"dynamicTools\" => DynamicTool.tool_specs()\n      }\n    })\n\n    case await_response(port, @thread_start_id) do\n      {:ok, %{\"thread\" => thread_payload}} ->\n        case thread_payload do\n          %{\"id\" => thread_id} -> {:ok, thread_id}\n          _ -> {:error, {:invalid_thread_payload, thread_payload}}\n        end\n\n      other ->\n        other\n    end\n  end\n\n  defp start_turn(port, thread_id, prompt, issue, workspace, approval_policy, turn_sandbox_policy) do\n    send_message(port, %{\n      \"method\" => \"turn/start\",\n      \"id\" => @turn_start_id,\n      \"params\" => %{\n        \"threadId\" => thread_id,\n        \"input\" => [\n          %{\n            \"type\" => \"text\",\n            \"text\" => prompt\n          }\n        ],\n        \"cwd\" => workspace,\n        \"title\" => \"#{issue.identifier}: #{issue.title}\",\n        \"approvalPolicy\" => approval_policy,\n        \"sandboxPolicy\" => turn_sandbox_policy\n      }\n    })\n\n    case await_response(port, @turn_start_id) do\n      {:ok, %{\"turn\" => %{\"id\" => turn_id}}} -> {:ok, turn_id}\n      other -> other\n    end\n  end\n\n  defp await_turn_completion(port, on_message, tool_executor, auto_approve_requests) do\n    receive_loop(\n      port,\n      on_message,\n      Config.settings!().codex.turn_timeout_ms,\n      \"\",\n      tool_executor,\n      auto_approve_requests\n    )\n  end\n\n  defp receive_loop(port, on_message, timeout_ms, pending_line, tool_executor, auto_approve_requests) do\n    receive do\n      {^port, {:data, {:eol, chunk}}} ->\n        complete_line = pending_line <> to_string(chunk)\n        handle_incoming(port, on_message, complete_line, timeout_ms, tool_executor, auto_approve_requests)\n\n      {^port, {:data, {:noeol, chunk}}} ->\n        receive_loop(\n          port,\n          on_message,\n          timeout_ms,\n          pending_line <> to_string(chunk),\n          tool_executor,\n          auto_approve_requests\n        )\n\n      {^port, {:exit_status, status}} ->\n        {:error, {:port_exit, status}}\n    after\n      timeout_ms ->\n        {:error, :turn_timeout}\n    end\n  end\n\n  defp handle_incoming(port, on_message, data, timeout_ms, tool_executor, auto_approve_requests) do\n    payload_string = to_string(data)\n\n    case Jason.decode(payload_string) do\n      {:ok, %{\"method\" => \"turn/completed\"} = payload} ->\n        emit_turn_event(on_message, :turn_completed, payload, payload_string, port, payload)\n        {:ok, :turn_completed}\n\n      {:ok, %{\"method\" => \"turn/failed\", \"params\" => _} = payload} ->\n        emit_turn_event(\n          on_message,\n          :turn_failed,\n          payload,\n          payload_string,\n          port,\n          Map.get(payload, \"params\")\n        )\n\n        {:error, {:turn_failed, Map.get(payload, \"params\")}}\n\n      {:ok, %{\"method\" => \"turn/cancelled\", \"params\" => _} = payload} ->\n        emit_turn_event(\n          on_message,\n          :turn_cancelled,\n          payload,\n          payload_string,\n          port,\n          Map.get(payload, \"params\")\n        )\n\n        {:error, {:turn_cancelled, Map.get(payload, \"params\")}}\n\n      {:ok, %{\"method\" => method} = payload}\n      when is_binary(method) ->\n        handle_turn_method(\n          port,\n          on_message,\n          payload,\n          payload_string,\n          method,\n          timeout_ms,\n          tool_executor,\n          auto_approve_requests\n        )\n\n      {:ok, payload} ->\n        emit_message(\n          on_message,\n          :other_message,\n          %{\n            payload: payload,\n            raw: payload_string\n          },\n          metadata_from_message(port, payload)\n        )\n\n        receive_loop(port, on_message, timeout_ms, \"\", tool_executor, auto_approve_requests)\n\n      {:error, _reason} ->\n        log_non_json_stream_line(payload_string, \"turn stream\")\n\n        if protocol_message_candidate?(payload_string) do\n          emit_message(\n            on_message,\n            :malformed,\n            %{\n              payload: payload_string,\n              raw: payload_string\n            },\n            metadata_from_message(port, %{raw: payload_string})\n          )\n        end\n\n        receive_loop(port, on_message, timeout_ms, \"\", tool_executor, auto_approve_requests)\n    end\n  end\n\n  defp emit_turn_event(on_message, event, payload, payload_string, port, payload_details) do\n    emit_message(\n      on_message,\n      event,\n      %{\n        payload: payload,\n        raw: payload_string,\n        details: payload_details\n      },\n      metadata_from_message(port, payload)\n    )\n  end\n\n  defp handle_turn_method(\n         port,\n         on_message,\n         payload,\n         payload_string,\n         method,\n         timeout_ms,\n         tool_executor,\n         auto_approve_requests\n       ) do\n    metadata = metadata_from_message(port, payload)\n\n    case maybe_handle_approval_request(\n           port,\n           method,\n           payload,\n           payload_string,\n           on_message,\n           metadata,\n           tool_executor,\n           auto_approve_requests\n         ) do\n      :input_required ->\n        emit_message(\n          on_message,\n          :turn_input_required,\n          %{payload: payload, raw: payload_string},\n          metadata\n        )\n\n        {:error, {:turn_input_required, payload}}\n\n      :approved ->\n        receive_loop(port, on_message, timeout_ms, \"\", tool_executor, auto_approve_requests)\n\n      :approval_required ->\n        emit_message(\n          on_message,\n          :approval_required,\n          %{payload: payload, raw: payload_string},\n          metadata\n        )\n\n        {:error, {:approval_required, payload}}\n\n      :unhandled ->\n        if needs_input?(method, payload) do\n          emit_message(\n            on_message,\n            :turn_input_required,\n            %{payload: payload, raw: payload_string},\n            metadata\n          )\n\n          {:error, {:turn_input_required, payload}}\n        else\n          emit_message(\n            on_message,\n            :notification,\n            %{\n              payload: payload,\n              raw: payload_string\n            },\n            metadata\n          )\n\n          Logger.debug(\"Codex notification: #{inspect(method)}\")\n          receive_loop(port, on_message, timeout_ms, \"\", tool_executor, auto_approve_requests)\n        end\n    end\n  end\n\n  defp maybe_handle_approval_request(\n         port,\n         \"item/commandExecution/requestApproval\",\n         %{\"id\" => id} = payload,\n         payload_string,\n         on_message,\n         metadata,\n         _tool_executor,\n         auto_approve_requests\n       ) do\n    approve_or_require(\n      port,\n      id,\n      \"acceptForSession\",\n      payload,\n      payload_string,\n      on_message,\n      metadata,\n      auto_approve_requests\n    )\n  end\n\n  defp maybe_handle_approval_request(\n         port,\n         \"item/tool/call\",\n         %{\"id\" => id, \"params\" => params} = payload,\n         payload_string,\n         on_message,\n         metadata,\n         tool_executor,\n         _auto_approve_requests\n       ) do\n    tool_name = tool_call_name(params)\n    arguments = tool_call_arguments(params)\n\n    result =\n      tool_name\n      |> tool_executor.(arguments)\n      |> normalize_dynamic_tool_result()\n\n    send_message(port, %{\n      \"id\" => id,\n      \"result\" => result\n    })\n\n    event =\n      case result do\n        %{\"success\" => true} -> :tool_call_completed\n        _ when is_nil(tool_name) -> :unsupported_tool_call\n        _ -> :tool_call_failed\n      end\n\n    emit_message(on_message, event, %{payload: payload, raw: payload_string}, metadata)\n\n    :approved\n  end\n\n  defp maybe_handle_approval_request(\n         port,\n         \"execCommandApproval\",\n         %{\"id\" => id} = payload,\n         payload_string,\n         on_message,\n         metadata,\n         _tool_executor,\n         auto_approve_requests\n       ) do\n    approve_or_require(\n      port,\n      id,\n      \"approved_for_session\",\n      payload,\n      payload_string,\n      on_message,\n      metadata,\n      auto_approve_requests\n    )\n  end\n\n  defp maybe_handle_approval_request(\n         port,\n         \"applyPatchApproval\",\n         %{\"id\" => id} = payload,\n         payload_string,\n         on_message,\n         metadata,\n         _tool_executor,\n         auto_approve_requests\n       ) do\n    approve_or_require(\n      port,\n      id,\n      \"approved_for_session\",\n      payload,\n      payload_string,\n      on_message,\n      metadata,\n      auto_approve_requests\n    )\n  end\n\n  defp maybe_handle_approval_request(\n         port,\n         \"item/fileChange/requestApproval\",\n         %{\"id\" => id} = payload,\n         payload_string,\n         on_message,\n         metadata,\n         _tool_executor,\n         auto_approve_requests\n       ) do\n    approve_or_require(\n      port,\n      id,\n      \"acceptForSession\",\n      payload,\n      payload_string,\n      on_message,\n      metadata,\n      auto_approve_requests\n    )\n  end\n\n  defp maybe_handle_approval_request(\n         port,\n         \"item/tool/requestUserInput\",\n         %{\"id\" => id, \"params\" => params} = payload,\n         payload_string,\n         on_message,\n         metadata,\n         _tool_executor,\n         auto_approve_requests\n       ) do\n    maybe_auto_answer_tool_request_user_input(\n      port,\n      id,\n      params,\n      payload,\n      payload_string,\n      on_message,\n      metadata,\n      auto_approve_requests\n    )\n  end\n\n  defp maybe_handle_approval_request(\n         _port,\n         _method,\n         _payload,\n         _payload_string,\n         _on_message,\n         _metadata,\n         _tool_executor,\n         _auto_approve_requests\n       ) do\n    :unhandled\n  end\n\n  defp normalize_dynamic_tool_result(%{\"success\" => success} = result) when is_boolean(success) do\n    output =\n      case Map.get(result, \"output\") do\n        existing_output when is_binary(existing_output) -> existing_output\n        _ -> dynamic_tool_output(result)\n      end\n\n    content_items =\n      case Map.get(result, \"contentItems\") do\n        existing_items when is_list(existing_items) -> existing_items\n        _ -> dynamic_tool_content_items(output)\n      end\n\n    result\n    |> Map.put(\"output\", output)\n    |> Map.put(\"contentItems\", content_items)\n  end\n\n  defp normalize_dynamic_tool_result(result) do\n    %{\n      \"success\" => false,\n      \"output\" => inspect(result),\n      \"contentItems\" => dynamic_tool_content_items(inspect(result))\n    }\n  end\n\n  defp dynamic_tool_output(%{\"contentItems\" => [%{\"text\" => text} | _]}) when is_binary(text), do: text\n  defp dynamic_tool_output(result), do: Jason.encode!(result, pretty: true)\n\n  defp dynamic_tool_content_items(output) when is_binary(output) do\n    [\n      %{\n        \"type\" => \"inputText\",\n        \"text\" => output\n      }\n    ]\n  end\n\n  defp approve_or_require(\n         port,\n         id,\n         decision,\n         payload,\n         payload_string,\n         on_message,\n         metadata,\n         true\n       ) do\n    send_message(port, %{\"id\" => id, \"result\" => %{\"decision\" => decision}})\n\n    emit_message(\n      on_message,\n      :approval_auto_approved,\n      %{payload: payload, raw: payload_string, decision: decision},\n      metadata\n    )\n\n    :approved\n  end\n\n  defp approve_or_require(\n         _port,\n         _id,\n         _decision,\n         _payload,\n         _payload_string,\n         _on_message,\n         _metadata,\n         false\n       ) do\n    :approval_required\n  end\n\n  defp maybe_auto_answer_tool_request_user_input(\n         port,\n         id,\n         params,\n         payload,\n         payload_string,\n         on_message,\n         metadata,\n         true\n       ) do\n    case tool_request_user_input_approval_answers(params) do\n      {:ok, answers, decision} ->\n        send_message(port, %{\"id\" => id, \"result\" => %{\"answers\" => answers}})\n\n        emit_message(\n          on_message,\n          :approval_auto_approved,\n          %{payload: payload, raw: payload_string, decision: decision},\n          metadata\n        )\n\n        :approved\n\n      :error ->\n        reply_with_non_interactive_tool_input_answer(\n          port,\n          id,\n          params,\n          payload,\n          payload_string,\n          on_message,\n          metadata\n        )\n    end\n  end\n\n  defp maybe_auto_answer_tool_request_user_input(\n         port,\n         id,\n         params,\n         payload,\n         payload_string,\n         on_message,\n         metadata,\n         false\n       ) do\n    reply_with_non_interactive_tool_input_answer(\n      port,\n      id,\n      params,\n      payload,\n      payload_string,\n      on_message,\n      metadata\n    )\n  end\n\n  defp tool_request_user_input_approval_answers(%{\"questions\" => questions}) when is_list(questions) do\n    answers =\n      Enum.reduce_while(questions, %{}, fn question, acc ->\n        case tool_request_user_input_approval_answer(question) do\n          {:ok, question_id, answer_label} ->\n            {:cont, Map.put(acc, question_id, %{\"answers\" => [answer_label]})}\n\n          :error ->\n            {:halt, :error}\n        end\n      end)\n\n    case answers do\n      :error -> :error\n      answer_map when map_size(answer_map) > 0 -> {:ok, answer_map, \"Approve this Session\"}\n      _ -> :error\n    end\n  end\n\n  defp tool_request_user_input_approval_answers(_params), do: :error\n\n  defp reply_with_non_interactive_tool_input_answer(\n         port,\n         id,\n         params,\n         payload,\n         payload_string,\n         on_message,\n         metadata\n       ) do\n    case tool_request_user_input_unavailable_answers(params) do\n      {:ok, answers} ->\n        send_message(port, %{\"id\" => id, \"result\" => %{\"answers\" => answers}})\n\n        emit_message(\n          on_message,\n          :tool_input_auto_answered,\n          %{payload: payload, raw: payload_string, answer: @non_interactive_tool_input_answer},\n          metadata\n        )\n\n        :approved\n\n      :error ->\n        :input_required\n    end\n  end\n\n  defp tool_request_user_input_unavailable_answers(%{\"questions\" => questions}) when is_list(questions) do\n    answers =\n      Enum.reduce_while(questions, %{}, fn question, acc ->\n        case tool_request_user_input_question_id(question) do\n          {:ok, question_id} ->\n            {:cont, Map.put(acc, question_id, %{\"answers\" => [@non_interactive_tool_input_answer]})}\n\n          :error ->\n            {:halt, :error}\n        end\n      end)\n\n    case answers do\n      :error -> :error\n      answer_map when map_size(answer_map) > 0 -> {:ok, answer_map}\n      _ -> :error\n    end\n  end\n\n  defp tool_request_user_input_unavailable_answers(_params), do: :error\n\n  defp tool_request_user_input_question_id(%{\"id\" => question_id}) when is_binary(question_id),\n    do: {:ok, question_id}\n\n  defp tool_request_user_input_question_id(_question), do: :error\n\n  defp tool_request_user_input_approval_answer(%{\"id\" => question_id, \"options\" => options})\n       when is_binary(question_id) and is_list(options) do\n    case tool_request_user_input_approval_option_label(options) do\n      nil -> :error\n      answer_label -> {:ok, question_id, answer_label}\n    end\n  end\n\n  defp tool_request_user_input_approval_answer(_question), do: :error\n\n  defp tool_request_user_input_approval_option_label(options) do\n    options\n    |> Enum.map(&tool_request_user_input_option_label/1)\n    |> Enum.reject(&is_nil/1)\n    |> case do\n      labels ->\n        Enum.find(labels, &(&1 == \"Approve this Session\")) ||\n          Enum.find(labels, &(&1 == \"Approve Once\")) ||\n          Enum.find(labels, &approval_option_label?/1)\n    end\n  end\n\n  defp tool_request_user_input_option_label(%{\"label\" => label}) when is_binary(label), do: label\n  defp tool_request_user_input_option_label(_option), do: nil\n\n  defp approval_option_label?(label) when is_binary(label) do\n    normalized_label =\n      label\n      |> String.trim()\n      |> String.downcase()\n\n    String.starts_with?(normalized_label, \"approve\") or String.starts_with?(normalized_label, \"allow\")\n  end\n\n  defp await_response(port, request_id) do\n    with_timeout_response(port, request_id, Config.settings!().codex.read_timeout_ms, \"\")\n  end\n\n  defp with_timeout_response(port, request_id, timeout_ms, pending_line) do\n    receive do\n      {^port, {:data, {:eol, chunk}}} ->\n        complete_line = pending_line <> to_string(chunk)\n        handle_response(port, request_id, complete_line, timeout_ms)\n\n      {^port, {:data, {:noeol, chunk}}} ->\n        with_timeout_response(port, request_id, timeout_ms, pending_line <> to_string(chunk))\n\n      {^port, {:exit_status, status}} ->\n        {:error, {:port_exit, status}}\n    after\n      timeout_ms ->\n        {:error, :response_timeout}\n    end\n  end\n\n  defp handle_response(port, request_id, data, timeout_ms) do\n    payload = to_string(data)\n\n    case Jason.decode(payload) do\n      {:ok, %{\"id\" => ^request_id, \"error\" => error}} ->\n        {:error, {:response_error, error}}\n\n      {:ok, %{\"id\" => ^request_id, \"result\" => result}} ->\n        {:ok, result}\n\n      {:ok, %{\"id\" => ^request_id} = response_payload} ->\n        {:error, {:response_error, response_payload}}\n\n      {:ok, %{} = other} ->\n        Logger.debug(\"Ignoring message while waiting for response: #{inspect(other)}\")\n        with_timeout_response(port, request_id, timeout_ms, \"\")\n\n      {:error, _} ->\n        log_non_json_stream_line(payload, \"response stream\")\n        with_timeout_response(port, request_id, timeout_ms, \"\")\n    end\n  end\n\n  defp log_non_json_stream_line(data, stream_label) do\n    text =\n      data\n      |> to_string()\n      |> String.trim()\n      |> String.slice(0, @max_stream_log_bytes)\n\n    if text != \"\" do\n      if String.match?(text, ~r/\\b(error|warn|warning|failed|fatal|panic|exception)\\b/i) do\n        Logger.warning(\"Codex #{stream_label} output: #{text}\")\n      else\n        Logger.debug(\"Codex #{stream_label} output: #{text}\")\n      end\n    end\n  end\n\n  defp protocol_message_candidate?(data) do\n    data\n    |> to_string()\n    |> String.trim_leading()\n    |> String.starts_with?(\"{\")\n  end\n\n  defp issue_context(%{id: issue_id, identifier: identifier}) do\n    \"issue_id=#{issue_id} issue_identifier=#{identifier}\"\n  end\n\n  defp stop_port(port) when is_port(port) do\n    case :erlang.port_info(port) do\n      :undefined ->\n        :ok\n\n      _ ->\n        try do\n          Port.close(port)\n          :ok\n        rescue\n          ArgumentError ->\n            :ok\n        end\n    end\n  end\n\n  defp emit_message(on_message, event, details, metadata) when is_function(on_message, 1) do\n    message = metadata |> Map.merge(details) |> Map.put(:event, event) |> Map.put(:timestamp, DateTime.utc_now())\n    on_message.(message)\n  end\n\n  defp metadata_from_message(port, payload) do\n    port |> port_metadata(nil) |> maybe_set_usage(payload)\n  end\n\n  defp maybe_set_usage(metadata, payload) when is_map(payload) do\n    usage = Map.get(payload, \"usage\") || Map.get(payload, :usage)\n\n    if is_map(usage) do\n      Map.put(metadata, :usage, usage)\n    else\n      metadata\n    end\n  end\n\n  defp maybe_set_usage(metadata, _payload), do: metadata\n\n  defp shell_escape(value) when is_binary(value) do\n    \"'\" <> String.replace(value, \"'\", \"'\\\"'\\\"'\") <> \"'\"\n  end\n\n  defp default_on_message(_message), do: :ok\n\n  defp tool_call_name(params) when is_map(params) do\n    case Map.get(params, \"tool\") || Map.get(params, :tool) || Map.get(params, \"name\") || Map.get(params, :name) do\n      name when is_binary(name) ->\n        case String.trim(name) do\n          \"\" -> nil\n          trimmed -> trimmed\n        end\n\n      _ ->\n        nil\n    end\n  end\n\n  defp tool_call_name(_params), do: nil\n\n  defp tool_call_arguments(params) when is_map(params) do\n    Map.get(params, \"arguments\") || Map.get(params, :arguments) || %{}\n  end\n\n  defp tool_call_arguments(_params), do: %{}\n\n  defp send_message(port, message) do\n    line = Jason.encode!(message) <> \"\\n\"\n    Port.command(port, line)\n  end\n\n  defp needs_input?(method, payload)\n       when is_binary(method) and is_map(payload) do\n    String.starts_with?(method, \"turn/\") && input_required_method?(method, payload)\n  end\n\n  defp needs_input?(_method, _payload), do: false\n\n  defp input_required_method?(method, payload) when is_binary(method) do\n    method in [\n      \"turn/input_required\",\n      \"turn/needs_input\",\n      \"turn/need_input\",\n      \"turn/request_input\",\n      \"turn/request_response\",\n      \"turn/provide_input\",\n      \"turn/approval_required\"\n    ] || request_payload_requires_input?(payload)\n  end\n\n  defp request_payload_requires_input?(payload) do\n    params = Map.get(payload, \"params\")\n    needs_input_field?(payload) || needs_input_field?(params)\n  end\n\n  defp needs_input_field?(payload) when is_map(payload) do\n    Map.get(payload, \"requiresInput\") == true or\n      Map.get(payload, \"needsInput\") == true or\n      Map.get(payload, \"input_required\") == true or\n      Map.get(payload, \"inputRequired\") == true or\n      Map.get(payload, \"type\") == \"input_required\" or\n      Map.get(payload, \"type\") == \"needs_input\"\n  end\n\n  defp needs_input_field?(_payload), do: false\nend\n"
  },
  {
    "path": "elixir/lib/symphony_elixir/codex/dynamic_tool.ex",
    "content": "defmodule SymphonyElixir.Codex.DynamicTool do\n  @moduledoc \"\"\"\n  Executes client-side tool calls requested by Codex app-server turns.\n  \"\"\"\n\n  alias SymphonyElixir.Linear.Client\n\n  @linear_graphql_tool \"linear_graphql\"\n  @linear_graphql_description \"\"\"\n  Execute a raw GraphQL query or mutation against Linear using Symphony's configured auth.\n  \"\"\"\n  @linear_graphql_input_schema %{\n    \"type\" => \"object\",\n    \"additionalProperties\" => false,\n    \"required\" => [\"query\"],\n    \"properties\" => %{\n      \"query\" => %{\n        \"type\" => \"string\",\n        \"description\" => \"GraphQL query or mutation document to execute against Linear.\"\n      },\n      \"variables\" => %{\n        \"type\" => [\"object\", \"null\"],\n        \"description\" => \"Optional GraphQL variables object.\",\n        \"additionalProperties\" => true\n      }\n    }\n  }\n\n  @spec execute(String.t() | nil, term(), keyword()) :: map()\n  def execute(tool, arguments, opts \\\\ []) do\n    case tool do\n      @linear_graphql_tool ->\n        execute_linear_graphql(arguments, opts)\n\n      other ->\n        failure_response(%{\n          \"error\" => %{\n            \"message\" => \"Unsupported dynamic tool: #{inspect(other)}.\",\n            \"supportedTools\" => supported_tool_names()\n          }\n        })\n    end\n  end\n\n  @spec tool_specs() :: [map()]\n  def tool_specs do\n    [\n      %{\n        \"name\" => @linear_graphql_tool,\n        \"description\" => @linear_graphql_description,\n        \"inputSchema\" => @linear_graphql_input_schema\n      }\n    ]\n  end\n\n  defp execute_linear_graphql(arguments, opts) do\n    linear_client = Keyword.get(opts, :linear_client, &Client.graphql/3)\n\n    with {:ok, query, variables} <- normalize_linear_graphql_arguments(arguments),\n         {:ok, response} <- linear_client.(query, variables, []) do\n      graphql_response(response)\n    else\n      {:error, reason} ->\n        failure_response(tool_error_payload(reason))\n    end\n  end\n\n  defp normalize_linear_graphql_arguments(arguments) when is_binary(arguments) do\n    case String.trim(arguments) do\n      \"\" -> {:error, :missing_query}\n      query -> {:ok, query, %{}}\n    end\n  end\n\n  defp normalize_linear_graphql_arguments(arguments) when is_map(arguments) do\n    case normalize_query(arguments) do\n      {:ok, query} ->\n        case normalize_variables(arguments) do\n          {:ok, variables} ->\n            {:ok, query, variables}\n\n          {:error, reason} ->\n            {:error, reason}\n        end\n\n      {:error, reason} ->\n        {:error, reason}\n    end\n  end\n\n  defp normalize_linear_graphql_arguments(_arguments), do: {:error, :invalid_arguments}\n\n  defp normalize_query(arguments) do\n    case Map.get(arguments, \"query\") || Map.get(arguments, :query) do\n      query when is_binary(query) ->\n        case String.trim(query) do\n          \"\" -> {:error, :missing_query}\n          trimmed -> {:ok, trimmed}\n        end\n\n      _ ->\n        {:error, :missing_query}\n    end\n  end\n\n  defp normalize_variables(arguments) do\n    case Map.get(arguments, \"variables\") || Map.get(arguments, :variables) || %{} do\n      variables when is_map(variables) -> {:ok, variables}\n      _ -> {:error, :invalid_variables}\n    end\n  end\n\n  defp graphql_response(response) do\n    success =\n      case response do\n        %{\"errors\" => errors} when is_list(errors) and errors != [] -> false\n        %{errors: errors} when is_list(errors) and errors != [] -> false\n        _ -> true\n      end\n\n    dynamic_tool_response(success, encode_payload(response))\n  end\n\n  defp failure_response(payload) do\n    dynamic_tool_response(false, encode_payload(payload))\n  end\n\n  defp dynamic_tool_response(success, output) when is_boolean(success) and is_binary(output) do\n    %{\n      \"success\" => success,\n      \"output\" => output,\n      \"contentItems\" => [\n        %{\n          \"type\" => \"inputText\",\n          \"text\" => output\n        }\n      ]\n    }\n  end\n\n  defp encode_payload(payload) when is_map(payload) or is_list(payload) do\n    Jason.encode!(payload, pretty: true)\n  end\n\n  defp encode_payload(payload), do: inspect(payload)\n\n  defp tool_error_payload(:missing_query) do\n    %{\n      \"error\" => %{\n        \"message\" => \"`linear_graphql` requires a non-empty `query` string.\"\n      }\n    }\n  end\n\n  defp tool_error_payload(:invalid_arguments) do\n    %{\n      \"error\" => %{\n        \"message\" => \"`linear_graphql` expects either a GraphQL query string or an object with `query` and optional `variables`.\"\n      }\n    }\n  end\n\n  defp tool_error_payload(:invalid_variables) do\n    %{\n      \"error\" => %{\n        \"message\" => \"`linear_graphql.variables` must be a JSON object when provided.\"\n      }\n    }\n  end\n\n  defp tool_error_payload(:missing_linear_api_token) do\n    %{\n      \"error\" => %{\n        \"message\" => \"Symphony is missing Linear auth. Set `linear.api_key` in `WORKFLOW.md` or export `LINEAR_API_KEY`.\"\n      }\n    }\n  end\n\n  defp tool_error_payload({:linear_api_status, status}) do\n    %{\n      \"error\" => %{\n        \"message\" => \"Linear GraphQL request failed with HTTP #{status}.\",\n        \"status\" => status\n      }\n    }\n  end\n\n  defp tool_error_payload({:linear_api_request, reason}) do\n    %{\n      \"error\" => %{\n        \"message\" => \"Linear GraphQL request failed before receiving a successful response.\",\n        \"reason\" => inspect(reason)\n      }\n    }\n  end\n\n  defp tool_error_payload(reason) do\n    %{\n      \"error\" => %{\n        \"message\" => \"Linear GraphQL tool execution failed.\",\n        \"reason\" => inspect(reason)\n      }\n    }\n  end\n\n  defp supported_tool_names do\n    Enum.map(tool_specs(), & &1[\"name\"])\n  end\nend\n"
  },
  {
    "path": "elixir/lib/symphony_elixir/config/schema.ex",
    "content": "defmodule SymphonyElixir.Config.Schema do\n  @moduledoc false\n\n  use Ecto.Schema\n\n  import Ecto.Changeset\n\n  alias SymphonyElixir.PathSafety\n\n  @primary_key false\n\n  @type t :: %__MODULE__{}\n\n  defmodule StringOrMap do\n    @moduledoc false\n    @behaviour Ecto.Type\n\n    @spec type() :: :map\n    def type, do: :map\n\n    @spec embed_as(term()) :: :self\n    def embed_as(_format), do: :self\n\n    @spec equal?(term(), term()) :: boolean()\n    def equal?(left, right), do: left == right\n\n    @spec cast(term()) :: {:ok, String.t() | map()} | :error\n    def cast(value) when is_binary(value) or is_map(value), do: {:ok, value}\n    def cast(_value), do: :error\n\n    @spec load(term()) :: {:ok, String.t() | map()} | :error\n    def load(value) when is_binary(value) or is_map(value), do: {:ok, value}\n    def load(_value), do: :error\n\n    @spec dump(term()) :: {:ok, String.t() | map()} | :error\n    def dump(value) when is_binary(value) or is_map(value), do: {:ok, value}\n    def dump(_value), do: :error\n  end\n\n  defmodule Tracker do\n    @moduledoc false\n    use Ecto.Schema\n    import Ecto.Changeset\n\n    @primary_key false\n\n    embedded_schema do\n      field(:kind, :string)\n      field(:endpoint, :string, default: \"https://api.linear.app/graphql\")\n      field(:api_key, :string)\n      field(:project_slug, :string)\n      field(:assignee, :string)\n      field(:active_states, {:array, :string}, default: [\"Todo\", \"In Progress\"])\n      field(:terminal_states, {:array, :string}, default: [\"Closed\", \"Cancelled\", \"Canceled\", \"Duplicate\", \"Done\"])\n    end\n\n    @spec changeset(%__MODULE__{}, map()) :: Ecto.Changeset.t()\n    def changeset(schema, attrs) do\n      schema\n      |> cast(\n        attrs,\n        [:kind, :endpoint, :api_key, :project_slug, :assignee, :active_states, :terminal_states],\n        empty_values: []\n      )\n    end\n  end\n\n  defmodule Polling do\n    @moduledoc false\n    use Ecto.Schema\n    import Ecto.Changeset\n\n    @primary_key false\n    embedded_schema do\n      field(:interval_ms, :integer, default: 30_000)\n    end\n\n    @spec changeset(%__MODULE__{}, map()) :: Ecto.Changeset.t()\n    def changeset(schema, attrs) do\n      schema\n      |> cast(attrs, [:interval_ms], empty_values: [])\n      |> validate_number(:interval_ms, greater_than: 0)\n    end\n  end\n\n  defmodule Workspace do\n    @moduledoc false\n    use Ecto.Schema\n    import Ecto.Changeset\n\n    @primary_key false\n    embedded_schema do\n      field(:root, :string, default: Path.join(System.tmp_dir!(), \"symphony_workspaces\"))\n    end\n\n    @spec changeset(%__MODULE__{}, map()) :: Ecto.Changeset.t()\n    def changeset(schema, attrs) do\n      schema\n      |> cast(attrs, [:root], empty_values: [])\n    end\n  end\n\n  defmodule Worker do\n    @moduledoc false\n    use Ecto.Schema\n    import Ecto.Changeset\n\n    @primary_key false\n    embedded_schema do\n      field(:ssh_hosts, {:array, :string}, default: [])\n      field(:max_concurrent_agents_per_host, :integer)\n    end\n\n    @spec changeset(%__MODULE__{}, map()) :: Ecto.Changeset.t()\n    def changeset(schema, attrs) do\n      schema\n      |> cast(attrs, [:ssh_hosts, :max_concurrent_agents_per_host], empty_values: [])\n      |> validate_number(:max_concurrent_agents_per_host, greater_than: 0)\n    end\n  end\n\n  defmodule Agent do\n    @moduledoc false\n    use Ecto.Schema\n    import Ecto.Changeset\n\n    alias SymphonyElixir.Config.Schema\n\n    @primary_key false\n    embedded_schema do\n      field(:max_concurrent_agents, :integer, default: 10)\n      field(:max_turns, :integer, default: 20)\n      field(:max_retry_backoff_ms, :integer, default: 300_000)\n      field(:max_concurrent_agents_by_state, :map, default: %{})\n    end\n\n    @spec changeset(%__MODULE__{}, map()) :: Ecto.Changeset.t()\n    def changeset(schema, attrs) do\n      schema\n      |> cast(\n        attrs,\n        [:max_concurrent_agents, :max_turns, :max_retry_backoff_ms, :max_concurrent_agents_by_state],\n        empty_values: []\n      )\n      |> validate_number(:max_concurrent_agents, greater_than: 0)\n      |> validate_number(:max_turns, greater_than: 0)\n      |> validate_number(:max_retry_backoff_ms, greater_than: 0)\n      |> update_change(:max_concurrent_agents_by_state, &Schema.normalize_state_limits/1)\n      |> Schema.validate_state_limits(:max_concurrent_agents_by_state)\n    end\n  end\n\n  defmodule Codex do\n    @moduledoc false\n    use Ecto.Schema\n    import Ecto.Changeset\n\n    @primary_key false\n    embedded_schema do\n      field(:command, :string, default: \"codex app-server\")\n\n      field(:approval_policy, StringOrMap,\n        default: %{\n          \"reject\" => %{\n            \"sandbox_approval\" => true,\n            \"rules\" => true,\n            \"mcp_elicitations\" => true\n          }\n        }\n      )\n\n      field(:thread_sandbox, :string, default: \"workspace-write\")\n      field(:turn_sandbox_policy, :map)\n      field(:turn_timeout_ms, :integer, default: 3_600_000)\n      field(:read_timeout_ms, :integer, default: 5_000)\n      field(:stall_timeout_ms, :integer, default: 300_000)\n    end\n\n    @spec changeset(%__MODULE__{}, map()) :: Ecto.Changeset.t()\n    def changeset(schema, attrs) do\n      schema\n      |> cast(\n        attrs,\n        [\n          :command,\n          :approval_policy,\n          :thread_sandbox,\n          :turn_sandbox_policy,\n          :turn_timeout_ms,\n          :read_timeout_ms,\n          :stall_timeout_ms\n        ],\n        empty_values: []\n      )\n      |> validate_required([:command])\n      |> validate_number(:turn_timeout_ms, greater_than: 0)\n      |> validate_number(:read_timeout_ms, greater_than: 0)\n      |> validate_number(:stall_timeout_ms, greater_than_or_equal_to: 0)\n    end\n  end\n\n  defmodule Hooks do\n    @moduledoc false\n    use Ecto.Schema\n    import Ecto.Changeset\n\n    @primary_key false\n    embedded_schema do\n      field(:after_create, :string)\n      field(:before_run, :string)\n      field(:after_run, :string)\n      field(:before_remove, :string)\n      field(:timeout_ms, :integer, default: 60_000)\n    end\n\n    @spec changeset(%__MODULE__{}, map()) :: Ecto.Changeset.t()\n    def changeset(schema, attrs) do\n      schema\n      |> cast(attrs, [:after_create, :before_run, :after_run, :before_remove, :timeout_ms], empty_values: [])\n      |> validate_number(:timeout_ms, greater_than: 0)\n    end\n  end\n\n  defmodule Observability do\n    @moduledoc false\n    use Ecto.Schema\n    import Ecto.Changeset\n\n    @primary_key false\n    embedded_schema do\n      field(:dashboard_enabled, :boolean, default: true)\n      field(:refresh_ms, :integer, default: 1_000)\n      field(:render_interval_ms, :integer, default: 16)\n    end\n\n    @spec changeset(%__MODULE__{}, map()) :: Ecto.Changeset.t()\n    def changeset(schema, attrs) do\n      schema\n      |> cast(attrs, [:dashboard_enabled, :refresh_ms, :render_interval_ms], empty_values: [])\n      |> validate_number(:refresh_ms, greater_than: 0)\n      |> validate_number(:render_interval_ms, greater_than: 0)\n    end\n  end\n\n  defmodule Server do\n    @moduledoc false\n    use Ecto.Schema\n    import Ecto.Changeset\n\n    @primary_key false\n    embedded_schema do\n      field(:port, :integer)\n      field(:host, :string, default: \"127.0.0.1\")\n    end\n\n    @spec changeset(%__MODULE__{}, map()) :: Ecto.Changeset.t()\n    def changeset(schema, attrs) do\n      schema\n      |> cast(attrs, [:port, :host], empty_values: [])\n      |> validate_number(:port, greater_than_or_equal_to: 0)\n    end\n  end\n\n  embedded_schema do\n    embeds_one(:tracker, Tracker, on_replace: :update, defaults_to_struct: true)\n    embeds_one(:polling, Polling, on_replace: :update, defaults_to_struct: true)\n    embeds_one(:workspace, Workspace, on_replace: :update, defaults_to_struct: true)\n    embeds_one(:worker, Worker, on_replace: :update, defaults_to_struct: true)\n    embeds_one(:agent, Agent, on_replace: :update, defaults_to_struct: true)\n    embeds_one(:codex, Codex, on_replace: :update, defaults_to_struct: true)\n    embeds_one(:hooks, Hooks, on_replace: :update, defaults_to_struct: true)\n    embeds_one(:observability, Observability, on_replace: :update, defaults_to_struct: true)\n    embeds_one(:server, Server, on_replace: :update, defaults_to_struct: true)\n  end\n\n  @spec parse(map()) :: {:ok, %__MODULE__{}} | {:error, {:invalid_workflow_config, String.t()}}\n  def parse(config) when is_map(config) do\n    config\n    |> normalize_keys()\n    |> drop_nil_values()\n    |> changeset()\n    |> apply_action(:validate)\n    |> case do\n      {:ok, settings} ->\n        {:ok, finalize_settings(settings)}\n\n      {:error, changeset} ->\n        {:error, {:invalid_workflow_config, format_errors(changeset)}}\n    end\n  end\n\n  @spec resolve_turn_sandbox_policy(%__MODULE__{}, Path.t() | nil) :: map()\n  def resolve_turn_sandbox_policy(settings, workspace \\\\ nil) do\n    case settings.codex.turn_sandbox_policy do\n      %{} = policy ->\n        policy\n\n      _ ->\n        workspace\n        |> default_workspace_root(settings.workspace.root)\n        |> expand_local_workspace_root()\n        |> default_turn_sandbox_policy()\n    end\n  end\n\n  @spec resolve_runtime_turn_sandbox_policy(%__MODULE__{}, Path.t() | nil, keyword()) ::\n          {:ok, map()} | {:error, term()}\n  def resolve_runtime_turn_sandbox_policy(settings, workspace \\\\ nil, opts \\\\ []) do\n    case settings.codex.turn_sandbox_policy do\n      %{} = policy ->\n        {:ok, policy}\n\n      _ ->\n        workspace\n        |> default_workspace_root(settings.workspace.root)\n        |> default_runtime_turn_sandbox_policy(opts)\n    end\n  end\n\n  @spec normalize_issue_state(String.t()) :: String.t()\n  def normalize_issue_state(state_name) when is_binary(state_name) do\n    String.downcase(state_name)\n  end\n\n  @doc false\n  @spec normalize_state_limits(nil | map()) :: map()\n  def normalize_state_limits(nil), do: %{}\n\n  def normalize_state_limits(limits) when is_map(limits) do\n    Enum.reduce(limits, %{}, fn {state_name, limit}, acc ->\n      Map.put(acc, normalize_issue_state(to_string(state_name)), limit)\n    end)\n  end\n\n  @doc false\n  @spec validate_state_limits(Ecto.Changeset.t(), atom()) :: Ecto.Changeset.t()\n  def validate_state_limits(changeset, field) do\n    validate_change(changeset, field, fn ^field, limits ->\n      Enum.flat_map(limits, fn {state_name, limit} ->\n        cond do\n          to_string(state_name) == \"\" ->\n            [{field, \"state names must not be blank\"}]\n\n          not is_integer(limit) or limit <= 0 ->\n            [{field, \"limits must be positive integers\"}]\n\n          true ->\n            []\n        end\n      end)\n    end)\n  end\n\n  defp changeset(attrs) do\n    %__MODULE__{}\n    |> cast(attrs, [])\n    |> cast_embed(:tracker, with: &Tracker.changeset/2)\n    |> cast_embed(:polling, with: &Polling.changeset/2)\n    |> cast_embed(:workspace, with: &Workspace.changeset/2)\n    |> cast_embed(:worker, with: &Worker.changeset/2)\n    |> cast_embed(:agent, with: &Agent.changeset/2)\n    |> cast_embed(:codex, with: &Codex.changeset/2)\n    |> cast_embed(:hooks, with: &Hooks.changeset/2)\n    |> cast_embed(:observability, with: &Observability.changeset/2)\n    |> cast_embed(:server, with: &Server.changeset/2)\n  end\n\n  defp finalize_settings(settings) do\n    tracker = %{\n      settings.tracker\n      | api_key: resolve_secret_setting(settings.tracker.api_key, System.get_env(\"LINEAR_API_KEY\")),\n        assignee: resolve_secret_setting(settings.tracker.assignee, System.get_env(\"LINEAR_ASSIGNEE\"))\n    }\n\n    workspace = %{\n      settings.workspace\n      | root: resolve_path_value(settings.workspace.root, Path.join(System.tmp_dir!(), \"symphony_workspaces\"))\n    }\n\n    codex = %{\n      settings.codex\n      | approval_policy: normalize_keys(settings.codex.approval_policy),\n        turn_sandbox_policy: normalize_optional_map(settings.codex.turn_sandbox_policy)\n    }\n\n    %{settings | tracker: tracker, workspace: workspace, codex: codex}\n  end\n\n  defp normalize_keys(value) when is_map(value) do\n    Enum.reduce(value, %{}, fn {key, raw_value}, normalized ->\n      Map.put(normalized, normalize_key(key), normalize_keys(raw_value))\n    end)\n  end\n\n  defp normalize_keys(value) when is_list(value), do: Enum.map(value, &normalize_keys/1)\n  defp normalize_keys(value), do: value\n\n  defp normalize_optional_map(nil), do: nil\n  defp normalize_optional_map(value) when is_map(value), do: normalize_keys(value)\n\n  defp normalize_key(value) when is_atom(value), do: Atom.to_string(value)\n  defp normalize_key(value), do: to_string(value)\n\n  defp drop_nil_values(value) when is_map(value) do\n    Enum.reduce(value, %{}, fn {key, nested}, acc ->\n      case drop_nil_values(nested) do\n        nil -> acc\n        normalized -> Map.put(acc, key, normalized)\n      end\n    end)\n  end\n\n  defp drop_nil_values(value) when is_list(value), do: Enum.map(value, &drop_nil_values/1)\n  defp drop_nil_values(value), do: value\n\n  defp resolve_secret_setting(nil, fallback), do: normalize_secret_value(fallback)\n\n  defp resolve_secret_setting(value, fallback) when is_binary(value) do\n    case resolve_env_value(value, fallback) do\n      resolved when is_binary(resolved) -> normalize_secret_value(resolved)\n      resolved -> resolved\n    end\n  end\n\n  defp resolve_path_value(value, default) when is_binary(value) do\n    case normalize_path_token(value) do\n      :missing ->\n        default\n\n      \"\" ->\n        default\n\n      path ->\n        path\n    end\n  end\n\n  defp resolve_env_value(value, fallback) when is_binary(value) do\n    case env_reference_name(value) do\n      {:ok, env_name} ->\n        case System.get_env(env_name) do\n          nil -> fallback\n          \"\" -> nil\n          env_value -> env_value\n        end\n\n      :error ->\n        value\n    end\n  end\n\n  defp normalize_path_token(value) when is_binary(value) do\n    case env_reference_name(value) do\n      {:ok, env_name} -> resolve_env_token(env_name)\n      :error -> value\n    end\n  end\n\n  defp env_reference_name(\"$\" <> env_name) do\n    if String.match?(env_name, ~r/^[A-Za-z_][A-Za-z0-9_]*$/) do\n      {:ok, env_name}\n    else\n      :error\n    end\n  end\n\n  defp env_reference_name(_value), do: :error\n\n  defp resolve_env_token(env_name) do\n    case System.get_env(env_name) do\n      nil -> :missing\n      env_value -> env_value\n    end\n  end\n\n  defp normalize_secret_value(value) when is_binary(value) do\n    if value == \"\", do: nil, else: value\n  end\n\n  defp normalize_secret_value(_value), do: nil\n\n  defp default_turn_sandbox_policy(workspace) do\n    %{\n      \"type\" => \"workspaceWrite\",\n      \"writableRoots\" => [workspace],\n      \"readOnlyAccess\" => %{\"type\" => \"fullAccess\"},\n      \"networkAccess\" => false,\n      \"excludeTmpdirEnvVar\" => false,\n      \"excludeSlashTmp\" => false\n    }\n  end\n\n  defp default_runtime_turn_sandbox_policy(workspace_root, opts) when is_binary(workspace_root) do\n    if Keyword.get(opts, :remote, false) do\n      {:ok, default_turn_sandbox_policy(workspace_root)}\n    else\n      with expanded_workspace_root <- expand_local_workspace_root(workspace_root),\n           {:ok, canonical_workspace_root} <- PathSafety.canonicalize(expanded_workspace_root) do\n        {:ok, default_turn_sandbox_policy(canonical_workspace_root)}\n      end\n    end\n  end\n\n  defp default_runtime_turn_sandbox_policy(workspace_root, _opts) do\n    {:error, {:unsafe_turn_sandbox_policy, {:invalid_workspace_root, workspace_root}}}\n  end\n\n  defp default_workspace_root(workspace, _fallback) when is_binary(workspace) and workspace != \"\",\n    do: workspace\n\n  defp default_workspace_root(nil, fallback), do: fallback\n  defp default_workspace_root(\"\", fallback), do: fallback\n  defp default_workspace_root(workspace, _fallback), do: workspace\n\n  defp expand_local_workspace_root(workspace_root)\n       when is_binary(workspace_root) and workspace_root != \"\" do\n    Path.expand(workspace_root)\n  end\n\n  defp expand_local_workspace_root(_workspace_root) do\n    Path.expand(Path.join(System.tmp_dir!(), \"symphony_workspaces\"))\n  end\n\n  defp format_errors(changeset) do\n    changeset\n    |> traverse_errors(&translate_error/1)\n    |> flatten_errors()\n    |> Enum.join(\", \")\n  end\n\n  defp flatten_errors(errors, prefix \\\\ nil)\n\n  defp flatten_errors(errors, prefix) when is_map(errors) do\n    Enum.flat_map(errors, fn {key, value} ->\n      next_prefix =\n        case prefix do\n          nil -> to_string(key)\n          current -> current <> \".\" <> to_string(key)\n        end\n\n      flatten_errors(value, next_prefix)\n    end)\n  end\n\n  defp flatten_errors(errors, prefix) when is_list(errors) do\n    Enum.map(errors, &(prefix <> \" \" <> &1))\n  end\n\n  defp translate_error({message, options}) do\n    Enum.reduce(options, message, fn {key, value}, acc ->\n      String.replace(acc, \"%{#{key}}\", error_value_to_string(value))\n    end)\n  end\n\n  defp error_value_to_string(value) when is_atom(value), do: Atom.to_string(value)\n  defp error_value_to_string(value), do: inspect(value)\nend\n"
  },
  {
    "path": "elixir/lib/symphony_elixir/config.ex",
    "content": "defmodule SymphonyElixir.Config do\n  @moduledoc \"\"\"\n  Runtime configuration loaded from `WORKFLOW.md`.\n  \"\"\"\n\n  alias SymphonyElixir.Config.Schema\n  alias SymphonyElixir.Workflow\n\n  @default_prompt_template \"\"\"\n  You are working on a Linear issue.\n\n  Identifier: {{ issue.identifier }}\n  Title: {{ issue.title }}\n\n  Body:\n  {% if issue.description %}\n  {{ issue.description }}\n  {% else %}\n  No description provided.\n  {% endif %}\n  \"\"\"\n\n  @type codex_runtime_settings :: %{\n          approval_policy: String.t() | map(),\n          thread_sandbox: String.t(),\n          turn_sandbox_policy: map()\n        }\n\n  @spec settings() :: {:ok, Schema.t()} | {:error, term()}\n  def settings do\n    case Workflow.current() do\n      {:ok, %{config: config}} when is_map(config) ->\n        Schema.parse(config)\n\n      {:error, reason} ->\n        {:error, reason}\n    end\n  end\n\n  @spec settings!() :: Schema.t()\n  def settings! do\n    case settings() do\n      {:ok, settings} ->\n        settings\n\n      {:error, reason} ->\n        raise ArgumentError, message: format_config_error(reason)\n    end\n  end\n\n  @spec max_concurrent_agents_for_state(term()) :: pos_integer()\n  def max_concurrent_agents_for_state(state_name) when is_binary(state_name) do\n    config = settings!()\n\n    Map.get(\n      config.agent.max_concurrent_agents_by_state,\n      Schema.normalize_issue_state(state_name),\n      config.agent.max_concurrent_agents\n    )\n  end\n\n  def max_concurrent_agents_for_state(_state_name), do: settings!().agent.max_concurrent_agents\n\n  @spec codex_turn_sandbox_policy(Path.t() | nil) :: map()\n  def codex_turn_sandbox_policy(workspace \\\\ nil) do\n    case Schema.resolve_runtime_turn_sandbox_policy(settings!(), workspace) do\n      {:ok, policy} ->\n        policy\n\n      {:error, reason} ->\n        raise ArgumentError, message: \"Invalid codex turn sandbox policy: #{inspect(reason)}\"\n    end\n  end\n\n  @spec workflow_prompt() :: String.t()\n  def workflow_prompt do\n    case Workflow.current() do\n      {:ok, %{prompt_template: prompt}} ->\n        if String.trim(prompt) == \"\", do: @default_prompt_template, else: prompt\n\n      _ ->\n        @default_prompt_template\n    end\n  end\n\n  @spec server_port() :: non_neg_integer() | nil\n  def server_port do\n    case Application.get_env(:symphony_elixir, :server_port_override) do\n      port when is_integer(port) and port >= 0 -> port\n      _ -> settings!().server.port\n    end\n  end\n\n  @spec validate!() :: :ok | {:error, term()}\n  def validate! do\n    with {:ok, settings} <- settings() do\n      validate_semantics(settings)\n    end\n  end\n\n  @spec codex_runtime_settings(Path.t() | nil, keyword()) ::\n          {:ok, codex_runtime_settings()} | {:error, term()}\n  def codex_runtime_settings(workspace \\\\ nil, opts \\\\ []) do\n    with {:ok, settings} <- settings() do\n      with {:ok, turn_sandbox_policy} <-\n             Schema.resolve_runtime_turn_sandbox_policy(settings, workspace, opts) do\n        {:ok,\n         %{\n           approval_policy: settings.codex.approval_policy,\n           thread_sandbox: settings.codex.thread_sandbox,\n           turn_sandbox_policy: turn_sandbox_policy\n         }}\n      end\n    end\n  end\n\n  defp validate_semantics(settings) do\n    cond do\n      is_nil(settings.tracker.kind) ->\n        {:error, :missing_tracker_kind}\n\n      settings.tracker.kind not in [\"linear\", \"memory\"] ->\n        {:error, {:unsupported_tracker_kind, settings.tracker.kind}}\n\n      settings.tracker.kind == \"linear\" and not is_binary(settings.tracker.api_key) ->\n        {:error, :missing_linear_api_token}\n\n      settings.tracker.kind == \"linear\" and not is_binary(settings.tracker.project_slug) ->\n        {:error, :missing_linear_project_slug}\n\n      true ->\n        :ok\n    end\n  end\n\n  defp format_config_error(reason) do\n    case reason do\n      {:invalid_workflow_config, message} ->\n        \"Invalid WORKFLOW.md config: #{message}\"\n\n      {:missing_workflow_file, path, raw_reason} ->\n        \"Missing WORKFLOW.md at #{path}: #{inspect(raw_reason)}\"\n\n      {:workflow_parse_error, raw_reason} ->\n        \"Failed to parse WORKFLOW.md: #{inspect(raw_reason)}\"\n\n      :workflow_front_matter_not_a_map ->\n        \"Failed to parse WORKFLOW.md: workflow front matter must decode to a map\"\n\n      other ->\n        \"Invalid WORKFLOW.md config: #{inspect(other)}\"\n    end\n  end\nend\n"
  },
  {
    "path": "elixir/lib/symphony_elixir/http_server.ex",
    "content": "defmodule SymphonyElixir.HttpServer do\n  @moduledoc \"\"\"\n  Compatibility facade that starts the Phoenix observability endpoint when enabled.\n  \"\"\"\n\n  alias SymphonyElixir.{Config, Orchestrator}\n  alias SymphonyElixirWeb.Endpoint\n\n  @secret_key_bytes 48\n\n  @spec child_spec(keyword()) :: Supervisor.child_spec()\n  def child_spec(opts) do\n    %{\n      id: __MODULE__,\n      start: {__MODULE__, :start_link, [opts]}\n    }\n  end\n\n  @spec start_link(keyword()) :: GenServer.on_start() | :ignore\n  def start_link(opts \\\\ []) do\n    case Keyword.get(opts, :port, Config.server_port()) do\n      port when is_integer(port) and port >= 0 ->\n        host = Keyword.get(opts, :host, Config.settings!().server.host)\n        orchestrator = Keyword.get(opts, :orchestrator, Orchestrator)\n        snapshot_timeout_ms = Keyword.get(opts, :snapshot_timeout_ms, 15_000)\n\n        with {:ok, ip} <- parse_host(host) do\n          endpoint_opts = [\n            server: true,\n            http: [ip: ip, port: port],\n            url: [host: normalize_host(host)],\n            orchestrator: orchestrator,\n            snapshot_timeout_ms: snapshot_timeout_ms,\n            secret_key_base: secret_key_base()\n          ]\n\n          endpoint_config =\n            :symphony_elixir\n            |> Application.get_env(Endpoint, [])\n            |> Keyword.merge(endpoint_opts)\n\n          Application.put_env(:symphony_elixir, Endpoint, endpoint_config)\n          Endpoint.start_link()\n        end\n\n      _ ->\n        :ignore\n    end\n  end\n\n  @spec bound_port(term()) :: non_neg_integer() | nil\n  def bound_port(_server \\\\ __MODULE__) do\n    case Bandit.PhoenixAdapter.server_info(Endpoint, :http) do\n      {:ok, {_ip, port}} when is_integer(port) -> port\n      _ -> nil\n    end\n  rescue\n    _error -> nil\n  catch\n    :exit, _reason -> nil\n  end\n\n  defp parse_host({_, _, _, _} = ip), do: {:ok, ip}\n  defp parse_host({_, _, _, _, _, _, _, _} = ip), do: {:ok, ip}\n\n  defp parse_host(host) when is_binary(host) do\n    charhost = String.to_charlist(host)\n\n    case :inet.parse_address(charhost) do\n      {:ok, ip} ->\n        {:ok, ip}\n\n      {:error, _reason} ->\n        case :inet.getaddr(charhost, :inet) do\n          {:ok, ip} -> {:ok, ip}\n          {:error, _reason} -> :inet.getaddr(charhost, :inet6)\n        end\n    end\n  end\n\n  defp normalize_host(host) when host in [\"\", nil], do: \"127.0.0.1\"\n  defp normalize_host(host) when is_binary(host), do: host\n  defp normalize_host(host), do: to_string(host)\n\n  defp secret_key_base do\n    Base.encode64(:crypto.strong_rand_bytes(@secret_key_bytes), padding: false)\n  end\nend\n"
  },
  {
    "path": "elixir/lib/symphony_elixir/linear/adapter.ex",
    "content": "defmodule SymphonyElixir.Linear.Adapter do\n  @moduledoc \"\"\"\n  Linear-backed tracker adapter.\n  \"\"\"\n\n  @behaviour SymphonyElixir.Tracker\n\n  alias SymphonyElixir.Linear.Client\n\n  @create_comment_mutation \"\"\"\n  mutation SymphonyCreateComment($issueId: String!, $body: String!) {\n    commentCreate(input: {issueId: $issueId, body: $body}) {\n      success\n    }\n  }\n  \"\"\"\n\n  @update_state_mutation \"\"\"\n  mutation SymphonyUpdateIssueState($issueId: String!, $stateId: String!) {\n    issueUpdate(id: $issueId, input: {stateId: $stateId}) {\n      success\n    }\n  }\n  \"\"\"\n\n  @state_lookup_query \"\"\"\n  query SymphonyResolveStateId($issueId: String!, $stateName: String!) {\n    issue(id: $issueId) {\n      team {\n        states(filter: {name: {eq: $stateName}}, first: 1) {\n          nodes {\n            id\n          }\n        }\n      }\n    }\n  }\n  \"\"\"\n\n  @spec fetch_candidate_issues() :: {:ok, [term()]} | {:error, term()}\n  def fetch_candidate_issues, do: client_module().fetch_candidate_issues()\n\n  @spec fetch_issues_by_states([String.t()]) :: {:ok, [term()]} | {:error, term()}\n  def fetch_issues_by_states(states), do: client_module().fetch_issues_by_states(states)\n\n  @spec fetch_issue_states_by_ids([String.t()]) :: {:ok, [term()]} | {:error, term()}\n  def fetch_issue_states_by_ids(issue_ids), do: client_module().fetch_issue_states_by_ids(issue_ids)\n\n  @spec create_comment(String.t(), String.t()) :: :ok | {:error, term()}\n  def create_comment(issue_id, body) when is_binary(issue_id) and is_binary(body) do\n    with {:ok, response} <- client_module().graphql(@create_comment_mutation, %{issueId: issue_id, body: body}),\n         true <- get_in(response, [\"data\", \"commentCreate\", \"success\"]) == true do\n      :ok\n    else\n      false -> {:error, :comment_create_failed}\n      {:error, reason} -> {:error, reason}\n      _ -> {:error, :comment_create_failed}\n    end\n  end\n\n  @spec update_issue_state(String.t(), String.t()) :: :ok | {:error, term()}\n  def update_issue_state(issue_id, state_name)\n      when is_binary(issue_id) and is_binary(state_name) do\n    with {:ok, state_id} <- resolve_state_id(issue_id, state_name),\n         {:ok, response} <-\n           client_module().graphql(@update_state_mutation, %{issueId: issue_id, stateId: state_id}),\n         true <- get_in(response, [\"data\", \"issueUpdate\", \"success\"]) == true do\n      :ok\n    else\n      false -> {:error, :issue_update_failed}\n      {:error, reason} -> {:error, reason}\n      _ -> {:error, :issue_update_failed}\n    end\n  end\n\n  defp client_module do\n    Application.get_env(:symphony_elixir, :linear_client_module, Client)\n  end\n\n  defp resolve_state_id(issue_id, state_name) do\n    with {:ok, response} <-\n           client_module().graphql(@state_lookup_query, %{issueId: issue_id, stateName: state_name}),\n         state_id when is_binary(state_id) <-\n           get_in(response, [\"data\", \"issue\", \"team\", \"states\", \"nodes\", Access.at(0), \"id\"]) do\n      {:ok, state_id}\n    else\n      {:error, reason} -> {:error, reason}\n      _ -> {:error, :state_not_found}\n    end\n  end\nend\n"
  },
  {
    "path": "elixir/lib/symphony_elixir/linear/client.ex",
    "content": "defmodule SymphonyElixir.Linear.Client do\n  @moduledoc \"\"\"\n  Thin Linear GraphQL client for polling candidate issues.\n  \"\"\"\n\n  require Logger\n  alias SymphonyElixir.{Config, Linear.Issue}\n\n  @issue_page_size 50\n  @max_error_body_log_bytes 1_000\n\n  @query \"\"\"\n  query SymphonyLinearPoll($projectSlug: String!, $stateNames: [String!]!, $first: Int!, $relationFirst: Int!, $after: String) {\n    issues(filter: {project: {slugId: {eq: $projectSlug}}, state: {name: {in: $stateNames}}}, first: $first, after: $after) {\n      nodes {\n        id\n        identifier\n        title\n        description\n        priority\n        state {\n          name\n        }\n        branchName\n        url\n        assignee {\n          id\n        }\n        labels {\n          nodes {\n            name\n          }\n        }\n        inverseRelations(first: $relationFirst) {\n          nodes {\n            type\n            issue {\n              id\n              identifier\n              state {\n                name\n              }\n            }\n          }\n        }\n        createdAt\n        updatedAt\n      }\n      pageInfo {\n        hasNextPage\n        endCursor\n      }\n    }\n  }\n  \"\"\"\n\n  @query_by_ids \"\"\"\n  query SymphonyLinearIssuesById($ids: [ID!]!, $first: Int!, $relationFirst: Int!) {\n    issues(filter: {id: {in: $ids}}, first: $first) {\n      nodes {\n        id\n        identifier\n        title\n        description\n        priority\n        state {\n          name\n        }\n        branchName\n        url\n        assignee {\n          id\n        }\n        labels {\n          nodes {\n            name\n          }\n        }\n        inverseRelations(first: $relationFirst) {\n          nodes {\n            type\n            issue {\n              id\n              identifier\n              state {\n                name\n              }\n            }\n          }\n        }\n        createdAt\n        updatedAt\n      }\n    }\n  }\n  \"\"\"\n\n  @viewer_query \"\"\"\n  query SymphonyLinearViewer {\n    viewer {\n      id\n    }\n  }\n  \"\"\"\n\n  @spec fetch_candidate_issues() :: {:ok, [Issue.t()]} | {:error, term()}\n  def fetch_candidate_issues do\n    tracker = Config.settings!().tracker\n    project_slug = tracker.project_slug\n\n    cond do\n      is_nil(tracker.api_key) ->\n        {:error, :missing_linear_api_token}\n\n      is_nil(project_slug) ->\n        {:error, :missing_linear_project_slug}\n\n      true ->\n        with {:ok, assignee_filter} <- routing_assignee_filter() do\n          do_fetch_by_states(project_slug, tracker.active_states, assignee_filter)\n        end\n    end\n  end\n\n  @spec fetch_issues_by_states([String.t()]) :: {:ok, [Issue.t()]} | {:error, term()}\n  def fetch_issues_by_states(state_names) when is_list(state_names) do\n    normalized_states = Enum.map(state_names, &to_string/1) |> Enum.uniq()\n\n    if normalized_states == [] do\n      {:ok, []}\n    else\n      tracker = Config.settings!().tracker\n      project_slug = tracker.project_slug\n\n      cond do\n        is_nil(tracker.api_key) ->\n          {:error, :missing_linear_api_token}\n\n        is_nil(project_slug) ->\n          {:error, :missing_linear_project_slug}\n\n        true ->\n          do_fetch_by_states(project_slug, normalized_states, nil)\n      end\n    end\n  end\n\n  @spec fetch_issue_states_by_ids([String.t()]) :: {:ok, [Issue.t()]} | {:error, term()}\n  def fetch_issue_states_by_ids(issue_ids) when is_list(issue_ids) do\n    ids = Enum.uniq(issue_ids)\n\n    case ids do\n      [] ->\n        {:ok, []}\n\n      ids ->\n        with {:ok, assignee_filter} <- routing_assignee_filter() do\n          do_fetch_issue_states(ids, assignee_filter)\n        end\n    end\n  end\n\n  @spec graphql(String.t(), map(), keyword()) :: {:ok, map()} | {:error, term()}\n  def graphql(query, variables \\\\ %{}, opts \\\\ [])\n      when is_binary(query) and is_map(variables) and is_list(opts) do\n    payload = build_graphql_payload(query, variables, Keyword.get(opts, :operation_name))\n    request_fun = Keyword.get(opts, :request_fun, &post_graphql_request/2)\n\n    with {:ok, headers} <- graphql_headers(),\n         {:ok, %{status: 200, body: body}} <- request_fun.(payload, headers) do\n      {:ok, body}\n    else\n      {:ok, response} ->\n        Logger.error(\n          \"Linear GraphQL request failed status=#{response.status}\" <>\n            linear_error_context(payload, response)\n        )\n\n        {:error, {:linear_api_status, response.status}}\n\n      {:error, reason} ->\n        Logger.error(\"Linear GraphQL request failed: #{inspect(reason)}\")\n        {:error, {:linear_api_request, reason}}\n    end\n  end\n\n  @doc false\n  @spec normalize_issue_for_test(map()) :: Issue.t() | nil\n  def normalize_issue_for_test(issue) when is_map(issue) do\n    normalize_issue(issue, nil)\n  end\n\n  @doc false\n  @spec normalize_issue_for_test(map(), String.t() | nil) :: Issue.t() | nil\n  def normalize_issue_for_test(issue, assignee) when is_map(issue) do\n    assignee_filter =\n      case assignee do\n        value when is_binary(value) ->\n          case build_assignee_filter(value) do\n            {:ok, filter} -> filter\n            {:error, _reason} -> nil\n          end\n\n        _ ->\n          nil\n      end\n\n    normalize_issue(issue, assignee_filter)\n  end\n\n  @doc false\n  @spec next_page_cursor_for_test(map()) :: {:ok, String.t()} | :done | {:error, term()}\n  def next_page_cursor_for_test(page_info) when is_map(page_info), do: next_page_cursor(page_info)\n\n  @doc false\n  @spec merge_issue_pages_for_test([[Issue.t()]]) :: [Issue.t()]\n  def merge_issue_pages_for_test(issue_pages) when is_list(issue_pages) do\n    issue_pages\n    |> Enum.reduce([], &prepend_page_issues/2)\n    |> finalize_paginated_issues()\n  end\n\n  @doc false\n  @spec fetch_issue_states_by_ids_for_test([String.t()], (String.t(), map() -> {:ok, map()} | {:error, term()})) ::\n          {:ok, [Issue.t()]} | {:error, term()}\n  def fetch_issue_states_by_ids_for_test(issue_ids, graphql_fun)\n      when is_list(issue_ids) and is_function(graphql_fun, 2) do\n    ids = Enum.uniq(issue_ids)\n\n    case ids do\n      [] ->\n        {:ok, []}\n\n      ids ->\n        do_fetch_issue_states(ids, nil, graphql_fun)\n    end\n  end\n\n  defp do_fetch_by_states(project_slug, state_names, assignee_filter) do\n    do_fetch_by_states_page(project_slug, state_names, assignee_filter, nil, [])\n  end\n\n  defp do_fetch_by_states_page(project_slug, state_names, assignee_filter, after_cursor, acc_issues) do\n    with {:ok, body} <-\n           graphql(@query, %{\n             projectSlug: project_slug,\n             stateNames: state_names,\n             first: @issue_page_size,\n             relationFirst: @issue_page_size,\n             after: after_cursor\n           }),\n         {:ok, issues, page_info} <- decode_linear_page_response(body, assignee_filter) do\n      updated_acc = prepend_page_issues(issues, acc_issues)\n\n      case next_page_cursor(page_info) do\n        {:ok, next_cursor} ->\n          do_fetch_by_states_page(project_slug, state_names, assignee_filter, next_cursor, updated_acc)\n\n        :done ->\n          {:ok, finalize_paginated_issues(updated_acc)}\n\n        {:error, reason} ->\n          {:error, reason}\n      end\n    end\n  end\n\n  defp prepend_page_issues(issues, acc_issues) when is_list(issues) and is_list(acc_issues) do\n    Enum.reverse(issues, acc_issues)\n  end\n\n  defp finalize_paginated_issues(acc_issues) when is_list(acc_issues), do: Enum.reverse(acc_issues)\n\n  defp do_fetch_issue_states(ids, assignee_filter) do\n    do_fetch_issue_states(ids, assignee_filter, &graphql/2)\n  end\n\n  defp do_fetch_issue_states(ids, assignee_filter, graphql_fun)\n       when is_list(ids) and is_function(graphql_fun, 2) do\n    issue_order_index = issue_order_index(ids)\n    do_fetch_issue_states_page(ids, assignee_filter, graphql_fun, [], issue_order_index)\n  end\n\n  defp do_fetch_issue_states_page([], _assignee_filter, _graphql_fun, acc_issues, issue_order_index) do\n    acc_issues\n    |> finalize_paginated_issues()\n    |> sort_issues_by_requested_ids(issue_order_index)\n    |> then(&{:ok, &1})\n  end\n\n  defp do_fetch_issue_states_page(ids, assignee_filter, graphql_fun, acc_issues, issue_order_index) do\n    {batch_ids, rest_ids} = Enum.split(ids, @issue_page_size)\n\n    case graphql_fun.(@query_by_ids, %{\n           ids: batch_ids,\n           first: length(batch_ids),\n           relationFirst: @issue_page_size\n         }) do\n      {:ok, body} ->\n        with {:ok, issues} <- decode_linear_response(body, assignee_filter) do\n          updated_acc = prepend_page_issues(issues, acc_issues)\n          do_fetch_issue_states_page(rest_ids, assignee_filter, graphql_fun, updated_acc, issue_order_index)\n        end\n\n      {:error, reason} ->\n        {:error, reason}\n    end\n  end\n\n  defp issue_order_index(ids) when is_list(ids) do\n    ids\n    |> Enum.with_index()\n    |> Map.new()\n  end\n\n  defp sort_issues_by_requested_ids(issues, issue_order_index)\n       when is_list(issues) and is_map(issue_order_index) do\n    fallback_index = map_size(issue_order_index)\n\n    Enum.sort_by(issues, fn\n      %Issue{id: issue_id} -> Map.get(issue_order_index, issue_id, fallback_index)\n      _ -> fallback_index\n    end)\n  end\n\n  defp build_graphql_payload(query, variables, operation_name) do\n    %{\n      \"query\" => query,\n      \"variables\" => variables\n    }\n    |> maybe_put_operation_name(operation_name)\n  end\n\n  defp maybe_put_operation_name(payload, operation_name) when is_binary(operation_name) do\n    trimmed = String.trim(operation_name)\n\n    if trimmed == \"\" do\n      payload\n    else\n      Map.put(payload, \"operationName\", trimmed)\n    end\n  end\n\n  defp maybe_put_operation_name(payload, _operation_name), do: payload\n\n  defp linear_error_context(payload, response) when is_map(payload) do\n    operation_name =\n      case Map.get(payload, \"operationName\") do\n        name when is_binary(name) and name != \"\" -> \" operation=#{name}\"\n        _ -> \"\"\n      end\n\n    body =\n      response\n      |> Map.get(:body)\n      |> summarize_error_body()\n\n    operation_name <> \" body=\" <> body\n  end\n\n  defp summarize_error_body(body) when is_binary(body) do\n    body\n    |> String.replace(~r/\\s+/, \" \")\n    |> String.trim()\n    |> truncate_error_body()\n    |> inspect()\n  end\n\n  defp summarize_error_body(body) do\n    body\n    |> inspect(limit: 20, printable_limit: @max_error_body_log_bytes)\n    |> truncate_error_body()\n  end\n\n  defp truncate_error_body(body) when is_binary(body) do\n    if byte_size(body) > @max_error_body_log_bytes do\n      binary_part(body, 0, @max_error_body_log_bytes) <> \"...<truncated>\"\n    else\n      body\n    end\n  end\n\n  defp graphql_headers do\n    case Config.settings!().tracker.api_key do\n      nil ->\n        {:error, :missing_linear_api_token}\n\n      token ->\n        {:ok,\n         [\n           {\"Authorization\", token},\n           {\"Content-Type\", \"application/json\"}\n         ]}\n    end\n  end\n\n  defp post_graphql_request(payload, headers) do\n    Req.post(Config.settings!().tracker.endpoint,\n      headers: headers,\n      json: payload,\n      connect_options: [timeout: 30_000]\n    )\n  end\n\n  defp decode_linear_response(%{\"data\" => %{\"issues\" => %{\"nodes\" => nodes}}}, assignee_filter) do\n    issues =\n      nodes\n      |> Enum.map(&normalize_issue(&1, assignee_filter))\n      |> Enum.reject(&is_nil(&1))\n\n    {:ok, issues}\n  end\n\n  defp decode_linear_response(%{\"errors\" => errors}, _assignee_filter) do\n    {:error, {:linear_graphql_errors, errors}}\n  end\n\n  defp decode_linear_response(_unknown, _assignee_filter) do\n    {:error, :linear_unknown_payload}\n  end\n\n  defp decode_linear_page_response(\n         %{\n           \"data\" => %{\n             \"issues\" => %{\n               \"nodes\" => nodes,\n               \"pageInfo\" => %{\"hasNextPage\" => has_next_page, \"endCursor\" => end_cursor}\n             }\n           }\n         },\n         assignee_filter\n       ) do\n    with {:ok, issues} <- decode_linear_response(%{\"data\" => %{\"issues\" => %{\"nodes\" => nodes}}}, assignee_filter) do\n      {:ok, issues, %{has_next_page: has_next_page == true, end_cursor: end_cursor}}\n    end\n  end\n\n  defp decode_linear_page_response(response, assignee_filter), do: decode_linear_response(response, assignee_filter)\n\n  defp next_page_cursor(%{has_next_page: true, end_cursor: end_cursor})\n       when is_binary(end_cursor) and byte_size(end_cursor) > 0 do\n    {:ok, end_cursor}\n  end\n\n  defp next_page_cursor(%{has_next_page: true}), do: {:error, :linear_missing_end_cursor}\n  defp next_page_cursor(_), do: :done\n\n  defp normalize_issue(issue, assignee_filter) when is_map(issue) do\n    assignee = issue[\"assignee\"]\n\n    %Issue{\n      id: issue[\"id\"],\n      identifier: issue[\"identifier\"],\n      title: issue[\"title\"],\n      description: issue[\"description\"],\n      priority: parse_priority(issue[\"priority\"]),\n      state: get_in(issue, [\"state\", \"name\"]),\n      branch_name: issue[\"branchName\"],\n      url: issue[\"url\"],\n      assignee_id: assignee_field(assignee, \"id\"),\n      blocked_by: extract_blockers(issue),\n      labels: extract_labels(issue),\n      assigned_to_worker: assigned_to_worker?(assignee, assignee_filter),\n      created_at: parse_datetime(issue[\"createdAt\"]),\n      updated_at: parse_datetime(issue[\"updatedAt\"])\n    }\n  end\n\n  defp normalize_issue(_issue, _assignee_filter), do: nil\n\n  defp assignee_field(%{} = assignee, field) when is_binary(field), do: assignee[field]\n  defp assignee_field(_assignee, _field), do: nil\n\n  defp assigned_to_worker?(_assignee, nil), do: true\n\n  defp assigned_to_worker?(%{} = assignee, %{match_values: match_values})\n       when is_struct(match_values, MapSet) do\n    assignee\n    |> assignee_id()\n    |> then(fn\n      nil -> false\n      assignee_id -> MapSet.member?(match_values, assignee_id)\n    end)\n  end\n\n  defp assigned_to_worker?(_assignee, _assignee_filter), do: false\n\n  defp assignee_id(%{} = assignee), do: normalize_assignee_match_value(assignee[\"id\"])\n\n  defp routing_assignee_filter do\n    case Config.settings!().tracker.assignee do\n      nil ->\n        {:ok, nil}\n\n      assignee ->\n        build_assignee_filter(assignee)\n    end\n  end\n\n  defp build_assignee_filter(assignee) when is_binary(assignee) do\n    case normalize_assignee_match_value(assignee) do\n      nil ->\n        {:ok, nil}\n\n      \"me\" ->\n        resolve_viewer_assignee_filter()\n\n      normalized ->\n        {:ok, %{configured_assignee: assignee, match_values: MapSet.new([normalized])}}\n    end\n  end\n\n  defp resolve_viewer_assignee_filter do\n    case graphql(@viewer_query, %{}) do\n      {:ok, %{\"data\" => %{\"viewer\" => viewer}}} when is_map(viewer) ->\n        case assignee_id(viewer) do\n          nil ->\n            {:error, :missing_linear_viewer_identity}\n\n          viewer_id ->\n            {:ok, %{configured_assignee: \"me\", match_values: MapSet.new([viewer_id])}}\n        end\n\n      {:ok, _body} ->\n        {:error, :missing_linear_viewer_identity}\n\n      {:error, reason} ->\n        {:error, reason}\n    end\n  end\n\n  defp normalize_assignee_match_value(value) when is_binary(value) do\n    case value |> String.trim() do\n      \"\" -> nil\n      normalized -> normalized\n    end\n  end\n\n  defp normalize_assignee_match_value(_value), do: nil\n\n  defp extract_labels(%{\"labels\" => %{\"nodes\" => labels}}) when is_list(labels) do\n    labels\n    |> Enum.map(& &1[\"name\"])\n    |> Enum.reject(&is_nil/1)\n    |> Enum.map(&String.downcase/1)\n  end\n\n  defp extract_labels(_), do: []\n\n  defp extract_blockers(%{\"inverseRelations\" => %{\"nodes\" => inverse_relations}})\n       when is_list(inverse_relations) do\n    inverse_relations\n    |> Enum.flat_map(fn\n      %{\"type\" => relation_type, \"issue\" => blocker_issue}\n      when is_binary(relation_type) and is_map(blocker_issue) ->\n        if String.downcase(String.trim(relation_type)) == \"blocks\" do\n          [\n            %{\n              id: blocker_issue[\"id\"],\n              identifier: blocker_issue[\"identifier\"],\n              state: get_in(blocker_issue, [\"state\", \"name\"])\n            }\n          ]\n        else\n          []\n        end\n\n      _ ->\n        []\n    end)\n  end\n\n  defp extract_blockers(_), do: []\n\n  defp parse_datetime(nil), do: nil\n\n  defp parse_datetime(raw) do\n    case DateTime.from_iso8601(raw) do\n      {:ok, dt, _offset} -> dt\n      _ -> nil\n    end\n  end\n\n  defp parse_priority(priority) when is_integer(priority), do: priority\n  defp parse_priority(_priority), do: nil\nend\n"
  },
  {
    "path": "elixir/lib/symphony_elixir/linear/issue.ex",
    "content": "defmodule SymphonyElixir.Linear.Issue do\n  @moduledoc \"\"\"\n  Normalized Linear issue representation used by the orchestrator.\n  \"\"\"\n\n  defstruct [\n    :id,\n    :identifier,\n    :title,\n    :description,\n    :priority,\n    :state,\n    :branch_name,\n    :url,\n    :assignee_id,\n    blocked_by: [],\n    labels: [],\n    assigned_to_worker: true,\n    created_at: nil,\n    updated_at: nil\n  ]\n\n  @type t :: %__MODULE__{\n          id: String.t() | nil,\n          identifier: String.t() | nil,\n          title: String.t() | nil,\n          description: String.t() | nil,\n          priority: integer() | nil,\n          state: String.t() | nil,\n          branch_name: String.t() | nil,\n          url: String.t() | nil,\n          assignee_id: String.t() | nil,\n          labels: [String.t()],\n          assigned_to_worker: boolean(),\n          created_at: DateTime.t() | nil,\n          updated_at: DateTime.t() | nil\n        }\n\n  @spec label_names(t()) :: [String.t()]\n  def label_names(%__MODULE__{labels: labels}) do\n    labels\n  end\nend\n"
  },
  {
    "path": "elixir/lib/symphony_elixir/log_file.ex",
    "content": "defmodule SymphonyElixir.LogFile do\n  @moduledoc \"\"\"\n  Configures OTP's built-in rotating disk log handler for application logs.\n  \"\"\"\n\n  require Logger\n\n  @handler_id :symphony_disk_log\n  @default_log_relative_path \"log/symphony.log\"\n  @default_max_bytes 10 * 1024 * 1024\n  @default_max_files 5\n\n  @spec default_log_file() :: Path.t()\n  def default_log_file do\n    default_log_file(File.cwd!())\n  end\n\n  @spec default_log_file(Path.t()) :: Path.t()\n  def default_log_file(logs_root) when is_binary(logs_root) do\n    Path.join(logs_root, @default_log_relative_path)\n  end\n\n  @spec configure() :: :ok\n  def configure do\n    log_file = Application.get_env(:symphony_elixir, :log_file, default_log_file())\n    max_bytes = Application.get_env(:symphony_elixir, :log_file_max_bytes, @default_max_bytes)\n    max_files = Application.get_env(:symphony_elixir, :log_file_max_files, @default_max_files)\n\n    setup_disk_handler(log_file, max_bytes, max_files)\n  end\n\n  defp setup_disk_handler(log_file, max_bytes, max_files) do\n    expanded_path = Path.expand(log_file)\n    :ok = File.mkdir_p(Path.dirname(expanded_path))\n    :ok = remove_existing_handler()\n\n    case :logger.add_handler(\n           @handler_id,\n           :logger_disk_log_h,\n           disk_log_handler_config(expanded_path, max_bytes, max_files)\n         ) do\n      :ok ->\n        remove_default_console_handler()\n        :ok\n\n      {:error, reason} ->\n        Logger.warning(\"Failed to configure rotating log file handler: #{inspect(reason)}\")\n        :ok\n    end\n  end\n\n  defp remove_existing_handler do\n    case :logger.remove_handler(@handler_id) do\n      :ok -> :ok\n      {:error, {:not_found, @handler_id}} -> :ok\n      {:error, _reason} -> :ok\n    end\n  end\n\n  defp remove_default_console_handler do\n    case :logger.remove_handler(:default) do\n      :ok -> :ok\n      {:error, {:not_found, :default}} -> :ok\n      {:error, _reason} -> :ok\n    end\n  end\n\n  defp disk_log_handler_config(path, max_bytes, max_files) do\n    %{\n      level: :all,\n      formatter: {:logger_formatter, %{single_line: true}},\n      config: %{\n        file: String.to_charlist(path),\n        type: :wrap,\n        max_no_bytes: max_bytes,\n        max_no_files: max_files\n      }\n    }\n  end\nend\n"
  },
  {
    "path": "elixir/lib/symphony_elixir/orchestrator.ex",
    "content": "defmodule SymphonyElixir.Orchestrator do\n  @moduledoc \"\"\"\n  Polls Linear and dispatches repository copies to Codex-backed workers.\n  \"\"\"\n\n  use GenServer\n  require Logger\n  import Bitwise, only: [<<<: 2]\n\n  alias SymphonyElixir.{AgentRunner, Config, StatusDashboard, Tracker, Workspace}\n  alias SymphonyElixir.Linear.Issue\n\n  @continuation_retry_delay_ms 1_000\n  @failure_retry_base_ms 10_000\n  # Slightly above the dashboard render interval so \"checking now…\" can render.\n  @poll_transition_render_delay_ms 20\n  @empty_codex_totals %{\n    input_tokens: 0,\n    output_tokens: 0,\n    total_tokens: 0,\n    seconds_running: 0\n  }\n\n  defmodule State do\n    @moduledoc \"\"\"\n    Runtime state for the orchestrator polling loop.\n    \"\"\"\n\n    defstruct [\n      :poll_interval_ms,\n      :max_concurrent_agents,\n      :next_poll_due_at_ms,\n      :poll_check_in_progress,\n      :tick_timer_ref,\n      :tick_token,\n      running: %{},\n      completed: MapSet.new(),\n      claimed: MapSet.new(),\n      retry_attempts: %{},\n      codex_totals: nil,\n      codex_rate_limits: nil\n    ]\n  end\n\n  @spec start_link(keyword()) :: GenServer.on_start()\n  def start_link(opts \\\\ []) do\n    name = Keyword.get(opts, :name, __MODULE__)\n    GenServer.start_link(__MODULE__, opts, name: name)\n  end\n\n  @impl true\n  def init(_opts) do\n    now_ms = System.monotonic_time(:millisecond)\n    config = Config.settings!()\n\n    state = %State{\n      poll_interval_ms: config.polling.interval_ms,\n      max_concurrent_agents: config.agent.max_concurrent_agents,\n      next_poll_due_at_ms: now_ms,\n      poll_check_in_progress: false,\n      tick_timer_ref: nil,\n      tick_token: nil,\n      codex_totals: @empty_codex_totals,\n      codex_rate_limits: nil\n    }\n\n    run_terminal_workspace_cleanup()\n    state = schedule_tick(state, 0)\n\n    {:ok, state}\n  end\n\n  @impl true\n  def handle_info({:tick, tick_token}, %{tick_token: tick_token} = state)\n      when is_reference(tick_token) do\n    state = refresh_runtime_config(state)\n\n    state = %{\n      state\n      | poll_check_in_progress: true,\n        next_poll_due_at_ms: nil,\n        tick_timer_ref: nil,\n        tick_token: nil\n    }\n\n    notify_dashboard()\n    :ok = schedule_poll_cycle_start()\n    {:noreply, state}\n  end\n\n  def handle_info({:tick, _tick_token}, state), do: {:noreply, state}\n\n  def handle_info(:tick, state) do\n    state = refresh_runtime_config(state)\n\n    state = %{\n      state\n      | poll_check_in_progress: true,\n        next_poll_due_at_ms: nil,\n        tick_timer_ref: nil,\n        tick_token: nil\n    }\n\n    notify_dashboard()\n    :ok = schedule_poll_cycle_start()\n    {:noreply, state}\n  end\n\n  def handle_info(:run_poll_cycle, state) do\n    state = refresh_runtime_config(state)\n    state = maybe_dispatch(state)\n    state = schedule_tick(state, state.poll_interval_ms)\n    state = %{state | poll_check_in_progress: false}\n\n    notify_dashboard()\n    {:noreply, state}\n  end\n\n  def handle_info(\n        {:DOWN, ref, :process, _pid, reason},\n        %{running: running} = state\n      ) do\n    case find_issue_id_for_ref(running, ref) do\n      nil ->\n        {:noreply, state}\n\n      issue_id ->\n        {running_entry, state} = pop_running_entry(state, issue_id)\n        state = record_session_completion_totals(state, running_entry)\n        session_id = running_entry_session_id(running_entry)\n\n        state =\n          case reason do\n            :normal ->\n              Logger.info(\"Agent task completed for issue_id=#{issue_id} session_id=#{session_id}; scheduling active-state continuation check\")\n\n              state\n              |> complete_issue(issue_id)\n              |> schedule_issue_retry(issue_id, 1, %{\n                identifier: running_entry.identifier,\n                delay_type: :continuation,\n                worker_host: Map.get(running_entry, :worker_host),\n                workspace_path: Map.get(running_entry, :workspace_path)\n              })\n\n            _ ->\n              Logger.warning(\"Agent task exited for issue_id=#{issue_id} session_id=#{session_id} reason=#{inspect(reason)}; scheduling retry\")\n\n              next_attempt = next_retry_attempt_from_running(running_entry)\n\n              schedule_issue_retry(state, issue_id, next_attempt, %{\n                identifier: running_entry.identifier,\n                error: \"agent exited: #{inspect(reason)}\",\n                worker_host: Map.get(running_entry, :worker_host),\n                workspace_path: Map.get(running_entry, :workspace_path)\n              })\n          end\n\n        Logger.info(\"Agent task finished for issue_id=#{issue_id} session_id=#{session_id} reason=#{inspect(reason)}\")\n\n        notify_dashboard()\n        {:noreply, state}\n    end\n  end\n\n  def handle_info({:worker_runtime_info, issue_id, runtime_info}, %{running: running} = state)\n      when is_binary(issue_id) and is_map(runtime_info) do\n    case Map.get(running, issue_id) do\n      nil ->\n        {:noreply, state}\n\n      running_entry ->\n        updated_running_entry =\n          running_entry\n          |> maybe_put_runtime_value(:worker_host, runtime_info[:worker_host])\n          |> maybe_put_runtime_value(:workspace_path, runtime_info[:workspace_path])\n\n        notify_dashboard()\n        {:noreply, %{state | running: Map.put(running, issue_id, updated_running_entry)}}\n    end\n  end\n\n  def handle_info(\n        {:codex_worker_update, issue_id, %{event: _, timestamp: _} = update},\n        %{running: running} = state\n      ) do\n    case Map.get(running, issue_id) do\n      nil ->\n        {:noreply, state}\n\n      running_entry ->\n        {updated_running_entry, token_delta} = integrate_codex_update(running_entry, update)\n\n        state =\n          state\n          |> apply_codex_token_delta(token_delta)\n          |> apply_codex_rate_limits(update)\n\n        notify_dashboard()\n        {:noreply, %{state | running: Map.put(running, issue_id, updated_running_entry)}}\n    end\n  end\n\n  def handle_info({:codex_worker_update, _issue_id, _update}, state), do: {:noreply, state}\n\n  def handle_info({:retry_issue, issue_id, retry_token}, state) do\n    result =\n      case pop_retry_attempt_state(state, issue_id, retry_token) do\n        {:ok, attempt, metadata, state} -> handle_retry_issue(state, issue_id, attempt, metadata)\n        :missing -> {:noreply, state}\n      end\n\n    notify_dashboard()\n    result\n  end\n\n  def handle_info({:retry_issue, _issue_id}, state), do: {:noreply, state}\n\n  def handle_info(msg, state) do\n    Logger.debug(\"Orchestrator ignored message: #{inspect(msg)}\")\n    {:noreply, state}\n  end\n\n  defp maybe_dispatch(%State{} = state) do\n    state = reconcile_running_issues(state)\n\n    with :ok <- Config.validate!(),\n         {:ok, issues} <- Tracker.fetch_candidate_issues(),\n         true <- available_slots(state) > 0 do\n      choose_issues(issues, state)\n    else\n      {:error, :missing_linear_api_token} ->\n        Logger.error(\"Linear API token missing in WORKFLOW.md\")\n        state\n\n      {:error, :missing_linear_project_slug} ->\n        Logger.error(\"Linear project slug missing in WORKFLOW.md\")\n        state\n\n      {:error, :missing_tracker_kind} ->\n        Logger.error(\"Tracker kind missing in WORKFLOW.md\")\n\n        state\n\n      {:error, {:unsupported_tracker_kind, kind}} ->\n        Logger.error(\"Unsupported tracker kind in WORKFLOW.md: #{inspect(kind)}\")\n\n        state\n\n      {:error, {:invalid_workflow_config, message}} ->\n        Logger.error(\"Invalid WORKFLOW.md config: #{message}\")\n        state\n\n      {:error, {:missing_workflow_file, path, reason}} ->\n        Logger.error(\"Missing WORKFLOW.md at #{path}: #{inspect(reason)}\")\n        state\n\n      {:error, :workflow_front_matter_not_a_map} ->\n        Logger.error(\"Failed to parse WORKFLOW.md: workflow front matter must decode to a map\")\n        state\n\n      {:error, {:workflow_parse_error, reason}} ->\n        Logger.error(\"Failed to parse WORKFLOW.md: #{inspect(reason)}\")\n        state\n\n      {:error, reason} ->\n        Logger.error(\"Failed to fetch from Linear: #{inspect(reason)}\")\n        state\n\n      false ->\n        state\n    end\n  end\n\n  defp reconcile_running_issues(%State{} = state) do\n    state = reconcile_stalled_running_issues(state)\n    running_ids = Map.keys(state.running)\n\n    if running_ids == [] do\n      state\n    else\n      case Tracker.fetch_issue_states_by_ids(running_ids) do\n        {:ok, issues} ->\n          issues\n          |> reconcile_running_issue_states(\n            state,\n            active_state_set(),\n            terminal_state_set()\n          )\n          |> reconcile_missing_running_issue_ids(running_ids, issues)\n\n        {:error, reason} ->\n          Logger.debug(\"Failed to refresh running issue states: #{inspect(reason)}; keeping active workers\")\n\n          state\n      end\n    end\n  end\n\n  @doc false\n  @spec reconcile_issue_states_for_test([Issue.t()], term()) :: term()\n  def reconcile_issue_states_for_test(issues, %State{} = state) when is_list(issues) do\n    reconcile_running_issue_states(issues, state, active_state_set(), terminal_state_set())\n  end\n\n  def reconcile_issue_states_for_test(issues, state) when is_list(issues) do\n    reconcile_running_issue_states(issues, state, active_state_set(), terminal_state_set())\n  end\n\n  @doc false\n  @spec should_dispatch_issue_for_test(Issue.t(), term()) :: boolean()\n  def should_dispatch_issue_for_test(%Issue{} = issue, %State{} = state) do\n    should_dispatch_issue?(issue, state, active_state_set(), terminal_state_set())\n  end\n\n  @doc false\n  @spec revalidate_issue_for_dispatch_for_test(Issue.t(), ([String.t()] -> term())) ::\n          {:ok, Issue.t()} | {:skip, Issue.t() | :missing} | {:error, term()}\n  def revalidate_issue_for_dispatch_for_test(%Issue{} = issue, issue_fetcher)\n      when is_function(issue_fetcher, 1) do\n    revalidate_issue_for_dispatch(issue, issue_fetcher, terminal_state_set())\n  end\n\n  @doc false\n  @spec sort_issues_for_dispatch_for_test([Issue.t()]) :: [Issue.t()]\n  def sort_issues_for_dispatch_for_test(issues) when is_list(issues) do\n    sort_issues_for_dispatch(issues)\n  end\n\n  @doc false\n  @spec select_worker_host_for_test(term(), String.t() | nil) :: String.t() | nil | :no_worker_capacity\n  def select_worker_host_for_test(%State{} = state, preferred_worker_host) do\n    select_worker_host(state, preferred_worker_host)\n  end\n\n  defp reconcile_running_issue_states([], state, _active_states, _terminal_states), do: state\n\n  defp reconcile_running_issue_states([issue | rest], state, active_states, terminal_states) do\n    reconcile_running_issue_states(\n      rest,\n      reconcile_issue_state(issue, state, active_states, terminal_states),\n      active_states,\n      terminal_states\n    )\n  end\n\n  defp reconcile_issue_state(%Issue{} = issue, state, active_states, terminal_states) do\n    cond do\n      terminal_issue_state?(issue.state, terminal_states) ->\n        Logger.info(\"Issue moved to terminal state: #{issue_context(issue)} state=#{issue.state}; stopping active agent\")\n\n        terminate_running_issue(state, issue.id, true)\n\n      !issue_routable_to_worker?(issue) ->\n        Logger.info(\"Issue no longer routed to this worker: #{issue_context(issue)} assignee=#{inspect(issue.assignee_id)}; stopping active agent\")\n\n        terminate_running_issue(state, issue.id, false)\n\n      active_issue_state?(issue.state, active_states) ->\n        refresh_running_issue_state(state, issue)\n\n      true ->\n        Logger.info(\"Issue moved to non-active state: #{issue_context(issue)} state=#{issue.state}; stopping active agent\")\n\n        terminate_running_issue(state, issue.id, false)\n    end\n  end\n\n  defp reconcile_issue_state(_issue, state, _active_states, _terminal_states), do: state\n\n  defp reconcile_missing_running_issue_ids(%State{} = state, requested_issue_ids, issues)\n       when is_list(requested_issue_ids) and is_list(issues) do\n    visible_issue_ids =\n      issues\n      |> Enum.flat_map(fn\n        %Issue{id: issue_id} when is_binary(issue_id) -> [issue_id]\n        _ -> []\n      end)\n      |> MapSet.new()\n\n    Enum.reduce(requested_issue_ids, state, fn issue_id, state_acc ->\n      if MapSet.member?(visible_issue_ids, issue_id) do\n        state_acc\n      else\n        log_missing_running_issue(state_acc, issue_id)\n        terminate_running_issue(state_acc, issue_id, false)\n      end\n    end)\n  end\n\n  defp reconcile_missing_running_issue_ids(state, _requested_issue_ids, _issues), do: state\n\n  defp log_missing_running_issue(%State{} = state, issue_id) when is_binary(issue_id) do\n    case Map.get(state.running, issue_id) do\n      %{identifier: identifier} ->\n        Logger.info(\"Issue no longer visible during running-state refresh: issue_id=#{issue_id} issue_identifier=#{identifier}; stopping active agent\")\n\n      _ ->\n        Logger.info(\"Issue no longer visible during running-state refresh: issue_id=#{issue_id}; stopping active agent\")\n    end\n  end\n\n  defp log_missing_running_issue(_state, _issue_id), do: :ok\n\n  defp refresh_running_issue_state(%State{} = state, %Issue{} = issue) do\n    case Map.get(state.running, issue.id) do\n      %{issue: _} = running_entry ->\n        %{state | running: Map.put(state.running, issue.id, %{running_entry | issue: issue})}\n\n      _ ->\n        state\n    end\n  end\n\n  defp terminate_running_issue(%State{} = state, issue_id, cleanup_workspace) do\n    case Map.get(state.running, issue_id) do\n      nil ->\n        release_issue_claim(state, issue_id)\n\n      %{pid: pid, ref: ref, identifier: identifier} = running_entry ->\n        state = record_session_completion_totals(state, running_entry)\n        worker_host = Map.get(running_entry, :worker_host)\n\n        if cleanup_workspace do\n          cleanup_issue_workspace(identifier, worker_host)\n        end\n\n        if is_pid(pid) do\n          terminate_task(pid)\n        end\n\n        if is_reference(ref) do\n          Process.demonitor(ref, [:flush])\n        end\n\n        %{\n          state\n          | running: Map.delete(state.running, issue_id),\n            claimed: MapSet.delete(state.claimed, issue_id),\n            retry_attempts: Map.delete(state.retry_attempts, issue_id)\n        }\n\n      _ ->\n        release_issue_claim(state, issue_id)\n    end\n  end\n\n  defp reconcile_stalled_running_issues(%State{} = state) do\n    timeout_ms = Config.settings!().codex.stall_timeout_ms\n\n    cond do\n      timeout_ms <= 0 ->\n        state\n\n      map_size(state.running) == 0 ->\n        state\n\n      true ->\n        now = DateTime.utc_now()\n\n        Enum.reduce(state.running, state, fn {issue_id, running_entry}, state_acc ->\n          restart_stalled_issue(state_acc, issue_id, running_entry, now, timeout_ms)\n        end)\n    end\n  end\n\n  defp restart_stalled_issue(state, issue_id, running_entry, now, timeout_ms) do\n    elapsed_ms = stall_elapsed_ms(running_entry, now)\n\n    if is_integer(elapsed_ms) and elapsed_ms > timeout_ms do\n      identifier = Map.get(running_entry, :identifier, issue_id)\n      session_id = running_entry_session_id(running_entry)\n\n      Logger.warning(\"Issue stalled: issue_id=#{issue_id} issue_identifier=#{identifier} session_id=#{session_id} elapsed_ms=#{elapsed_ms}; restarting with backoff\")\n\n      next_attempt = next_retry_attempt_from_running(running_entry)\n\n      state\n      |> terminate_running_issue(issue_id, false)\n      |> schedule_issue_retry(issue_id, next_attempt, %{\n        identifier: identifier,\n        error: \"stalled for #{elapsed_ms}ms without codex activity\"\n      })\n    else\n      state\n    end\n  end\n\n  defp stall_elapsed_ms(running_entry, now) do\n    running_entry\n    |> last_activity_timestamp()\n    |> case do\n      %DateTime{} = timestamp ->\n        max(0, DateTime.diff(now, timestamp, :millisecond))\n\n      _ ->\n        nil\n    end\n  end\n\n  defp last_activity_timestamp(running_entry) when is_map(running_entry) do\n    Map.get(running_entry, :last_codex_timestamp) || Map.get(running_entry, :started_at)\n  end\n\n  defp last_activity_timestamp(_running_entry), do: nil\n\n  defp terminate_task(pid) when is_pid(pid) do\n    case Task.Supervisor.terminate_child(SymphonyElixir.TaskSupervisor, pid) do\n      :ok ->\n        :ok\n\n      {:error, :not_found} ->\n        Process.exit(pid, :shutdown)\n    end\n  end\n\n  defp terminate_task(_pid), do: :ok\n\n  defp choose_issues(issues, state) do\n    active_states = active_state_set()\n    terminal_states = terminal_state_set()\n\n    issues\n    |> sort_issues_for_dispatch()\n    |> Enum.reduce(state, fn issue, state_acc ->\n      if should_dispatch_issue?(issue, state_acc, active_states, terminal_states) do\n        dispatch_issue(state_acc, issue)\n      else\n        state_acc\n      end\n    end)\n  end\n\n  defp sort_issues_for_dispatch(issues) when is_list(issues) do\n    Enum.sort_by(issues, fn\n      %Issue{} = issue ->\n        {priority_rank(issue.priority), issue_created_at_sort_key(issue), issue.identifier || issue.id || \"\"}\n\n      _ ->\n        {priority_rank(nil), issue_created_at_sort_key(nil), \"\"}\n    end)\n  end\n\n  defp priority_rank(priority) when is_integer(priority) and priority in 1..4, do: priority\n  defp priority_rank(_priority), do: 5\n\n  defp issue_created_at_sort_key(%Issue{created_at: %DateTime{} = created_at}) do\n    DateTime.to_unix(created_at, :microsecond)\n  end\n\n  defp issue_created_at_sort_key(%Issue{}), do: 9_223_372_036_854_775_807\n  defp issue_created_at_sort_key(_issue), do: 9_223_372_036_854_775_807\n\n  defp should_dispatch_issue?(\n         %Issue{} = issue,\n         %State{running: running, claimed: claimed} = state,\n         active_states,\n         terminal_states\n       ) do\n    candidate_issue?(issue, active_states, terminal_states) and\n      !todo_issue_blocked_by_non_terminal?(issue, terminal_states) and\n      !MapSet.member?(claimed, issue.id) and\n      !Map.has_key?(running, issue.id) and\n      available_slots(state) > 0 and\n      state_slots_available?(issue, running) and\n      worker_slots_available?(state)\n  end\n\n  defp should_dispatch_issue?(_issue, _state, _active_states, _terminal_states), do: false\n\n  defp state_slots_available?(%Issue{state: issue_state}, running) when is_map(running) do\n    limit = Config.max_concurrent_agents_for_state(issue_state)\n    used = running_issue_count_for_state(running, issue_state)\n    limit > used\n  end\n\n  defp state_slots_available?(_issue, _running), do: false\n\n  defp running_issue_count_for_state(running, issue_state) when is_map(running) do\n    normalized_state = normalize_issue_state(issue_state)\n\n    Enum.count(running, fn\n      {_id, %{issue: %Issue{state: state_name}}} ->\n        normalize_issue_state(state_name) == normalized_state\n\n      _ ->\n        false\n    end)\n  end\n\n  defp candidate_issue?(\n         %Issue{\n           id: id,\n           identifier: identifier,\n           title: title,\n           state: state_name\n         } = issue,\n         active_states,\n         terminal_states\n       )\n       when is_binary(id) and is_binary(identifier) and is_binary(title) and is_binary(state_name) do\n    issue_routable_to_worker?(issue) and\n      active_issue_state?(state_name, active_states) and\n      !terminal_issue_state?(state_name, terminal_states)\n  end\n\n  defp candidate_issue?(_issue, _active_states, _terminal_states), do: false\n\n  defp issue_routable_to_worker?(%Issue{assigned_to_worker: assigned_to_worker})\n       when is_boolean(assigned_to_worker),\n       do: assigned_to_worker\n\n  defp issue_routable_to_worker?(_issue), do: true\n\n  defp todo_issue_blocked_by_non_terminal?(\n         %Issue{state: issue_state, blocked_by: blockers},\n         terminal_states\n       )\n       when is_binary(issue_state) and is_list(blockers) do\n    normalize_issue_state(issue_state) == \"todo\" and\n      Enum.any?(blockers, fn\n        %{state: blocker_state} when is_binary(blocker_state) ->\n          !terminal_issue_state?(blocker_state, terminal_states)\n\n        _ ->\n          true\n      end)\n  end\n\n  defp todo_issue_blocked_by_non_terminal?(_issue, _terminal_states), do: false\n\n  defp terminal_issue_state?(state_name, terminal_states) when is_binary(state_name) do\n    MapSet.member?(terminal_states, normalize_issue_state(state_name))\n  end\n\n  defp terminal_issue_state?(_state_name, _terminal_states), do: false\n\n  defp active_issue_state?(state_name, active_states) when is_binary(state_name) do\n    MapSet.member?(active_states, normalize_issue_state(state_name))\n  end\n\n  defp normalize_issue_state(state_name) when is_binary(state_name) do\n    String.downcase(String.trim(state_name))\n  end\n\n  defp terminal_state_set do\n    Config.settings!().tracker.terminal_states\n    |> Enum.map(&normalize_issue_state/1)\n    |> Enum.filter(&(&1 != \"\"))\n    |> MapSet.new()\n  end\n\n  defp active_state_set do\n    Config.settings!().tracker.active_states\n    |> Enum.map(&normalize_issue_state/1)\n    |> Enum.filter(&(&1 != \"\"))\n    |> MapSet.new()\n  end\n\n  defp dispatch_issue(%State{} = state, issue, attempt \\\\ nil, preferred_worker_host \\\\ nil) do\n    case revalidate_issue_for_dispatch(issue, &Tracker.fetch_issue_states_by_ids/1, terminal_state_set()) do\n      {:ok, %Issue{} = refreshed_issue} ->\n        do_dispatch_issue(state, refreshed_issue, attempt, preferred_worker_host)\n\n      {:skip, :missing} ->\n        Logger.info(\"Skipping dispatch; issue no longer active or visible: #{issue_context(issue)}\")\n        state\n\n      {:skip, %Issue{} = refreshed_issue} ->\n        Logger.info(\"Skipping stale dispatch after issue refresh: #{issue_context(refreshed_issue)} state=#{inspect(refreshed_issue.state)} blocked_by=#{length(refreshed_issue.blocked_by)}\")\n\n        state\n\n      {:error, reason} ->\n        Logger.warning(\"Skipping dispatch; issue refresh failed for #{issue_context(issue)}: #{inspect(reason)}\")\n        state\n    end\n  end\n\n  defp do_dispatch_issue(%State{} = state, issue, attempt, preferred_worker_host) do\n    recipient = self()\n\n    case select_worker_host(state, preferred_worker_host) do\n      :no_worker_capacity ->\n        Logger.debug(\"No SSH worker slots available for #{issue_context(issue)} preferred_worker_host=#{inspect(preferred_worker_host)}\")\n        state\n\n      worker_host ->\n        spawn_issue_on_worker_host(state, issue, attempt, recipient, worker_host)\n    end\n  end\n\n  defp spawn_issue_on_worker_host(%State{} = state, issue, attempt, recipient, worker_host) do\n    case Task.Supervisor.start_child(SymphonyElixir.TaskSupervisor, fn ->\n           AgentRunner.run(issue, recipient, attempt: attempt, worker_host: worker_host)\n         end) do\n      {:ok, pid} ->\n        ref = Process.monitor(pid)\n\n        Logger.info(\"Dispatching issue to agent: #{issue_context(issue)} pid=#{inspect(pid)} attempt=#{inspect(attempt)} worker_host=#{worker_host || \"local\"}\")\n\n        running =\n          Map.put(state.running, issue.id, %{\n            pid: pid,\n            ref: ref,\n            identifier: issue.identifier,\n            issue: issue,\n            worker_host: worker_host,\n            workspace_path: nil,\n            session_id: nil,\n            last_codex_message: nil,\n            last_codex_timestamp: nil,\n            last_codex_event: nil,\n            codex_app_server_pid: nil,\n            codex_input_tokens: 0,\n            codex_output_tokens: 0,\n            codex_total_tokens: 0,\n            codex_last_reported_input_tokens: 0,\n            codex_last_reported_output_tokens: 0,\n            codex_last_reported_total_tokens: 0,\n            turn_count: 0,\n            retry_attempt: normalize_retry_attempt(attempt),\n            started_at: DateTime.utc_now()\n          })\n\n        %{\n          state\n          | running: running,\n            claimed: MapSet.put(state.claimed, issue.id),\n            retry_attempts: Map.delete(state.retry_attempts, issue.id)\n        }\n\n      {:error, reason} ->\n        Logger.error(\"Unable to spawn agent for #{issue_context(issue)}: #{inspect(reason)}\")\n        next_attempt = if is_integer(attempt), do: attempt + 1, else: nil\n\n        schedule_issue_retry(state, issue.id, next_attempt, %{\n          identifier: issue.identifier,\n          error: \"failed to spawn agent: #{inspect(reason)}\",\n          worker_host: worker_host\n        })\n    end\n  end\n\n  defp revalidate_issue_for_dispatch(%Issue{id: issue_id}, issue_fetcher, terminal_states)\n       when is_binary(issue_id) and is_function(issue_fetcher, 1) do\n    case issue_fetcher.([issue_id]) do\n      {:ok, [%Issue{} = refreshed_issue | _]} ->\n        if retry_candidate_issue?(refreshed_issue, terminal_states) do\n          {:ok, refreshed_issue}\n        else\n          {:skip, refreshed_issue}\n        end\n\n      {:ok, []} ->\n        {:skip, :missing}\n\n      {:error, reason} ->\n        {:error, reason}\n    end\n  end\n\n  defp revalidate_issue_for_dispatch(issue, _issue_fetcher, _terminal_states), do: {:ok, issue}\n\n  defp complete_issue(%State{} = state, issue_id) do\n    %{\n      state\n      | completed: MapSet.put(state.completed, issue_id),\n        retry_attempts: Map.delete(state.retry_attempts, issue_id)\n    }\n  end\n\n  defp schedule_issue_retry(%State{} = state, issue_id, attempt, metadata)\n       when is_binary(issue_id) and is_map(metadata) do\n    previous_retry = Map.get(state.retry_attempts, issue_id, %{attempt: 0})\n    next_attempt = if is_integer(attempt), do: attempt, else: previous_retry.attempt + 1\n    delay_ms = retry_delay(next_attempt, metadata)\n    old_timer = Map.get(previous_retry, :timer_ref)\n    retry_token = make_ref()\n    due_at_ms = System.monotonic_time(:millisecond) + delay_ms\n    identifier = pick_retry_identifier(issue_id, previous_retry, metadata)\n    error = pick_retry_error(previous_retry, metadata)\n    worker_host = pick_retry_worker_host(previous_retry, metadata)\n    workspace_path = pick_retry_workspace_path(previous_retry, metadata)\n\n    if is_reference(old_timer) do\n      Process.cancel_timer(old_timer)\n    end\n\n    timer_ref = Process.send_after(self(), {:retry_issue, issue_id, retry_token}, delay_ms)\n\n    error_suffix = if is_binary(error), do: \" error=#{error}\", else: \"\"\n\n    Logger.warning(\"Retrying issue_id=#{issue_id} issue_identifier=#{identifier} in #{delay_ms}ms (attempt #{next_attempt})#{error_suffix}\")\n\n    %{\n      state\n      | retry_attempts:\n          Map.put(state.retry_attempts, issue_id, %{\n            attempt: next_attempt,\n            timer_ref: timer_ref,\n            retry_token: retry_token,\n            due_at_ms: due_at_ms,\n            identifier: identifier,\n            error: error,\n            worker_host: worker_host,\n            workspace_path: workspace_path\n          })\n    }\n  end\n\n  defp pop_retry_attempt_state(%State{} = state, issue_id, retry_token) when is_reference(retry_token) do\n    case Map.get(state.retry_attempts, issue_id) do\n      %{attempt: attempt, retry_token: ^retry_token} = retry_entry ->\n        metadata = %{\n          identifier: Map.get(retry_entry, :identifier),\n          error: Map.get(retry_entry, :error),\n          worker_host: Map.get(retry_entry, :worker_host),\n          workspace_path: Map.get(retry_entry, :workspace_path)\n        }\n\n        {:ok, attempt, metadata, %{state | retry_attempts: Map.delete(state.retry_attempts, issue_id)}}\n\n      _ ->\n        :missing\n    end\n  end\n\n  defp handle_retry_issue(%State{} = state, issue_id, attempt, metadata) do\n    case Tracker.fetch_candidate_issues() do\n      {:ok, issues} ->\n        issues\n        |> find_issue_by_id(issue_id)\n        |> handle_retry_issue_lookup(state, issue_id, attempt, metadata)\n\n      {:error, reason} ->\n        Logger.warning(\"Retry poll failed for issue_id=#{issue_id} issue_identifier=#{metadata[:identifier] || issue_id}: #{inspect(reason)}\")\n\n        {:noreply,\n         schedule_issue_retry(\n           state,\n           issue_id,\n           attempt + 1,\n           Map.merge(metadata, %{error: \"retry poll failed: #{inspect(reason)}\"})\n         )}\n    end\n  end\n\n  defp handle_retry_issue_lookup(%Issue{} = issue, state, issue_id, attempt, metadata) do\n    terminal_states = terminal_state_set()\n\n    cond do\n      terminal_issue_state?(issue.state, terminal_states) ->\n        Logger.info(\"Issue state is terminal: issue_id=#{issue_id} issue_identifier=#{issue.identifier} state=#{issue.state}; removing associated workspace\")\n\n        cleanup_issue_workspace(issue.identifier, metadata[:worker_host])\n        {:noreply, release_issue_claim(state, issue_id)}\n\n      retry_candidate_issue?(issue, terminal_states) ->\n        handle_active_retry(state, issue, attempt, metadata)\n\n      true ->\n        Logger.debug(\"Issue left active states, removing claim issue_id=#{issue_id} issue_identifier=#{issue.identifier}\")\n\n        {:noreply, release_issue_claim(state, issue_id)}\n    end\n  end\n\n  defp handle_retry_issue_lookup(nil, state, issue_id, _attempt, _metadata) do\n    Logger.debug(\"Issue no longer visible, removing claim issue_id=#{issue_id}\")\n    {:noreply, release_issue_claim(state, issue_id)}\n  end\n\n  defp cleanup_issue_workspace(identifier, worker_host \\\\ nil)\n\n  defp cleanup_issue_workspace(identifier, worker_host) when is_binary(identifier) do\n    Workspace.remove_issue_workspaces(identifier, worker_host)\n  end\n\n  defp cleanup_issue_workspace(_identifier, _worker_host), do: :ok\n\n  defp run_terminal_workspace_cleanup do\n    case Tracker.fetch_issues_by_states(Config.settings!().tracker.terminal_states) do\n      {:ok, issues} ->\n        issues\n        |> Enum.each(fn\n          %Issue{identifier: identifier} when is_binary(identifier) ->\n            cleanup_issue_workspace(identifier)\n\n          _ ->\n            :ok\n        end)\n\n      {:error, reason} ->\n        Logger.warning(\"Skipping startup terminal workspace cleanup; failed to fetch terminal issues: #{inspect(reason)}\")\n    end\n  end\n\n  defp notify_dashboard do\n    StatusDashboard.notify_update()\n  end\n\n  defp handle_active_retry(state, issue, attempt, metadata) do\n    if retry_candidate_issue?(issue, terminal_state_set()) and\n         dispatch_slots_available?(issue, state) and\n         worker_slots_available?(state, metadata[:worker_host]) do\n      {:noreply, dispatch_issue(state, issue, attempt, metadata[:worker_host])}\n    else\n      Logger.debug(\"No available slots for retrying #{issue_context(issue)}; retrying again\")\n\n      {:noreply,\n       schedule_issue_retry(\n         state,\n         issue.id,\n         attempt + 1,\n         Map.merge(metadata, %{\n           identifier: issue.identifier,\n           error: \"no available orchestrator slots\"\n         })\n       )}\n    end\n  end\n\n  defp release_issue_claim(%State{} = state, issue_id) do\n    %{state | claimed: MapSet.delete(state.claimed, issue_id)}\n  end\n\n  defp retry_delay(attempt, metadata) when is_integer(attempt) and attempt > 0 and is_map(metadata) do\n    if metadata[:delay_type] == :continuation and attempt == 1 do\n      @continuation_retry_delay_ms\n    else\n      failure_retry_delay(attempt)\n    end\n  end\n\n  defp failure_retry_delay(attempt) do\n    max_delay_power = min(attempt - 1, 10)\n    min(@failure_retry_base_ms * (1 <<< max_delay_power), Config.settings!().agent.max_retry_backoff_ms)\n  end\n\n  defp normalize_retry_attempt(attempt) when is_integer(attempt) and attempt > 0, do: attempt\n  defp normalize_retry_attempt(_attempt), do: 0\n\n  defp next_retry_attempt_from_running(running_entry) do\n    case Map.get(running_entry, :retry_attempt) do\n      attempt when is_integer(attempt) and attempt > 0 -> attempt + 1\n      _ -> nil\n    end\n  end\n\n  defp pick_retry_identifier(issue_id, previous_retry, metadata) do\n    metadata[:identifier] || Map.get(previous_retry, :identifier) || issue_id\n  end\n\n  defp pick_retry_error(previous_retry, metadata) do\n    metadata[:error] || Map.get(previous_retry, :error)\n  end\n\n  defp pick_retry_worker_host(previous_retry, metadata) do\n    metadata[:worker_host] || Map.get(previous_retry, :worker_host)\n  end\n\n  defp pick_retry_workspace_path(previous_retry, metadata) do\n    metadata[:workspace_path] || Map.get(previous_retry, :workspace_path)\n  end\n\n  defp maybe_put_runtime_value(running_entry, _key, nil), do: running_entry\n\n  defp maybe_put_runtime_value(running_entry, key, value) when is_map(running_entry) do\n    Map.put(running_entry, key, value)\n  end\n\n  defp select_worker_host(%State{} = state, preferred_worker_host) do\n    case Config.settings!().worker.ssh_hosts do\n      [] ->\n        nil\n\n      hosts ->\n        available_hosts = Enum.filter(hosts, &worker_host_slots_available?(state, &1))\n\n        cond do\n          available_hosts == [] ->\n            :no_worker_capacity\n\n          preferred_worker_host_available?(preferred_worker_host, available_hosts) ->\n            preferred_worker_host\n\n          true ->\n            least_loaded_worker_host(state, available_hosts)\n        end\n    end\n  end\n\n  defp preferred_worker_host_available?(preferred_worker_host, hosts)\n       when is_binary(preferred_worker_host) and is_list(hosts) do\n    preferred_worker_host != \"\" and preferred_worker_host in hosts\n  end\n\n  defp preferred_worker_host_available?(_preferred_worker_host, _hosts), do: false\n\n  defp least_loaded_worker_host(%State{} = state, hosts) when is_list(hosts) do\n    hosts\n    |> Enum.with_index()\n    |> Enum.min_by(fn {host, index} ->\n      {running_worker_host_count(state.running, host), index}\n    end)\n    |> elem(0)\n  end\n\n  defp running_worker_host_count(running, worker_host) when is_map(running) and is_binary(worker_host) do\n    Enum.count(running, fn\n      {_issue_id, %{worker_host: ^worker_host}} -> true\n      _ -> false\n    end)\n  end\n\n  defp worker_slots_available?(%State{} = state) do\n    select_worker_host(state, nil) != :no_worker_capacity\n  end\n\n  defp worker_slots_available?(%State{} = state, preferred_worker_host) do\n    select_worker_host(state, preferred_worker_host) != :no_worker_capacity\n  end\n\n  defp worker_host_slots_available?(%State{} = state, worker_host) when is_binary(worker_host) do\n    case Config.settings!().worker.max_concurrent_agents_per_host do\n      limit when is_integer(limit) and limit > 0 ->\n        running_worker_host_count(state.running, worker_host) < limit\n\n      _ ->\n        true\n    end\n  end\n\n  defp find_issue_by_id(issues, issue_id) when is_binary(issue_id) do\n    Enum.find(issues, fn\n      %Issue{id: ^issue_id} ->\n        true\n\n      _ ->\n        false\n    end)\n  end\n\n  defp find_issue_id_for_ref(running, ref) do\n    running\n    |> Enum.find_value(fn {issue_id, %{ref: running_ref}} ->\n      if running_ref == ref, do: issue_id\n    end)\n  end\n\n  defp running_entry_session_id(%{session_id: session_id}) when is_binary(session_id),\n    do: session_id\n\n  defp running_entry_session_id(_running_entry), do: \"n/a\"\n\n  defp issue_context(%Issue{id: issue_id, identifier: identifier}) do\n    \"issue_id=#{issue_id} issue_identifier=#{identifier}\"\n  end\n\n  defp available_slots(%State{} = state) do\n    max(\n      (state.max_concurrent_agents || Config.settings!().agent.max_concurrent_agents) -\n        map_size(state.running),\n      0\n    )\n  end\n\n  @spec request_refresh() :: map() | :unavailable\n  def request_refresh do\n    request_refresh(__MODULE__)\n  end\n\n  @spec request_refresh(GenServer.server()) :: map() | :unavailable\n  def request_refresh(server) do\n    if Process.whereis(server) do\n      GenServer.call(server, :request_refresh)\n    else\n      :unavailable\n    end\n  end\n\n  @spec snapshot() :: map() | :timeout | :unavailable\n  def snapshot, do: snapshot(__MODULE__, 15_000)\n\n  @spec snapshot(GenServer.server(), timeout()) :: map() | :timeout | :unavailable\n  def snapshot(server, timeout) do\n    if Process.whereis(server) do\n      try do\n        GenServer.call(server, :snapshot, timeout)\n      catch\n        :exit, {:timeout, _} -> :timeout\n        :exit, _ -> :unavailable\n      end\n    else\n      :unavailable\n    end\n  end\n\n  @impl true\n  def handle_call(:snapshot, _from, state) do\n    state = refresh_runtime_config(state)\n    now = DateTime.utc_now()\n    now_ms = System.monotonic_time(:millisecond)\n\n    running =\n      state.running\n      |> Enum.map(fn {issue_id, metadata} ->\n        %{\n          issue_id: issue_id,\n          identifier: metadata.identifier,\n          state: metadata.issue.state,\n          worker_host: Map.get(metadata, :worker_host),\n          workspace_path: Map.get(metadata, :workspace_path),\n          session_id: metadata.session_id,\n          codex_app_server_pid: metadata.codex_app_server_pid,\n          codex_input_tokens: metadata.codex_input_tokens,\n          codex_output_tokens: metadata.codex_output_tokens,\n          codex_total_tokens: metadata.codex_total_tokens,\n          turn_count: Map.get(metadata, :turn_count, 0),\n          started_at: metadata.started_at,\n          last_codex_timestamp: metadata.last_codex_timestamp,\n          last_codex_message: metadata.last_codex_message,\n          last_codex_event: metadata.last_codex_event,\n          runtime_seconds: running_seconds(metadata.started_at, now)\n        }\n      end)\n\n    retrying =\n      state.retry_attempts\n      |> Enum.map(fn {issue_id, %{attempt: attempt, due_at_ms: due_at_ms} = retry} ->\n        %{\n          issue_id: issue_id,\n          attempt: attempt,\n          due_in_ms: max(0, due_at_ms - now_ms),\n          identifier: Map.get(retry, :identifier),\n          error: Map.get(retry, :error),\n          worker_host: Map.get(retry, :worker_host),\n          workspace_path: Map.get(retry, :workspace_path)\n        }\n      end)\n\n    {:reply,\n     %{\n       running: running,\n       retrying: retrying,\n       codex_totals: state.codex_totals,\n       rate_limits: Map.get(state, :codex_rate_limits),\n       polling: %{\n         checking?: state.poll_check_in_progress == true,\n         next_poll_in_ms: next_poll_in_ms(state.next_poll_due_at_ms, now_ms),\n         poll_interval_ms: state.poll_interval_ms\n       }\n     }, state}\n  end\n\n  def handle_call(:request_refresh, _from, state) do\n    now_ms = System.monotonic_time(:millisecond)\n    already_due? = is_integer(state.next_poll_due_at_ms) and state.next_poll_due_at_ms <= now_ms\n    coalesced = state.poll_check_in_progress == true or already_due?\n    state = if coalesced, do: state, else: schedule_tick(state, 0)\n\n    {:reply,\n     %{\n       queued: true,\n       coalesced: coalesced,\n       requested_at: DateTime.utc_now(),\n       operations: [\"poll\", \"reconcile\"]\n     }, state}\n  end\n\n  defp integrate_codex_update(running_entry, %{event: event, timestamp: timestamp} = update) do\n    token_delta = extract_token_delta(running_entry, update)\n    codex_input_tokens = Map.get(running_entry, :codex_input_tokens, 0)\n    codex_output_tokens = Map.get(running_entry, :codex_output_tokens, 0)\n    codex_total_tokens = Map.get(running_entry, :codex_total_tokens, 0)\n    codex_app_server_pid = Map.get(running_entry, :codex_app_server_pid)\n    last_reported_input = Map.get(running_entry, :codex_last_reported_input_tokens, 0)\n    last_reported_output = Map.get(running_entry, :codex_last_reported_output_tokens, 0)\n    last_reported_total = Map.get(running_entry, :codex_last_reported_total_tokens, 0)\n    turn_count = Map.get(running_entry, :turn_count, 0)\n\n    {\n      Map.merge(running_entry, %{\n        last_codex_timestamp: timestamp,\n        last_codex_message: summarize_codex_update(update),\n        session_id: session_id_for_update(running_entry.session_id, update),\n        last_codex_event: event,\n        codex_app_server_pid: codex_app_server_pid_for_update(codex_app_server_pid, update),\n        codex_input_tokens: codex_input_tokens + token_delta.input_tokens,\n        codex_output_tokens: codex_output_tokens + token_delta.output_tokens,\n        codex_total_tokens: codex_total_tokens + token_delta.total_tokens,\n        codex_last_reported_input_tokens: max(last_reported_input, token_delta.input_reported),\n        codex_last_reported_output_tokens: max(last_reported_output, token_delta.output_reported),\n        codex_last_reported_total_tokens: max(last_reported_total, token_delta.total_reported),\n        turn_count: turn_count_for_update(turn_count, running_entry.session_id, update)\n      }),\n      token_delta\n    }\n  end\n\n  defp codex_app_server_pid_for_update(_existing, %{codex_app_server_pid: pid})\n       when is_binary(pid),\n       do: pid\n\n  defp codex_app_server_pid_for_update(_existing, %{codex_app_server_pid: pid})\n       when is_integer(pid),\n       do: Integer.to_string(pid)\n\n  defp codex_app_server_pid_for_update(_existing, %{codex_app_server_pid: pid}) when is_list(pid),\n    do: to_string(pid)\n\n  defp codex_app_server_pid_for_update(existing, _update), do: existing\n\n  defp session_id_for_update(_existing, %{session_id: session_id}) when is_binary(session_id),\n    do: session_id\n\n  defp session_id_for_update(existing, _update), do: existing\n\n  defp turn_count_for_update(existing_count, existing_session_id, %{\n         event: :session_started,\n         session_id: session_id\n       })\n       when is_integer(existing_count) and is_binary(session_id) do\n    if session_id == existing_session_id do\n      existing_count\n    else\n      existing_count + 1\n    end\n  end\n\n  defp turn_count_for_update(existing_count, _existing_session_id, _update)\n       when is_integer(existing_count),\n       do: existing_count\n\n  defp turn_count_for_update(_existing_count, _existing_session_id, _update), do: 0\n\n  defp summarize_codex_update(update) do\n    %{\n      event: update[:event],\n      message: update[:payload] || update[:raw],\n      timestamp: update[:timestamp]\n    }\n  end\n\n  defp schedule_tick(%State{} = state, delay_ms) when is_integer(delay_ms) and delay_ms >= 0 do\n    if is_reference(state.tick_timer_ref) do\n      Process.cancel_timer(state.tick_timer_ref)\n    end\n\n    tick_token = make_ref()\n    timer_ref = Process.send_after(self(), {:tick, tick_token}, delay_ms)\n\n    %{\n      state\n      | tick_timer_ref: timer_ref,\n        tick_token: tick_token,\n        next_poll_due_at_ms: System.monotonic_time(:millisecond) + delay_ms\n    }\n  end\n\n  defp schedule_poll_cycle_start do\n    :timer.send_after(@poll_transition_render_delay_ms, self(), :run_poll_cycle)\n    :ok\n  end\n\n  defp next_poll_in_ms(nil, _now_ms), do: nil\n\n  defp next_poll_in_ms(next_poll_due_at_ms, now_ms) when is_integer(next_poll_due_at_ms) do\n    max(0, next_poll_due_at_ms - now_ms)\n  end\n\n  defp pop_running_entry(state, issue_id) do\n    {Map.get(state.running, issue_id), %{state | running: Map.delete(state.running, issue_id)}}\n  end\n\n  defp record_session_completion_totals(state, running_entry) when is_map(running_entry) do\n    runtime_seconds = running_seconds(running_entry.started_at, DateTime.utc_now())\n\n    codex_totals =\n      apply_token_delta(\n        state.codex_totals,\n        %{\n          input_tokens: 0,\n          output_tokens: 0,\n          total_tokens: 0,\n          seconds_running: runtime_seconds\n        }\n      )\n\n    %{state | codex_totals: codex_totals}\n  end\n\n  defp record_session_completion_totals(state, _running_entry), do: state\n\n  defp refresh_runtime_config(%State{} = state) do\n    config = Config.settings!()\n\n    %{\n      state\n      | poll_interval_ms: config.polling.interval_ms,\n        max_concurrent_agents: config.agent.max_concurrent_agents\n    }\n  end\n\n  defp retry_candidate_issue?(%Issue{} = issue, terminal_states) do\n    candidate_issue?(issue, active_state_set(), terminal_states) and\n      !todo_issue_blocked_by_non_terminal?(issue, terminal_states)\n  end\n\n  defp dispatch_slots_available?(%Issue{} = issue, %State{} = state) do\n    available_slots(state) > 0 and state_slots_available?(issue, state.running)\n  end\n\n  defp apply_codex_token_delta(\n         %{codex_totals: codex_totals} = state,\n         %{input_tokens: input, output_tokens: output, total_tokens: total} = token_delta\n       )\n       when is_integer(input) and is_integer(output) and is_integer(total) do\n    %{state | codex_totals: apply_token_delta(codex_totals, token_delta)}\n  end\n\n  defp apply_codex_token_delta(state, _token_delta), do: state\n\n  defp apply_codex_rate_limits(%State{} = state, update) when is_map(update) do\n    case extract_rate_limits(update) do\n      %{} = rate_limits ->\n        %{state | codex_rate_limits: rate_limits}\n\n      _ ->\n        state\n    end\n  end\n\n  defp apply_codex_rate_limits(state, _update), do: state\n\n  defp apply_token_delta(codex_totals, token_delta) do\n    input_tokens = Map.get(codex_totals, :input_tokens, 0) + token_delta.input_tokens\n    output_tokens = Map.get(codex_totals, :output_tokens, 0) + token_delta.output_tokens\n    total_tokens = Map.get(codex_totals, :total_tokens, 0) + token_delta.total_tokens\n\n    seconds_running =\n      Map.get(codex_totals, :seconds_running, 0) + Map.get(token_delta, :seconds_running, 0)\n\n    %{\n      input_tokens: max(0, input_tokens),\n      output_tokens: max(0, output_tokens),\n      total_tokens: max(0, total_tokens),\n      seconds_running: max(0, seconds_running)\n    }\n  end\n\n  defp extract_token_delta(running_entry, %{event: _, timestamp: _} = update) do\n    running_entry = running_entry || %{}\n    usage = extract_token_usage(update)\n\n    {\n      compute_token_delta(\n        running_entry,\n        :input,\n        usage,\n        :codex_last_reported_input_tokens\n      ),\n      compute_token_delta(\n        running_entry,\n        :output,\n        usage,\n        :codex_last_reported_output_tokens\n      ),\n      compute_token_delta(\n        running_entry,\n        :total,\n        usage,\n        :codex_last_reported_total_tokens\n      )\n    }\n    |> Tuple.to_list()\n    |> then(fn [input, output, total] ->\n      %{\n        input_tokens: input.delta,\n        output_tokens: output.delta,\n        total_tokens: total.delta,\n        input_reported: input.reported,\n        output_reported: output.reported,\n        total_reported: total.reported\n      }\n    end)\n  end\n\n  defp compute_token_delta(running_entry, token_key, usage, reported_key) do\n    next_total = get_token_usage(usage, token_key)\n    prev_reported = Map.get(running_entry, reported_key, 0)\n\n    delta =\n      if is_integer(next_total) and next_total >= prev_reported do\n        next_total - prev_reported\n      else\n        0\n      end\n\n    %{\n      delta: max(delta, 0),\n      reported: if(is_integer(next_total), do: next_total, else: prev_reported)\n    }\n  end\n\n  defp extract_token_usage(update) do\n    payloads = [\n      update[:usage],\n      Map.get(update, \"usage\"),\n      Map.get(update, :usage),\n      update[:payload],\n      Map.get(update, \"payload\"),\n      update\n    ]\n\n    Enum.find_value(payloads, &absolute_token_usage_from_payload/1) ||\n      Enum.find_value(payloads, &turn_completed_usage_from_payload/1) ||\n      %{}\n  end\n\n  defp extract_rate_limits(update) do\n    rate_limits_from_payload(update[:rate_limits]) ||\n      rate_limits_from_payload(Map.get(update, \"rate_limits\")) ||\n      rate_limits_from_payload(Map.get(update, :rate_limits)) ||\n      rate_limits_from_payload(update[:payload]) ||\n      rate_limits_from_payload(Map.get(update, \"payload\")) ||\n      rate_limits_from_payload(update)\n  end\n\n  defp absolute_token_usage_from_payload(payload) when is_map(payload) do\n    absolute_paths = [\n      [\"params\", \"msg\", \"payload\", \"info\", \"total_token_usage\"],\n      [:params, :msg, :payload, :info, :total_token_usage],\n      [\"params\", \"msg\", \"info\", \"total_token_usage\"],\n      [:params, :msg, :info, :total_token_usage],\n      [\"params\", \"tokenUsage\", \"total\"],\n      [:params, :tokenUsage, :total],\n      [\"tokenUsage\", \"total\"],\n      [:tokenUsage, :total]\n    ]\n\n    explicit_map_at_paths(payload, absolute_paths)\n  end\n\n  defp absolute_token_usage_from_payload(_payload), do: nil\n\n  defp turn_completed_usage_from_payload(payload) when is_map(payload) do\n    method = Map.get(payload, \"method\") || Map.get(payload, :method)\n\n    if method in [\"turn/completed\", :turn_completed] do\n      direct =\n        Map.get(payload, \"usage\") ||\n          Map.get(payload, :usage) ||\n          map_at_path(payload, [\"params\", \"usage\"]) ||\n          map_at_path(payload, [:params, :usage])\n\n      if is_map(direct) and integer_token_map?(direct), do: direct\n    end\n  end\n\n  defp turn_completed_usage_from_payload(_payload), do: nil\n\n  defp rate_limits_from_payload(payload) when is_map(payload) do\n    direct = Map.get(payload, \"rate_limits\") || Map.get(payload, :rate_limits)\n\n    cond do\n      rate_limits_map?(direct) ->\n        direct\n\n      rate_limits_map?(payload) ->\n        payload\n\n      true ->\n        rate_limit_payloads(payload)\n    end\n  end\n\n  defp rate_limits_from_payload(payload) when is_list(payload) do\n    rate_limit_payloads(payload)\n  end\n\n  defp rate_limits_from_payload(_payload), do: nil\n\n  defp rate_limit_payloads(payload) when is_map(payload) do\n    Map.values(payload)\n    |> Enum.reduce_while(nil, fn\n      value, nil ->\n        case rate_limits_from_payload(value) do\n          nil -> {:cont, nil}\n          rate_limits -> {:halt, rate_limits}\n        end\n\n      _value, result ->\n        {:halt, result}\n    end)\n  end\n\n  defp rate_limit_payloads(payload) when is_list(payload) do\n    payload\n    |> Enum.reduce_while(nil, fn\n      value, nil ->\n        case rate_limits_from_payload(value) do\n          nil -> {:cont, nil}\n          rate_limits -> {:halt, rate_limits}\n        end\n\n      _value, result ->\n        {:halt, result}\n    end)\n  end\n\n  defp rate_limits_map?(payload) when is_map(payload) do\n    limit_id =\n      Map.get(payload, \"limit_id\") ||\n        Map.get(payload, :limit_id) ||\n        Map.get(payload, \"limit_name\") ||\n        Map.get(payload, :limit_name)\n\n    has_buckets =\n      Enum.any?(\n        [\"primary\", :primary, \"secondary\", :secondary, \"credits\", :credits],\n        &Map.has_key?(payload, &1)\n      )\n\n    !is_nil(limit_id) and has_buckets\n  end\n\n  defp rate_limits_map?(_payload), do: false\n\n  defp explicit_map_at_paths(payload, paths) when is_map(payload) and is_list(paths) do\n    Enum.find_value(paths, fn path ->\n      value = map_at_path(payload, path)\n\n      if is_map(value) and integer_token_map?(value), do: value\n    end)\n  end\n\n  defp explicit_map_at_paths(_payload, _paths), do: nil\n\n  defp map_at_path(payload, path) when is_map(payload) and is_list(path) do\n    Enum.reduce_while(path, payload, fn key, acc ->\n      if is_map(acc) and Map.has_key?(acc, key) do\n        {:cont, Map.get(acc, key)}\n      else\n        {:halt, nil}\n      end\n    end)\n  end\n\n  defp map_at_path(_payload, _path), do: nil\n\n  defp integer_token_map?(payload) do\n    token_fields = [\n      :input_tokens,\n      :output_tokens,\n      :total_tokens,\n      :prompt_tokens,\n      :completion_tokens,\n      :inputTokens,\n      :outputTokens,\n      :totalTokens,\n      :promptTokens,\n      :completionTokens,\n      \"input_tokens\",\n      \"output_tokens\",\n      \"total_tokens\",\n      \"prompt_tokens\",\n      \"completion_tokens\",\n      \"inputTokens\",\n      \"outputTokens\",\n      \"totalTokens\",\n      \"promptTokens\",\n      \"completionTokens\"\n    ]\n\n    token_fields\n    |> Enum.any?(fn field ->\n      value = payload_get(payload, field)\n      !is_nil(integer_like(value))\n    end)\n  end\n\n  defp get_token_usage(usage, :input),\n    do:\n      payload_get(usage, [\n        \"input_tokens\",\n        \"prompt_tokens\",\n        :input_tokens,\n        :prompt_tokens,\n        :input,\n        \"promptTokens\",\n        :promptTokens,\n        \"inputTokens\",\n        :inputTokens\n      ])\n\n  defp get_token_usage(usage, :output),\n    do:\n      payload_get(usage, [\n        \"output_tokens\",\n        \"completion_tokens\",\n        :output_tokens,\n        :completion_tokens,\n        :output,\n        :completion,\n        \"outputTokens\",\n        :outputTokens,\n        \"completionTokens\",\n        :completionTokens\n      ])\n\n  defp get_token_usage(usage, :total),\n    do:\n      payload_get(usage, [\n        \"total_tokens\",\n        \"total\",\n        :total_tokens,\n        :total,\n        \"totalTokens\",\n        :totalTokens\n      ])\n\n  defp payload_get(payload, fields) when is_list(fields) do\n    Enum.find_value(fields, fn field -> map_integer_value(payload, field) end)\n  end\n\n  defp payload_get(payload, field), do: map_integer_value(payload, field)\n\n  defp map_integer_value(payload, field) do\n    if is_map(payload) do\n      value = Map.get(payload, field)\n      integer_like(value)\n    else\n      nil\n    end\n  end\n\n  defp running_seconds(%DateTime{} = started_at, %DateTime{} = now) do\n    max(0, DateTime.diff(now, started_at, :second))\n  end\n\n  defp running_seconds(_started_at, _now), do: 0\n\n  defp integer_like(value) when is_integer(value) and value >= 0, do: value\n\n  defp integer_like(value) when is_binary(value) do\n    case Integer.parse(String.trim(value)) do\n      {num, _} when num >= 0 -> num\n      _ -> nil\n    end\n  end\n\n  defp integer_like(_value), do: nil\nend\n"
  },
  {
    "path": "elixir/lib/symphony_elixir/path_safety.ex",
    "content": "defmodule SymphonyElixir.PathSafety do\n  @moduledoc false\n\n  @spec canonicalize(Path.t()) :: {:ok, Path.t()} | {:error, term()}\n  def canonicalize(path) when is_binary(path) do\n    expanded_path = Path.expand(path)\n    {root, segments} = split_absolute_path(expanded_path)\n\n    case resolve_segments(root, [], segments) do\n      {:ok, canonical_path} ->\n        {:ok, canonical_path}\n\n      {:error, reason} ->\n        {:error, {:path_canonicalize_failed, expanded_path, reason}}\n    end\n  end\n\n  defp split_absolute_path(path) when is_binary(path) do\n    [root | segments] = Path.split(path)\n    {root, segments}\n  end\n\n  defp resolve_segments(root, resolved_segments, []), do: {:ok, join_path(root, resolved_segments)}\n\n  defp resolve_segments(root, resolved_segments, [segment | rest]) do\n    candidate_path = join_path(root, resolved_segments ++ [segment])\n\n    case File.lstat(candidate_path) do\n      {:ok, %File.Stat{type: :symlink}} ->\n        with {:ok, target} <- :file.read_link_all(String.to_charlist(candidate_path)) do\n          resolved_target = Path.expand(IO.chardata_to_string(target), join_path(root, resolved_segments))\n          {target_root, target_segments} = split_absolute_path(resolved_target)\n          resolve_segments(target_root, [], target_segments ++ rest)\n        end\n\n      {:ok, _stat} ->\n        resolve_segments(root, resolved_segments ++ [segment], rest)\n\n      {:error, :enoent} ->\n        {:ok, join_path(root, resolved_segments ++ [segment | rest])}\n\n      {:error, reason} ->\n        {:error, reason}\n    end\n  end\n\n  defp join_path(root, segments) when is_list(segments) do\n    Enum.reduce(segments, root, fn segment, acc -> Path.join(acc, segment) end)\n  end\nend\n"
  },
  {
    "path": "elixir/lib/symphony_elixir/prompt_builder.ex",
    "content": "defmodule SymphonyElixir.PromptBuilder do\n  @moduledoc \"\"\"\n  Builds agent prompts from Linear issue data.\n  \"\"\"\n\n  alias SymphonyElixir.{Config, Workflow}\n\n  @render_opts [strict_variables: true, strict_filters: true]\n\n  @spec build_prompt(SymphonyElixir.Linear.Issue.t(), keyword()) :: String.t()\n  def build_prompt(issue, opts \\\\ []) do\n    template =\n      Workflow.current()\n      |> prompt_template!()\n      |> parse_template!()\n\n    template\n    |> Solid.render!(\n      %{\n        \"attempt\" => Keyword.get(opts, :attempt),\n        \"issue\" => issue |> Map.from_struct() |> to_solid_map()\n      },\n      @render_opts\n    )\n    |> IO.iodata_to_binary()\n  end\n\n  defp prompt_template!({:ok, %{prompt_template: prompt}}), do: default_prompt(prompt)\n\n  defp prompt_template!({:error, reason}) do\n    raise RuntimeError, \"workflow_unavailable: #{inspect(reason)}\"\n  end\n\n  defp parse_template!(prompt) when is_binary(prompt) do\n    Solid.parse!(prompt)\n  rescue\n    error ->\n      reraise %RuntimeError{\n                message: \"template_parse_error: #{Exception.message(error)} template=#{inspect(prompt)}\"\n              },\n              __STACKTRACE__\n  end\n\n  defp to_solid_map(map) when is_map(map) do\n    Map.new(map, fn {key, value} -> {to_string(key), to_solid_value(value)} end)\n  end\n\n  defp to_solid_value(%DateTime{} = value), do: DateTime.to_iso8601(value)\n  defp to_solid_value(%NaiveDateTime{} = value), do: NaiveDateTime.to_iso8601(value)\n  defp to_solid_value(%Date{} = value), do: Date.to_iso8601(value)\n  defp to_solid_value(%Time{} = value), do: Time.to_iso8601(value)\n  defp to_solid_value(%_{} = value), do: value |> Map.from_struct() |> to_solid_map()\n  defp to_solid_value(value) when is_map(value), do: to_solid_map(value)\n  defp to_solid_value(value) when is_list(value), do: Enum.map(value, &to_solid_value/1)\n  defp to_solid_value(value), do: value\n\n  defp default_prompt(prompt) when is_binary(prompt) do\n    if String.trim(prompt) == \"\" do\n      Config.workflow_prompt()\n    else\n      prompt\n    end\n  end\nend\n"
  },
  {
    "path": "elixir/lib/symphony_elixir/specs_check.ex",
    "content": "defmodule SymphonyElixir.SpecsCheck do\n  @moduledoc false\n\n  @type finding :: %{\n          file: String.t(),\n          module: String.t(),\n          name: atom(),\n          arity: non_neg_integer(),\n          line: pos_integer()\n        }\n\n  @spec missing_public_specs([Path.t()], keyword()) :: [finding()]\n  def missing_public_specs(paths, opts \\\\ []) do\n    exemptions =\n      opts\n      |> Keyword.get(:exemptions, [])\n      |> MapSet.new()\n\n    paths\n    |> Enum.flat_map(&collect_elixir_files/1)\n    |> Enum.flat_map(&file_findings(&1, exemptions))\n    |> Enum.sort_by(&{&1.file, &1.line, &1.name, &1.arity})\n  end\n\n  @spec finding_identifier(finding()) :: String.t()\n  def finding_identifier(%{module: module, name: name, arity: arity}) do\n    \"#{module}.#{name}/#{arity}\"\n  end\n\n  defp collect_elixir_files(path) do\n    cond do\n      File.regular?(path) and String.ends_with?(path, \".ex\") ->\n        [path]\n\n      File.dir?(path) ->\n        Path.wildcard(Path.join(path, \"**/*.ex\"))\n\n      true ->\n        []\n    end\n  end\n\n  defp file_findings(file, exemptions) do\n    with {:ok, source} <- File.read(file),\n         {:ok, ast} <- Code.string_to_quoted(source, columns: true, file: file) do\n      ast\n      |> module_nodes()\n      |> Enum.flat_map(fn {module_name, body} ->\n        find_missing_specs(body, module_name, file, exemptions)\n      end)\n    else\n      {:error, {line, error, token}} ->\n        Mix.raise(\"Unable to parse #{file}:#{line} #{error} #{inspect(token)}\")\n\n      {:error, reason} ->\n        Mix.raise(\"Unable to read #{file}: #{inspect(reason)}\")\n    end\n  end\n\n  defp module_nodes(ast) do\n    {_ast, modules} =\n      Macro.prewalk(ast, [], fn\n        {:defmodule, _meta, [module_ast, [do: body]]} = node, acc ->\n          {node, [{Macro.to_string(module_ast), body} | acc]}\n\n        node, acc ->\n          {node, acc}\n      end)\n\n    Enum.reverse(modules)\n  end\n\n  defp find_missing_specs(body, module_name, file, exemptions) do\n    body\n    |> normalize_block()\n    |> Enum.reduce(initial_state(), fn form, state ->\n      consume_form(form, state, module_name, file, exemptions)\n    end)\n    |> Map.fetch!(:findings)\n  end\n\n  defp initial_state do\n    %{pending_specs: MapSet.new(), pending_impl: false, seen_defs: MapSet.new(), findings: []}\n  end\n\n  defp consume_form({:@, _, [{:spec, _, spec_nodes}]}, state, _module_name, _file, _exemptions) do\n    ids =\n      spec_nodes\n      |> Enum.flat_map(&extract_spec_identifiers/1)\n      |> MapSet.new()\n\n    %{state | pending_specs: MapSet.union(state.pending_specs, ids)}\n  end\n\n  defp consume_form({:@, _, [{:impl, _, _}]}, state, _module_name, _file, _exemptions) do\n    %{state | pending_impl: true}\n  end\n\n  defp consume_form({:@, _, _}, state, _module_name, _file, _exemptions), do: state\n\n  defp consume_form({:def, meta, [head_ast, _]} = _form, state, module_name, file, exemptions) do\n    {name, arity} = def_head_to_identifier(head_ast)\n\n    id = {name, arity}\n\n    if MapSet.member?(state.seen_defs, id) do\n      %{state | pending_specs: MapSet.new(), pending_impl: false}\n    else\n      finding = %{\n        file: file,\n        module: module_name,\n        name: name,\n        arity: arity,\n        line: Keyword.get(meta, :line, 1)\n      }\n\n      next_state = %{\n        state\n        | pending_specs: MapSet.new(),\n          pending_impl: false,\n          seen_defs: MapSet.put(state.seen_defs, id)\n      }\n\n      if compliant?(finding, state, exemptions) do\n        next_state\n      else\n        %{next_state | findings: [finding | next_state.findings]}\n      end\n    end\n  end\n\n  defp consume_form({:defp, _, _}, state, _module_name, _file, _exemptions) do\n    %{state | pending_specs: MapSet.new(), pending_impl: false}\n  end\n\n  defp consume_form(_form, state, _module_name, _file, _exemptions) do\n    %{state | pending_specs: MapSet.new(), pending_impl: false}\n  end\n\n  defp compliant?(finding, state, exemptions) do\n    id = {finding.name, finding.arity}\n\n    MapSet.member?(state.pending_specs, id) or\n      state.pending_impl or\n      MapSet.member?(exemptions, finding_identifier(finding))\n  end\n\n  defp normalize_block({:__block__, _, forms}), do: forms\n  defp normalize_block(form), do: [form]\n\n  defp extract_spec_identifiers({:\"::\", _, [head, _return_type]}) do\n    case spec_head_to_identifier(head) do\n      nil -> []\n      id -> [id]\n    end\n  end\n\n  defp extract_spec_identifiers({:when, _, [{:\"::\", _, [head, _return_type]} | _guards]}) do\n    case spec_head_to_identifier(head) do\n      nil -> []\n      id -> [id]\n    end\n  end\n\n  defp extract_spec_identifiers(_), do: []\n\n  defp spec_head_to_identifier({:when, _, [inner | _guards]}), do: spec_head_to_identifier(inner)\n  defp spec_head_to_identifier({name, _, args}) when is_atom(name) and is_list(args), do: {name, length(args)}\n  defp spec_head_to_identifier({name, _, nil}) when is_atom(name), do: {name, 0}\n  defp spec_head_to_identifier(_), do: nil\n\n  defp def_head_to_identifier({:when, _, [head | _guards]}), do: def_head_to_identifier(head)\n  defp def_head_to_identifier({name, _, args}) when is_atom(name) and is_list(args), do: {name, length(args)}\n  defp def_head_to_identifier({name, _, nil}) when is_atom(name), do: {name, 0}\nend\n"
  },
  {
    "path": "elixir/lib/symphony_elixir/ssh.ex",
    "content": "defmodule SymphonyElixir.SSH do\n  @moduledoc false\n\n  @spec run(String.t(), String.t(), keyword()) :: {:ok, {String.t(), non_neg_integer()}} | {:error, term()}\n  def run(host, command, opts \\\\ []) when is_binary(host) and is_binary(command) do\n    with {:ok, executable} <- ssh_executable() do\n      {:ok, System.cmd(executable, ssh_args(host, command), opts)}\n    end\n  end\n\n  @spec start_port(String.t(), String.t(), keyword()) :: {:ok, port()} | {:error, term()}\n  def start_port(host, command, opts \\\\ []) when is_binary(host) and is_binary(command) do\n    with {:ok, executable} <- ssh_executable() do\n      line_bytes = Keyword.get(opts, :line)\n\n      port_opts =\n        [\n          :binary,\n          :exit_status,\n          :stderr_to_stdout,\n          args: Enum.map(ssh_args(host, command), &String.to_charlist/1)\n        ]\n        |> maybe_put_line_option(line_bytes)\n\n      {:ok, Port.open({:spawn_executable, String.to_charlist(executable)}, port_opts)}\n    end\n  end\n\n  @spec remote_shell_command(String.t()) :: String.t()\n  def remote_shell_command(command) when is_binary(command) do\n    \"bash -lc \" <> shell_escape(command)\n  end\n\n  defp ssh_executable do\n    case System.find_executable(\"ssh\") do\n      nil -> {:error, :ssh_not_found}\n      executable -> {:ok, executable}\n    end\n  end\n\n  defp ssh_args(host, command) do\n    %{destination: destination, port: port} = parse_target(host)\n\n    []\n    |> maybe_put_config()\n    |> Kernel.++([\"-T\"])\n    |> maybe_put_port(port)\n    |> Kernel.++([destination, remote_shell_command(command)])\n  end\n\n  defp maybe_put_line_option(port_opts, nil), do: port_opts\n  defp maybe_put_line_option(port_opts, line_bytes), do: Keyword.put(port_opts, :line, line_bytes)\n\n  defp maybe_put_config(args) do\n    case System.get_env(\"SYMPHONY_SSH_CONFIG\") do\n      config_path when is_binary(config_path) and config_path != \"\" ->\n        args ++ [\"-F\", config_path]\n\n      _ ->\n        args\n    end\n  end\n\n  defp maybe_put_port(args, nil), do: args\n  defp maybe_put_port(args, port), do: args ++ [\"-p\", port]\n\n  defp parse_target(target) when is_binary(target) do\n    trimmed_target = String.trim(target)\n\n    # OpenSSH does not interpret bare \"host:port\" as \"host + port\"; it treats the\n    # whole value as a hostname and leaves the port at 22. We split that shorthand\n    # here so worker config can use \"localhost:2222\" without requiring ssh:// URIs.\n    case Regex.run(~r/^(.*):(\\d+)$/, trimmed_target, capture: :all_but_first) do\n      [destination, port] ->\n        if valid_port_destination?(destination) do\n          %{destination: destination, port: port}\n        else\n          %{destination: trimmed_target, port: nil}\n        end\n\n      _ ->\n        %{destination: trimmed_target, port: nil}\n    end\n  end\n\n  defp valid_port_destination?(destination) when is_binary(destination) do\n    destination != \"\" and\n      (not String.contains?(destination, \":\") or bracketed_host?(destination))\n  end\n\n  defp bracketed_host?(destination) when is_binary(destination) do\n    # IPv6 literals contain \":\" already, so we only accept additional \":port\"\n    # parsing when the host is explicitly bracketed, e.g. \"[::1]:2222\".\n    String.contains?(destination, \"[\") and String.contains?(destination, \"]\")\n  end\n\n  defp shell_escape(value) when is_binary(value) do\n    \"'\" <> String.replace(value, \"'\", \"'\\\"'\\\"'\") <> \"'\"\n  end\nend\n"
  },
  {
    "path": "elixir/lib/symphony_elixir/status_dashboard.ex",
    "content": "defmodule SymphonyElixir.StatusDashboard do\n  @moduledoc \"\"\"\n  Renders a status snapshot for orchestrator and worker activity as a terminal UI.\n  \"\"\"\n\n  use GenServer\n  require Logger\n\n  alias SymphonyElixir.{Config, HttpServer}\n  alias SymphonyElixir.Orchestrator\n  alias SymphonyElixirWeb.ObservabilityPubSub\n\n  @minimum_idle_rerender_ms 1_000\n  @throughput_window_ms 5_000\n  @throughput_graph_window_ms 10 * 60 * 1000\n  @throughput_graph_columns 24\n  @sparkline_blocks [\"▁\", \"▂\", \"▃\", \"▄\", \"▅\", \"▆\", \"▇\", \"█\"]\n  @running_id_width 8\n  @running_stage_width 14\n  @running_pid_width 8\n  @running_age_width 12\n  @running_tokens_width 10\n  @running_session_width 14\n  @running_event_default_width 44\n  @running_event_min_width 12\n  @running_row_chrome_width 10\n  @default_terminal_columns 115\n\n  @ansi_reset IO.ANSI.reset()\n  @ansi_bold IO.ANSI.bright()\n  @ansi_blue IO.ANSI.blue()\n  @ansi_cyan IO.ANSI.cyan()\n  @ansi_dim IO.ANSI.faint()\n  @ansi_green IO.ANSI.green()\n  @ansi_red IO.ANSI.red()\n  @ansi_orange IO.ANSI.yellow()\n  @ansi_yellow IO.ANSI.yellow()\n  @ansi_magenta IO.ANSI.magenta()\n  @ansi_gray IO.ANSI.light_black()\n\n  defstruct [\n    :refresh_ms,\n    :enabled,\n    :render_interval_ms,\n    :refresh_ms_override,\n    :enabled_override,\n    :render_interval_ms_override,\n    :render_fun,\n    :token_samples,\n    :last_tps_second,\n    :last_tps_value,\n    :last_rendered_content,\n    :last_rendered_at_ms,\n    :pending_content,\n    :flush_timer_ref,\n    :last_snapshot_fingerprint\n  ]\n\n  @type t :: %__MODULE__{\n          refresh_ms: pos_integer(),\n          enabled: boolean(),\n          render_interval_ms: pos_integer(),\n          refresh_ms_override: pos_integer() | nil,\n          enabled_override: boolean() | nil,\n          render_interval_ms_override: pos_integer() | nil,\n          render_fun: (String.t() -> term()),\n          token_samples: [{integer(), integer()}],\n          last_tps_second: integer() | nil,\n          last_tps_value: float() | nil,\n          last_rendered_content: String.t() | nil,\n          last_rendered_at_ms: integer() | nil,\n          pending_content: String.t() | nil,\n          flush_timer_ref: reference() | nil,\n          last_snapshot_fingerprint: term() | nil\n        }\n\n  @spec start_link(keyword()) :: GenServer.on_start()\n  def start_link(opts \\\\ []) do\n    name = Keyword.get(opts, :name, __MODULE__)\n    GenServer.start_link(__MODULE__, opts, name: name)\n  end\n\n  @spec notify_update(GenServer.name()) :: :ok\n  def notify_update(server \\\\ __MODULE__) do\n    ObservabilityPubSub.broadcast_update()\n\n    case GenServer.whereis(server) do\n      pid when is_pid(pid) ->\n        send(pid, :refresh)\n        :ok\n\n      _ ->\n        :ok\n    end\n  end\n\n  @spec init(keyword()) :: {:ok, t()}\n  def init(opts) do\n    refresh_ms_override = keyword_override(opts, :refresh_ms)\n    enabled_override = keyword_override(opts, :enabled)\n    render_interval_ms_override = keyword_override(opts, :render_interval_ms)\n    observability = Config.settings!().observability\n    refresh_ms = refresh_ms_override || observability.refresh_ms\n    render_interval_ms = render_interval_ms_override || observability.render_interval_ms\n    render_fun = Keyword.get(opts, :render_fun, &render_to_terminal/1)\n    enabled = resolve_override(enabled_override, observability.dashboard_enabled and dashboard_enabled?())\n    schedule_tick(refresh_ms, enabled)\n\n    {:ok,\n     %__MODULE__{\n       refresh_ms: refresh_ms,\n       enabled: enabled,\n       render_interval_ms: render_interval_ms,\n       refresh_ms_override: refresh_ms_override,\n       enabled_override: enabled_override,\n       render_interval_ms_override: render_interval_ms_override,\n       render_fun: render_fun,\n       token_samples: [],\n       last_tps_second: nil,\n       last_tps_value: nil,\n       last_rendered_content: nil,\n       last_rendered_at_ms: nil,\n       pending_content: nil,\n       flush_timer_ref: nil,\n       last_snapshot_fingerprint: nil\n     }}\n  end\n\n  @spec render_offline_status() :: :ok\n  def render_offline_status do\n    content =\n      [\n        colorize(\"╭─ SYMPHONY STATUS\", @ansi_bold),\n        colorize(\"│ app_status=offline\", @ansi_red),\n        closing_border()\n      ]\n      |> Enum.join(\"\\n\")\n\n    render_to_terminal(content)\n    :ok\n  rescue\n    error in [ArgumentError, RuntimeError] ->\n      Logger.warning(\"Failed rendering offline status: #{Exception.message(error)}\")\n      :ok\n  end\n\n  @spec handle_info(term(), t()) :: {:noreply, t()}\n  def handle_info(:tick, %{enabled: true} = state) do\n    state = refresh_runtime_config(state)\n    state = maybe_render(state)\n    schedule_tick(state.refresh_ms, true)\n    {:noreply, state}\n  end\n\n  def handle_info(:refresh, %{enabled: true} = state), do: {:noreply, maybe_render(refresh_runtime_config(state))}\n  def handle_info(:refresh, state), do: {:noreply, state}\n\n  def handle_info({:flush_render, timer_ref}, %{enabled: true, flush_timer_ref: timer_ref} = state) do\n    now_ms = System.monotonic_time(:millisecond)\n\n    state =\n      case state.pending_content do\n        nil ->\n          %{state | flush_timer_ref: nil}\n\n        content ->\n          state\n          |> Map.put(:flush_timer_ref, nil)\n          |> Map.put(:pending_content, nil)\n          |> render_content(content, now_ms)\n      end\n\n    {:noreply, state}\n  end\n\n  def handle_info({:flush_render, _timer_ref}, state), do: {:noreply, state}\n  def handle_info(:tick, state), do: {:noreply, state}\n\n  defp refresh_runtime_config(%__MODULE__{} = state) do\n    observability = Config.settings!().observability\n\n    %{\n      state\n      | enabled: resolve_override(state.enabled_override, observability.dashboard_enabled and dashboard_enabled?()),\n        refresh_ms: state.refresh_ms_override || observability.refresh_ms,\n        render_interval_ms: state.render_interval_ms_override || observability.render_interval_ms\n    }\n  end\n\n  defp schedule_tick(refresh_ms, true), do: Process.send_after(self(), :tick, refresh_ms)\n  defp schedule_tick(_refresh_ms, false), do: :ok\n\n  defp maybe_render(state) do\n    now_ms = System.monotonic_time(:millisecond)\n    {snapshot_data, token_samples} = snapshot_with_samples(state.token_samples, now_ms)\n    state = Map.put(state, :token_samples, token_samples)\n\n    current_tokens = snapshot_total_tokens(snapshot_data)\n\n    {tps_second, tps} =\n      throttled_tps(\n        state.last_tps_second,\n        state.last_tps_value,\n        now_ms,\n        token_samples,\n        current_tokens\n      )\n\n    state =\n      state\n      |> Map.put(:last_tps_second, tps_second)\n      |> Map.put(:last_tps_value, tps)\n\n    if snapshot_data != state.last_snapshot_fingerprint or periodic_rerender_due?(state, now_ms) do\n      content =\n        format_snapshot_content(\n          snapshot_data,\n          tps\n        )\n\n      state\n      |> maybe_update_snapshot_fingerprint(snapshot_data)\n      |> maybe_enqueue_render(content, now_ms)\n    else\n      state\n    end\n  rescue\n    error in [ArgumentError, RuntimeError] ->\n      Logger.warning(\"Failed rendering status dashboard: #{Exception.message(error)}\")\n      state\n  end\n\n  defp maybe_enqueue_render(state, content, now_ms) do\n    cond do\n      content == state.last_rendered_content ->\n        state\n\n      render_now?(state, now_ms) ->\n        render_content(state, content, now_ms)\n\n      true ->\n        schedule_flush_render(%{state | pending_content: content}, now_ms)\n    end\n  end\n\n  defp maybe_update_snapshot_fingerprint(state, snapshot_data) do\n    if snapshot_data == state.last_snapshot_fingerprint do\n      state\n    else\n      Map.put(state, :last_snapshot_fingerprint, snapshot_data)\n    end\n  end\n\n  defp periodic_rerender_due?(%{last_rendered_at_ms: nil}, _now_ms), do: true\n\n  defp periodic_rerender_due?(%{last_rendered_at_ms: last_rendered_at_ms}, now_ms)\n       when is_integer(last_rendered_at_ms) do\n    now_ms - last_rendered_at_ms >= @minimum_idle_rerender_ms\n  end\n\n  defp periodic_rerender_due?(_state, _now_ms), do: false\n\n  defp render_now?(%{last_rendered_at_ms: nil, flush_timer_ref: nil}, _now_ms), do: true\n\n  defp render_now?(%{last_rendered_at_ms: last_rendered_at_ms, render_interval_ms: render_interval_ms}, now_ms)\n       when is_integer(last_rendered_at_ms) and is_integer(render_interval_ms) do\n    now_ms - last_rendered_at_ms >= render_interval_ms\n  end\n\n  defp render_now?(_state, _now_ms), do: false\n\n  defp schedule_flush_render(%{flush_timer_ref: timer_ref} = state, _now_ms) when is_reference(timer_ref),\n    do: state\n\n  defp schedule_flush_render(state, now_ms) do\n    delay_ms = flush_delay_ms(state, now_ms)\n    timer_ref = make_ref()\n    Process.send_after(self(), {:flush_render, timer_ref}, delay_ms)\n    %{state | flush_timer_ref: timer_ref}\n  end\n\n  defp flush_delay_ms(%{last_rendered_at_ms: nil}, _now_ms), do: 1\n\n  defp flush_delay_ms(\n         %{last_rendered_at_ms: last_rendered_at_ms, render_interval_ms: render_interval_ms},\n         now_ms\n       ) do\n    remaining = render_interval_ms - (now_ms - last_rendered_at_ms)\n    max(1, remaining)\n  end\n\n  defp render_content(state, content, now_ms) do\n    state.render_fun.(content)\n\n    %{\n      state\n      | last_rendered_content: content,\n        last_rendered_at_ms: now_ms,\n        pending_content: nil,\n        flush_timer_ref: nil\n    }\n  rescue\n    error in [ArgumentError, RuntimeError] ->\n      Logger.warning(\"Failed rendering terminal dashboard frame: #{Exception.message(error)}\")\n      %{state | pending_content: nil, flush_timer_ref: nil}\n  end\n\n  defp snapshot_with_samples(token_samples, now_ms) do\n    case snapshot_payload() do\n      {:ok, %{running: running, retrying: retrying, codex_totals: codex_totals} = snapshot} ->\n        total_tokens = Map.get(codex_totals, :total_tokens, 0)\n\n        {\n          {:ok,\n           %{\n             running: running,\n             retrying: retrying,\n             codex_totals: codex_totals,\n             rate_limits: Map.get(snapshot, :rate_limits),\n             polling: Map.get(snapshot, :polling)\n           }},\n          update_token_samples(token_samples, now_ms, total_tokens)\n        }\n\n      :error ->\n        {\n          :error,\n          prune_samples(token_samples, now_ms)\n        }\n    end\n  end\n\n  defp format_snapshot_content(snapshot_data, tps, terminal_columns_override \\\\ nil) do\n    case snapshot_data do\n      {:ok, %{running: running, retrying: retrying, codex_totals: codex_totals} = snapshot} ->\n        rate_limits = Map.get(snapshot, :rate_limits)\n        project_link_lines = format_project_link_lines()\n        project_refresh_line = format_project_refresh_line(Map.get(snapshot, :polling))\n        codex_input_tokens = Map.get(codex_totals, :input_tokens, 0)\n        codex_output_tokens = Map.get(codex_totals, :output_tokens, 0)\n        codex_total_tokens = Map.get(codex_totals, :total_tokens, 0)\n        codex_seconds_running = Map.get(codex_totals, :seconds_running, 0)\n        agent_count = length(running)\n        max_agents = Config.settings!().agent.max_concurrent_agents\n        running_event_width = running_event_width(terminal_columns_override)\n        running_rows = format_running_rows(running, running_event_width)\n        running_to_backoff_spacer = if(running == [], do: [], else: [\"│\"])\n        backoff_rows = format_retry_rows(retrying)\n\n        ([\n           colorize(\"╭─ SYMPHONY STATUS\", @ansi_bold),\n           colorize(\"│ Agents: \", @ansi_bold) <>\n             colorize(\"#{agent_count}\", @ansi_green) <>\n             colorize(\"/\", @ansi_gray) <>\n             colorize(\"#{max_agents}\", @ansi_gray),\n           colorize(\"│ Throughput: \", @ansi_bold) <> colorize(\"#{format_tps(tps)} tps\", @ansi_cyan),\n           colorize(\"│ Runtime: \", @ansi_bold) <>\n             colorize(format_runtime_seconds(codex_seconds_running), @ansi_magenta),\n           colorize(\"│ Tokens: \", @ansi_bold) <>\n             colorize(\"in #{format_count(codex_input_tokens)}\", @ansi_yellow) <>\n             colorize(\" | \", @ansi_gray) <>\n             colorize(\"out #{format_count(codex_output_tokens)}\", @ansi_yellow) <>\n             colorize(\" | \", @ansi_gray) <>\n             colorize(\"total #{format_count(codex_total_tokens)}\", @ansi_yellow),\n           colorize(\"│ Rate Limits: \", @ansi_bold) <> format_rate_limits(rate_limits),\n           project_link_lines,\n           project_refresh_line,\n           colorize(\"├─ Running\", @ansi_bold),\n           \"│\",\n           running_table_header_row(running_event_width),\n           running_table_separator_row(running_event_width)\n         ] ++\n           running_rows ++\n           running_to_backoff_spacer ++\n           [colorize(\"├─ Backoff queue\", @ansi_bold), \"│\"] ++\n           backoff_rows ++\n           [closing_border()])\n        |> List.flatten()\n        |> Enum.join(\"\\n\")\n\n      :error ->\n        [\n          colorize(\"╭─ SYMPHONY STATUS\", @ansi_bold),\n          colorize(\"│ Orchestrator snapshot unavailable\", @ansi_red),\n          colorize(\"│ Throughput: \", @ansi_bold) <> colorize(\"#{format_tps(tps)} tps\", @ansi_cyan),\n          format_project_link_lines(),\n          format_project_refresh_line(nil),\n          closing_border()\n        ]\n        |> List.flatten()\n        |> Enum.join(\"\\n\")\n    end\n  end\n\n  defp format_project_link_lines do\n    project_part =\n      case Config.settings!().tracker.project_slug do\n        project_slug when is_binary(project_slug) and project_slug != \"\" ->\n          colorize(linear_project_url(project_slug), @ansi_cyan)\n\n        _ ->\n          colorize(\"n/a\", @ansi_gray)\n      end\n\n    project_line = colorize(\"│ Project: \", @ansi_bold) <> project_part\n\n    case dashboard_url() do\n      url when is_binary(url) ->\n        [project_line, colorize(\"│ Dashboard: \", @ansi_bold) <> colorize(url, @ansi_cyan)]\n\n      _ ->\n        [project_line]\n    end\n  end\n\n  defp format_project_refresh_line(%{checking?: true}) do\n    colorize(\"│ Next refresh: \", @ansi_bold) <> colorize(\"checking now…\", @ansi_cyan)\n  end\n\n  defp format_project_refresh_line(%{next_poll_in_ms: due_in_ms}) when is_integer(due_in_ms) do\n    due_in_ms = max(due_in_ms, 0)\n    seconds = div(due_in_ms + 999, 1000)\n    colorize(\"│ Next refresh: \", @ansi_bold) <> colorize(\"#{seconds}s\", @ansi_cyan)\n  end\n\n  defp format_project_refresh_line(_) do\n    colorize(\"│ Next refresh: \", @ansi_bold) <> colorize(\"n/a\", @ansi_gray)\n  end\n\n  defp linear_project_url(project_slug), do: \"https://linear.app/project/#{project_slug}/issues\"\n\n  defp dashboard_url do\n    dashboard_url(Config.settings!().server.host, Config.server_port(), HttpServer.bound_port())\n  end\n\n  defp dashboard_url(_host, nil, _bound_port), do: nil\n\n  defp dashboard_url(host, configured_port, bound_port) do\n    port = bound_port || configured_port\n\n    if is_integer(port) and port > 0 do\n      \"http://#{dashboard_url_host(host)}:#{port}/\"\n    else\n      nil\n    end\n  end\n\n  defp dashboard_url_host(host) when host in [\"0.0.0.0\", \"::\", \"[::]\", \"\"], do: \"127.0.0.1\"\n\n  defp dashboard_url_host(host) when is_binary(host) do\n    trimmed_host = String.trim(host)\n\n    cond do\n      trimmed_host in [\"0.0.0.0\", \"::\", \"[::]\", \"\"] ->\n        \"127.0.0.1\"\n\n      String.starts_with?(trimmed_host, \"[\") and String.ends_with?(trimmed_host, \"]\") ->\n        trimmed_host\n\n      String.contains?(trimmed_host, \":\") ->\n        \"[#{trimmed_host}]\"\n\n      true ->\n        trimmed_host\n    end\n  end\n\n  defp render_to_terminal(content) do\n    IO.write([\n      IO.ANSI.home(),\n      IO.ANSI.clear(),\n      normalize_status_lines(content),\n      \"\\n\"\n    ])\n  end\n\n  defp update_token_samples(samples, now_ms, total_tokens) do\n    prune_graph_samples([{now_ms, total_tokens} | samples], now_ms)\n  end\n\n  defp prune_samples(samples, now_ms) do\n    min_timestamp = now_ms - @throughput_window_ms\n    Enum.filter(samples, fn {timestamp, _} -> timestamp >= min_timestamp end)\n  end\n\n  defp prune_graph_samples(samples, now_ms) do\n    min_timestamp = now_ms - max(@throughput_window_ms, @throughput_graph_window_ms)\n    Enum.filter(samples, fn {timestamp, _} -> timestamp >= min_timestamp end)\n  end\n\n  @doc false\n  @spec rolling_tps([{integer(), integer()}], integer(), integer()) :: float()\n  def rolling_tps(samples, now_ms, current_tokens) do\n    samples = [{now_ms, current_tokens} | samples]\n    samples = prune_samples(samples, now_ms)\n\n    case samples do\n      [] ->\n        0.0\n\n      [_one] ->\n        0.0\n\n      _ ->\n        first = List.last(samples)\n        {start_ms, start_tokens} = first\n        elapsed_ms = now_ms - start_ms\n        delta_tokens = max(0, current_tokens - start_tokens)\n\n        if elapsed_ms <= 0 do\n          0.0\n        else\n          delta_tokens / (elapsed_ms / 1000.0)\n        end\n    end\n  end\n\n  @doc false\n  @spec throttled_tps(integer() | nil, float() | nil, integer(), [{integer(), integer()}], integer()) ::\n          {integer(), float()}\n  def throttled_tps(last_second, last_value, now_ms, token_samples, current_tokens) do\n    second = div(now_ms, 1000)\n\n    if is_integer(last_second) and last_second == second and is_number(last_value) do\n      {second, last_value}\n    else\n      {second, rolling_tps(token_samples, now_ms, current_tokens)}\n    end\n  end\n\n  @doc false\n  @spec format_timestamp_for_test(DateTime.t()) :: String.t()\n  def format_timestamp_for_test(%DateTime{} = datetime), do: format_timestamp(datetime)\n\n  @doc false\n  @spec format_snapshot_content_for_test(term(), number()) :: String.t()\n  def format_snapshot_content_for_test(snapshot_data, tps), do: format_snapshot_content(snapshot_data, tps)\n\n  @doc false\n  @spec format_snapshot_content_for_test(term(), number(), integer() | nil) :: String.t()\n  def format_snapshot_content_for_test(snapshot_data, tps, terminal_columns),\n    do: format_snapshot_content(snapshot_data, tps, terminal_columns)\n\n  @doc false\n  @spec dashboard_url_for_test(String.t(), non_neg_integer() | nil, non_neg_integer() | nil) ::\n          String.t() | nil\n  def dashboard_url_for_test(host, configured_port, bound_port),\n    do: dashboard_url(host, configured_port, bound_port)\n\n  defp snapshot_payload do\n    if Process.whereis(Orchestrator) do\n      case Orchestrator.snapshot() do\n        %{\n          running: running,\n          retrying: retrying,\n          codex_totals: codex_totals\n        } = snapshot\n        when is_list(running) and is_list(retrying) ->\n          {:ok,\n           %{\n             running: running,\n             retrying: retrying,\n             codex_totals: codex_totals,\n             rate_limits: Map.get(snapshot, :rate_limits),\n             polling: Map.get(snapshot, :polling)\n           }}\n\n        _ ->\n          :error\n      end\n    else\n      :error\n    end\n  end\n\n  defp format_running_rows(running, running_event_width) do\n    if running == [] do\n      [\n        \"│  \" <> colorize(\"No active agents\", @ansi_gray),\n        \"│\"\n      ]\n    else\n      running\n      |> Enum.sort_by(& &1.identifier)\n      |> Enum.map(&format_running_summary(&1, running_event_width))\n    end\n  end\n\n  # credo:disable-for-next-line\n  defp format_running_summary(running_entry, running_event_width) do\n    issue = format_cell(running_entry.identifier || \"unknown\", @running_id_width)\n    state = running_entry.state || \"unknown\"\n    state_display = format_cell(to_string(state), @running_stage_width)\n    session = running_entry.session_id |> compact_session_id() |> format_cell(@running_session_width)\n    pid = format_cell(running_entry.codex_app_server_pid || \"n/a\", @running_pid_width)\n    total_tokens = running_entry.codex_total_tokens || 0\n    runtime_seconds = running_entry.runtime_seconds || 0\n    turn_count = Map.get(running_entry, :turn_count, 0)\n    age = format_cell(format_runtime_and_turns(runtime_seconds, turn_count), @running_age_width)\n    event = running_entry.last_codex_event || \"none\"\n    event_label = format_cell(summarize_message(running_entry.last_codex_message), running_event_width)\n\n    tokens = format_count(total_tokens) |> format_cell(@running_tokens_width, :right)\n\n    status_color =\n      case event do\n        :none -> @ansi_red\n        \"codex/event/token_count\" -> @ansi_yellow\n        \"codex/event/task_started\" -> @ansi_green\n        \"turn_completed\" -> @ansi_magenta\n        _ -> @ansi_blue\n      end\n\n    [\n      \"│ \",\n      status_dot(status_color),\n      \" \",\n      colorize(issue, @ansi_cyan),\n      \" \",\n      colorize(state_display, status_color),\n      \" \",\n      colorize(pid, @ansi_yellow),\n      \" \",\n      colorize(age, @ansi_magenta),\n      \" \",\n      colorize(tokens, @ansi_yellow),\n      \" \",\n      colorize(session, @ansi_cyan),\n      \" \",\n      colorize(event_label, status_color)\n    ]\n    |> Enum.join(\"\")\n  end\n\n  @doc false\n  @spec format_running_summary_for_test(map(), integer() | nil) :: String.t()\n  def format_running_summary_for_test(running_entry, terminal_columns \\\\ nil),\n    do: format_running_summary(running_entry, running_event_width(terminal_columns))\n\n  @doc false\n  @spec format_tps_for_test(number()) :: String.t()\n  def format_tps_for_test(value), do: format_tps(value)\n\n  @doc false\n  @spec tps_graph_for_test([{integer(), integer()}], integer(), integer()) :: String.t()\n  def tps_graph_for_test(samples, now_ms, current_tokens), do: tps_graph(samples, now_ms, current_tokens)\n\n  defp format_retry_rows(retrying) do\n    if retrying == [] do\n      [\"│  \" <> colorize(\"No queued retries\", @ansi_gray)]\n    else\n      retrying\n      |> Enum.sort_by(& &1.due_in_ms)\n      |> Enum.map_join(\", \", &format_retry_summary/1)\n      |> String.split(\", \")\n    end\n  end\n\n  defp format_retry_summary(retry_entry) do\n    issue_id = retry_entry.issue_id || \"unknown\"\n    identifier = retry_entry.identifier || issue_id\n    attempt = retry_entry.attempt || 0\n    due_in_ms = retry_entry.due_in_ms || 0\n    error = format_retry_error(retry_entry.error)\n\n    \"│  #{colorize(\"↻\", @ansi_orange)} \" <>\n      colorize(\"#{identifier}\", @ansi_red) <>\n      \" \" <>\n      colorize(\"attempt=#{attempt}\", @ansi_yellow) <>\n      colorize(\" in \", @ansi_dim) <>\n      colorize(next_in_words(due_in_ms), @ansi_cyan) <>\n      error\n  end\n\n  defp next_in_words(due_in_ms) when is_integer(due_in_ms) do\n    secs = div(due_in_ms, 1000)\n    millis = rem(due_in_ms, 1000)\n    \"#{secs}.#{String.pad_leading(to_string(millis), 3, \"0\")}s\"\n  end\n\n  defp next_in_words(_), do: \"n/a\"\n\n  defp format_retry_error(error) when is_binary(error) do\n    sanitized =\n      error\n      |> String.replace(\"\\\\r\\\\n\", \" \")\n      |> String.replace(\"\\\\r\", \" \")\n      |> String.replace(\"\\\\n\", \" \")\n      |> String.replace(\"\\r\\n\", \" \")\n      |> String.replace(\"\\r\", \" \")\n      |> String.replace(\"\\n\", \" \")\n      |> String.replace(~r/\\s+/, \" \")\n      |> String.trim()\n\n    if sanitized == \"\" do\n      \"\"\n    else\n      \" \" <> colorize(\"error=#{truncate(sanitized, 96)}\", @ansi_dim)\n    end\n  end\n\n  defp format_retry_error(_), do: \"\"\n\n  defp format_runtime_seconds(seconds) when is_integer(seconds) do\n    mins = div(seconds, 60)\n    secs = rem(seconds, 60)\n    \"#{mins}m #{secs}s\"\n  end\n\n  defp format_runtime_seconds(seconds) when is_binary(seconds), do: seconds\n  defp format_runtime_seconds(_), do: \"0m 0s\"\n\n  defp format_runtime_and_turns(seconds, turn_count) when is_integer(turn_count) and turn_count > 0 do\n    \"#{format_runtime_seconds(seconds)} / #{turn_count}\"\n  end\n\n  defp format_runtime_and_turns(seconds, _turn_count), do: format_runtime_seconds(seconds)\n\n  defp format_count(nil), do: \"0\"\n\n  defp format_count(value) when is_integer(value) do\n    value\n    |> Integer.to_string()\n    |> group_thousands()\n  end\n\n  defp format_count(value) when is_binary(value) do\n    value\n    |> String.trim()\n    |> Integer.parse()\n    |> case do\n      {number, \"\"} -> group_thousands(Integer.to_string(number))\n      _ -> value\n    end\n  end\n\n  defp format_count(value), do: to_string(value)\n\n  defp running_table_header_row(running_event_width) do\n    header =\n      [\n        format_cell(\"ID\", @running_id_width),\n        format_cell(\"STAGE\", @running_stage_width),\n        format_cell(\"PID\", @running_pid_width),\n        format_cell(\"AGE / TURN\", @running_age_width),\n        format_cell(\"TOKENS\", @running_tokens_width),\n        format_cell(\"SESSION\", @running_session_width),\n        format_cell(\"EVENT\", running_event_width)\n      ]\n      |> Enum.join(\" \")\n\n    \"│   \" <> colorize(header, @ansi_gray)\n  end\n\n  defp running_table_separator_row(running_event_width) do\n    separator_width =\n      @running_id_width +\n        @running_stage_width +\n        @running_pid_width +\n        @running_age_width +\n        @running_tokens_width +\n        @running_session_width +\n        running_event_width + 6\n\n    \"│   \" <> colorize(String.duplicate(\"─\", separator_width), @ansi_gray)\n  end\n\n  defp running_event_width(terminal_columns) do\n    terminal_columns = terminal_columns || terminal_columns()\n\n    max(\n      @running_event_min_width,\n      terminal_columns - fixed_running_width() - @running_row_chrome_width\n    )\n  end\n\n  defp fixed_running_width do\n    @running_id_width +\n      @running_stage_width +\n      @running_pid_width +\n      @running_age_width +\n      @running_tokens_width +\n      @running_session_width\n  end\n\n  defp terminal_columns do\n    case :io.columns() do\n      {:ok, columns} when is_integer(columns) and columns > 0 ->\n        columns\n\n      _ ->\n        terminal_columns_from_env()\n    end\n  end\n\n  defp terminal_columns_from_env do\n    case System.get_env(\"COLUMNS\") do\n      nil ->\n        fixed_running_width() + @running_row_chrome_width + @running_event_default_width\n\n      value ->\n        case Integer.parse(String.trim(value)) do\n          {columns, \"\"} when columns > 0 -> columns\n          _ -> @default_terminal_columns\n        end\n    end\n  end\n\n  defp format_cell(value, width, align \\\\ :left) do\n    value =\n      value\n      |> to_string()\n      |> String.replace(\"\\n\", \" \")\n      |> String.replace(~r/\\s+/, \" \")\n      |> String.trim()\n      |> truncate_plain(width)\n\n    case align do\n      :right -> String.pad_leading(value, width)\n      _ -> String.pad_trailing(value, width)\n    end\n  end\n\n  defp truncate_plain(value, width) do\n    if byte_size(value) <= width do\n      value\n    else\n      String.slice(value, 0, width - 3) <> \"...\"\n    end\n  end\n\n  defp compact_session_id(nil), do: \"n/a\"\n  defp compact_session_id(session_id) when not is_binary(session_id), do: \"n/a\"\n\n  defp compact_session_id(session_id) do\n    if String.length(session_id) > 10 do\n      String.slice(session_id, 0, 4) <> \"...\" <> String.slice(session_id, -6, 6)\n    else\n      session_id\n    end\n  end\n\n  defp group_thousands(value) when is_binary(value) do\n    sign = if String.starts_with?(value, \"-\"), do: \"-\", else: \"\"\n    unsigned = if sign == \"\", do: value, else: String.slice(value, 1, String.length(value) - 1)\n\n    unsigned\n    |> String.reverse()\n    |> String.replace(~r/(\\d{3})(?=\\d)/, \"\\\\1,\")\n    |> String.reverse()\n    |> prepend(sign)\n  end\n\n  defp prepend(\"\", value), do: value\n  defp prepend(prefix, value), do: prefix <> value\n\n  defp format_tps(value) when is_number(value) do\n    value\n    |> trunc()\n    |> Integer.to_string()\n    |> group_thousands()\n  end\n\n  defp tps_graph(samples, now_ms, current_tokens) do\n    bucket_ms = div(@throughput_graph_window_ms, @throughput_graph_columns)\n    active_bucket_start = div(now_ms, bucket_ms) * bucket_ms\n    graph_window_start = active_bucket_start - (@throughput_graph_columns - 1) * bucket_ms\n\n    rates =\n      [{now_ms, current_tokens} | samples]\n      |> prune_graph_samples(now_ms)\n      |> Enum.sort_by(&elem(&1, 0))\n      |> Enum.chunk_every(2, 1, :discard)\n      |> Enum.map(fn [{start_ms, start_tokens}, {end_ms, end_tokens}] ->\n        elapsed_ms = end_ms - start_ms\n        delta_tokens = max(0, end_tokens - start_tokens)\n        tps = if elapsed_ms <= 0, do: 0.0, else: delta_tokens / (elapsed_ms / 1000.0)\n        {end_ms, tps}\n      end)\n\n    bucketed_tps =\n      0..(@throughput_graph_columns - 1)\n      |> Enum.map(fn bucket_idx ->\n        bucket_start = graph_window_start + bucket_idx * bucket_ms\n        bucket_end = bucket_start + bucket_ms\n        last_bucket? = bucket_idx == @throughput_graph_columns - 1\n\n        values =\n          rates\n          |> Enum.filter(fn {timestamp, _tps} ->\n            in_bucket?(timestamp, bucket_start, bucket_end, last_bucket?)\n          end)\n          |> Enum.map(fn {_timestamp, tps} -> tps end)\n\n        if values == [] do\n          0.0\n        else\n          Enum.sum(values) / length(values)\n        end\n      end)\n\n    max_tps = Enum.max(bucketed_tps, fn -> 0.0 end)\n\n    bucketed_tps\n    |> Enum.map_join(fn value ->\n      index =\n        if max_tps <= 0 do\n          0\n        else\n          round(value / max_tps * (length(@sparkline_blocks) - 1))\n        end\n\n      Enum.at(@sparkline_blocks, index, \"▁\")\n    end)\n  end\n\n  defp in_bucket?(timestamp, bucket_start, bucket_end, true),\n    do: timestamp >= bucket_start and timestamp <= bucket_end\n\n  defp in_bucket?(timestamp, bucket_start, bucket_end, false),\n    do: timestamp >= bucket_start and timestamp < bucket_end\n\n  defp format_rate_limits(nil), do: colorize(\"unavailable\", @ansi_gray)\n\n  defp format_rate_limits(rate_limits) when is_map(rate_limits) do\n    limit_id =\n      map_value(rate_limits, [\"limit_id\", :limit_id, \"limit_name\", :limit_name]) ||\n        \"unknown\"\n\n    primary = format_rate_limit_bucket(map_value(rate_limits, [\"primary\", :primary]))\n    secondary = format_rate_limit_bucket(map_value(rate_limits, [\"secondary\", :secondary]))\n    credits = format_rate_limit_credits(map_value(rate_limits, [\"credits\", :credits]))\n\n    colorize(to_string(limit_id), @ansi_yellow) <>\n      colorize(\" | \", @ansi_gray) <>\n      colorize(\"primary #{primary}\", @ansi_cyan) <>\n      colorize(\" | \", @ansi_gray) <>\n      colorize(\"secondary #{secondary}\", @ansi_cyan) <>\n      colorize(\" | \", @ansi_gray) <>\n      colorize(credits, @ansi_green)\n  end\n\n  defp format_rate_limits(other) do\n    other\n    |> inspect(limit: 10)\n    |> truncate(80)\n    |> colorize(@ansi_gray)\n  end\n\n  defp format_rate_limit_bucket(nil), do: \"n/a\"\n\n  defp format_rate_limit_bucket(bucket) when is_map(bucket) do\n    remaining = map_value(bucket, [\"remaining\", :remaining])\n    limit = map_value(bucket, [\"limit\", :limit])\n\n    reset_value =\n      map_value(bucket, [\n        \"reset_in_seconds\",\n        :reset_in_seconds,\n        \"resetInSeconds\",\n        :resetInSeconds,\n        \"reset_at\",\n        :reset_at,\n        \"resetAt\",\n        :resetAt,\n        \"resets_at\",\n        :resets_at,\n        \"resetsAt\",\n        :resetsAt\n      ])\n\n    base =\n      cond do\n        integer_like?(remaining) and integer_like?(limit) ->\n          \"#{format_count(remaining)}/#{format_count(limit)}\"\n\n        integer_like?(remaining) ->\n          \"remaining #{format_count(remaining)}\"\n\n        integer_like?(limit) ->\n          \"limit #{format_count(limit)}\"\n\n        map_size(bucket) == 0 ->\n          \"n/a\"\n\n        true ->\n          bucket |> inspect(limit: 6) |> truncate(40)\n      end\n\n    if is_nil(reset_value) do\n      base\n    else\n      \"#{base} reset #{format_reset_value(reset_value)}\"\n    end\n  end\n\n  defp format_rate_limit_bucket(other), do: to_string(other)\n\n  defp format_rate_limit_credits(nil), do: \"credits n/a\"\n\n  defp format_rate_limit_credits(credits) when is_map(credits) do\n    unlimited = map_value(credits, [\"unlimited\", :unlimited]) == true\n    has_credits = map_value(credits, [\"has_credits\", :has_credits]) == true\n    balance = map_value(credits, [\"balance\", :balance])\n\n    cond do\n      unlimited ->\n        \"credits unlimited\"\n\n      has_credits and is_number(balance) ->\n        \"credits #{format_number(balance)}\"\n\n      has_credits ->\n        \"credits available\"\n\n      true ->\n        \"credits none\"\n    end\n  end\n\n  defp format_rate_limit_credits(other), do: \"credits #{to_string(other)}\"\n\n  defp format_reset_value(value) when is_integer(value), do: \"#{format_count(value)}s\"\n  defp format_reset_value(value) when is_binary(value), do: value\n  defp format_reset_value(value), do: to_string(value)\n\n  defp format_number(value) when is_integer(value), do: format_count(value)\n\n  defp format_number(value) when is_float(value) do\n    value\n    |> Float.round(2)\n    |> :erlang.float_to_binary(decimals: 2)\n  end\n\n  defp map_value(map, keys) when is_map(map) and is_list(keys) do\n    Enum.find_value(keys, &Map.get(map, &1))\n  end\n\n  defp map_value(_map, _keys), do: nil\n\n  defp integer_like?(value) when is_integer(value), do: true\n  defp integer_like?(_value), do: false\n\n  defp status_dot(color_code) do\n    colorize(\"●\", color_code)\n  end\n\n  defp snapshot_total_tokens({:ok, %{codex_totals: codex_totals}}) when is_map(codex_totals) do\n    Map.get(codex_totals, :total_tokens, 0)\n  end\n\n  defp snapshot_total_tokens(_snapshot_data), do: 0\n\n  defp format_timestamp(datetime) do\n    datetime\n    |> DateTime.truncate(:second)\n    |> DateTime.to_string()\n  end\n\n  defp normalize_status_lines(content) do\n    content\n  end\n\n  defp closing_border, do: \"╰─\"\n\n  defp colorize(value, code) do\n    \"#{code}#{value}#{@ansi_reset}\"\n  end\n\n  @doc false\n  @spec humanize_codex_message(term()) :: String.t()\n  def humanize_codex_message(nil), do: \"no codex message yet\"\n\n  def humanize_codex_message(%{event: event, message: message}) do\n    payload = unwrap_codex_message_payload(message)\n\n    (humanize_codex_event(event, message, payload) || humanize_codex_payload(payload))\n    |> truncate(140)\n  end\n\n  def humanize_codex_message(%{message: message}) do\n    message\n    |> unwrap_codex_message_payload()\n    |> humanize_codex_payload()\n    |> truncate(140)\n  end\n\n  def humanize_codex_message(message) do\n    message\n    |> unwrap_codex_message_payload()\n    |> humanize_codex_payload()\n    |> truncate(140)\n  end\n\n  defp summarize_message(message), do: humanize_codex_message(message)\n\n  defp humanize_codex_event(:session_started, _message, payload) do\n    session_id = map_value(payload, [\"session_id\", :session_id])\n\n    if is_binary(session_id) do\n      \"session started (#{session_id})\"\n    else\n      \"session started\"\n    end\n  end\n\n  defp humanize_codex_event(:turn_input_required, _message, _payload), do: \"turn blocked: waiting for user input\"\n\n  defp humanize_codex_event(:approval_auto_approved, message, payload) do\n    method =\n      map_value(payload, [\"method\", :method]) ||\n        map_path(message, [\"payload\", \"method\"]) ||\n        map_path(message, [:payload, :method])\n\n    decision = map_value(message, [\"decision\", :decision])\n\n    base =\n      if is_binary(method) do\n        \"#{humanize_codex_method(method, payload)} (auto-approved)\"\n      else\n        \"approval request auto-approved\"\n      end\n\n    if is_binary(decision), do: \"#{base}: #{decision}\", else: base\n  end\n\n  defp humanize_codex_event(:tool_input_auto_answered, message, payload) do\n    answer = map_value(message, [\"answer\", :answer])\n\n    base =\n      case humanize_codex_method(\"item/tool/requestUserInput\", payload) do\n        nil -> \"tool input auto-answered\"\n        text -> \"#{text} (auto-answered)\"\n      end\n\n    if is_binary(answer), do: \"#{base}: #{inline_text(answer)}\", else: base\n  end\n\n  defp humanize_codex_event(:tool_call_completed, _message, payload),\n    do: humanize_dynamic_tool_event(\"dynamic tool call completed\", payload)\n\n  defp humanize_codex_event(:tool_call_failed, _message, payload),\n    do: humanize_dynamic_tool_event(\"dynamic tool call failed\", payload)\n\n  defp humanize_codex_event(:unsupported_tool_call, _message, payload),\n    do: humanize_dynamic_tool_event(\"unsupported dynamic tool call rejected\", payload)\n\n  defp humanize_codex_event(:turn_ended_with_error, message, _payload), do: \"turn ended with error: #{format_reason(message)}\"\n  defp humanize_codex_event(:startup_failed, message, _payload), do: \"startup failed: #{format_reason(message)}\"\n  defp humanize_codex_event(:turn_failed, _message, payload), do: humanize_codex_method(\"turn/failed\", payload)\n  defp humanize_codex_event(:turn_cancelled, _message, _payload), do: \"turn cancelled\"\n  defp humanize_codex_event(:malformed, _message, _payload), do: \"malformed JSON event from codex\"\n  defp humanize_codex_event(_event, _message, _payload), do: nil\n\n  defp unwrap_codex_message_payload(%{} = message) do\n    cond do\n      is_binary(map_value(message, [\"method\", :method])) -> message\n      is_binary(map_value(message, [\"session_id\", :session_id])) -> message\n      is_binary(map_value(message, [\"reason\", :reason])) -> message\n      true -> map_value(message, [\"payload\", :payload]) || message\n    end\n  end\n\n  defp unwrap_codex_message_payload(message), do: message\n\n  defp humanize_codex_payload(%{} = payload) do\n    case map_value(payload, [\"method\", :method]) do\n      method when is_binary(method) ->\n        humanize_codex_method(method, payload)\n\n      _ ->\n        cond do\n          is_binary(map_value(payload, [\"session_id\", :session_id])) ->\n            \"session started (#{map_value(payload, [\"session_id\", :session_id])})\"\n\n          match?(%{\"error\" => _}, payload) ->\n            \"error: #{format_error_value(Map.get(payload, \"error\"))}\"\n\n          true ->\n            payload\n            |> inspect(pretty: true, limit: 30)\n            |> String.replace(\"\\n\", \" \")\n            |> sanitize_ansi_and_control_bytes()\n            |> String.trim()\n        end\n    end\n  end\n\n  defp humanize_codex_payload(payload) when is_binary(payload) do\n    payload\n    |> String.replace(\"\\n\", \" \")\n    |> sanitize_ansi_and_control_bytes()\n    |> String.trim()\n  end\n\n  defp humanize_codex_payload(payload) do\n    payload\n    |> inspect(pretty: true, limit: 20)\n    |> String.replace(\"\\n\", \" \")\n    |> sanitize_ansi_and_control_bytes()\n    |> String.trim()\n  end\n\n  defp sanitize_ansi_and_control_bytes(value) when is_binary(value) do\n    value\n    |> String.replace(~r/\\x1B\\[[0-9;]*[A-Za-z]/, \"\")\n    |> String.replace(~r/\\x1B./, \"\")\n    |> String.replace(~r/[\\x00-\\x1F\\x7F]/, \"\")\n  end\n\n  defp humanize_codex_method(\"thread/started\", payload) do\n    thread_id = map_path(payload, [\"params\", \"thread\", \"id\"]) || map_path(payload, [:params, :thread, :id])\n\n    if is_binary(thread_id) do\n      \"thread started (#{thread_id})\"\n    else\n      \"thread started\"\n    end\n  end\n\n  defp humanize_codex_method(\"turn/started\", payload) do\n    turn_id = map_path(payload, [\"params\", \"turn\", \"id\"]) || map_path(payload, [:params, :turn, :id])\n\n    if is_binary(turn_id) do\n      \"turn started (#{turn_id})\"\n    else\n      \"turn started\"\n    end\n  end\n\n  defp humanize_codex_method(\"turn/completed\", payload) do\n    status =\n      map_path(payload, [\"params\", \"turn\", \"status\"]) ||\n        map_path(payload, [:params, :turn, :status]) ||\n        \"completed\"\n\n    usage =\n      map_path(payload, [\"params\", \"usage\"]) ||\n        map_path(payload, [:params, :usage]) ||\n        map_path(payload, [\"params\", \"tokenUsage\"]) ||\n        map_path(payload, [:params, :tokenUsage]) ||\n        map_value(payload, [\"usage\", :usage])\n\n    usage_suffix =\n      case format_usage_counts(usage) do\n        nil -> \"\"\n        usage_text -> \" (#{usage_text})\"\n      end\n\n    \"turn completed (#{status})#{usage_suffix}\"\n  end\n\n  defp humanize_codex_method(\"turn/failed\", payload) do\n    error_message =\n      map_path(payload, [\"params\", \"error\", \"message\"]) ||\n        map_path(payload, [:params, :error, :message])\n\n    if is_binary(error_message), do: \"turn failed: #{error_message}\", else: \"turn failed\"\n  end\n\n  defp humanize_codex_method(\"turn/cancelled\", _payload), do: \"turn cancelled\"\n\n  defp humanize_codex_method(\"turn/diff/updated\", payload) do\n    diff =\n      map_path(payload, [\"params\", \"diff\"]) ||\n        map_path(payload, [:params, :diff]) ||\n        \"\"\n\n    if is_binary(diff) and diff != \"\" do\n      line_count = diff |> String.split(\"\\n\", trim: true) |> length()\n      \"turn diff updated (#{line_count} lines)\"\n    else\n      \"turn diff updated\"\n    end\n  end\n\n  defp humanize_codex_method(\"turn/plan/updated\", payload) do\n    plan_entries =\n      map_path(payload, [\"params\", \"plan\"]) ||\n        map_path(payload, [:params, :plan]) ||\n        map_path(payload, [\"params\", \"steps\"]) ||\n        map_path(payload, [:params, :steps]) ||\n        map_path(payload, [\"params\", \"items\"]) ||\n        map_path(payload, [:params, :items]) ||\n        []\n\n    if is_list(plan_entries) do\n      \"plan updated (#{length(plan_entries)} steps)\"\n    else\n      \"plan updated\"\n    end\n  end\n\n  defp humanize_codex_method(\"thread/tokenUsage/updated\", payload) do\n    usage =\n      map_path(payload, [\"params\", \"tokenUsage\", \"total\"]) ||\n        map_path(payload, [:params, :tokenUsage, :total]) ||\n        map_value(payload, [\"usage\", :usage])\n\n    case format_usage_counts(usage) do\n      nil -> \"thread token usage updated\"\n      usage_text -> \"thread token usage updated (#{usage_text})\"\n    end\n  end\n\n  defp humanize_codex_method(\"item/started\", payload), do: humanize_item_lifecycle(\"started\", payload)\n  defp humanize_codex_method(\"item/completed\", payload), do: humanize_item_lifecycle(\"completed\", payload)\n\n  defp humanize_codex_method(\"item/agentMessage/delta\", payload),\n    do: humanize_streaming_event(\"agent message streaming\", payload)\n\n  defp humanize_codex_method(\"item/plan/delta\", payload),\n    do: humanize_streaming_event(\"plan streaming\", payload)\n\n  defp humanize_codex_method(\"item/reasoning/summaryTextDelta\", payload),\n    do: humanize_streaming_event(\"reasoning summary streaming\", payload)\n\n  defp humanize_codex_method(\"item/reasoning/summaryPartAdded\", payload),\n    do: humanize_streaming_event(\"reasoning summary section added\", payload)\n\n  defp humanize_codex_method(\"item/reasoning/textDelta\", payload),\n    do: humanize_streaming_event(\"reasoning text streaming\", payload)\n\n  defp humanize_codex_method(\"item/commandExecution/outputDelta\", payload),\n    do: humanize_streaming_event(\"command output streaming\", payload)\n\n  defp humanize_codex_method(\"item/fileChange/outputDelta\", payload),\n    do: humanize_streaming_event(\"file change output streaming\", payload)\n\n  defp humanize_codex_method(\"item/commandExecution/requestApproval\", payload) do\n    command = extract_command(payload)\n\n    if is_binary(command) do\n      \"command approval requested (#{command})\"\n    else\n      \"command approval requested\"\n    end\n  end\n\n  defp humanize_codex_method(\"item/fileChange/requestApproval\", payload) do\n    change_count = map_path(payload, [\"params\", \"fileChangeCount\"]) || map_path(payload, [\"params\", \"changeCount\"])\n\n    if is_integer(change_count) and change_count > 0 do\n      \"file change approval requested (#{change_count} files)\"\n    else\n      \"file change approval requested\"\n    end\n  end\n\n  defp humanize_codex_method(\"item/tool/requestUserInput\", payload) do\n    question =\n      map_path(payload, [\"params\", \"question\"]) ||\n        map_path(payload, [\"params\", \"prompt\"]) ||\n        map_path(payload, [:params, :question]) ||\n        map_path(payload, [:params, :prompt])\n\n    if is_binary(question) and String.trim(question) != \"\" do\n      \"tool requires user input: #{inline_text(question)}\"\n    else\n      \"tool requires user input\"\n    end\n  end\n\n  defp humanize_codex_method(\"tool/requestUserInput\", payload),\n    do: humanize_codex_method(\"item/tool/requestUserInput\", payload)\n\n  defp humanize_codex_method(\"account/updated\", payload) do\n    auth_mode =\n      map_path(payload, [\"params\", \"authMode\"]) ||\n        map_path(payload, [:params, :authMode]) ||\n        \"unknown\"\n\n    \"account updated (auth #{auth_mode})\"\n  end\n\n  defp humanize_codex_method(\"account/rateLimits/updated\", payload) do\n    rate_limits =\n      map_path(payload, [\"params\", \"rateLimits\"]) ||\n        map_path(payload, [:params, :rateLimits])\n\n    \"rate limits updated: #{format_rate_limits_summary(rate_limits)}\"\n  end\n\n  defp humanize_codex_method(\"account/chatgptAuthTokens/refresh\", _payload), do: \"account auth token refresh requested\"\n\n  defp humanize_codex_method(\"item/tool/call\", payload) do\n    tool = dynamic_tool_name(payload)\n\n    if is_binary(tool) and String.trim(tool) != \"\" do\n      \"dynamic tool call requested (#{tool})\"\n    else\n      \"dynamic tool call requested\"\n    end\n  end\n\n  defp humanize_codex_method(<<\"codex/event/\", suffix::binary>>, payload) do\n    humanize_codex_wrapper_event(suffix, payload)\n  end\n\n  defp humanize_codex_method(method, payload) do\n    msg_type =\n      map_path(payload, [\"params\", \"msg\", \"type\"]) ||\n        map_path(payload, [:params, :msg, :type])\n\n    if is_binary(msg_type) do\n      \"#{method} (#{msg_type})\"\n    else\n      method\n    end\n  end\n\n  defp humanize_dynamic_tool_event(base, payload) do\n    case dynamic_tool_name(payload) do\n      tool when is_binary(tool) ->\n        trimmed = String.trim(tool)\n\n        if trimmed == \"\" do\n          base\n        else\n          \"#{base} (#{trimmed})\"\n        end\n\n      _ ->\n        base\n    end\n  end\n\n  defp dynamic_tool_name(payload) do\n    map_path(payload, [\"params\", \"tool\"]) ||\n      map_path(payload, [\"params\", \"name\"]) ||\n      map_path(payload, [:params, :tool]) ||\n      map_path(payload, [:params, :name])\n  end\n\n  defp humanize_item_lifecycle(state, payload) do\n    item =\n      map_path(payload, [\"params\", \"item\"]) ||\n        map_path(payload, [:params, :item]) ||\n        %{}\n\n    item_type = item |> map_value([\"type\", :type]) |> humanize_item_type()\n    item_status = map_value(item, [\"status\", :status])\n    item_id = map_value(item, [\"id\", :id])\n\n    details =\n      []\n      |> append_if_present(short_id(item_id))\n      |> append_if_present(humanize_status(item_status))\n\n    detail_suffix = if details == [], do: \"\", else: \" (#{Enum.join(details, \", \")})\"\n    \"item #{state}: #{item_type}#{detail_suffix}\"\n  end\n\n  defp humanize_codex_wrapper_event(\"mcp_startup_update\", payload) do\n    server =\n      map_path(payload, [\"params\", \"msg\", \"server\"]) ||\n        map_path(payload, [:params, :msg, :server]) ||\n        \"mcp\"\n\n    state =\n      map_path(payload, [\"params\", \"msg\", \"status\", \"state\"]) ||\n        map_path(payload, [:params, :msg, :status, :state]) ||\n        \"updated\"\n\n    \"mcp startup: #{server} #{state}\"\n  end\n\n  defp humanize_codex_wrapper_event(\"mcp_startup_complete\", _payload), do: \"mcp startup complete\"\n  defp humanize_codex_wrapper_event(\"task_started\", _payload), do: \"task started\"\n  defp humanize_codex_wrapper_event(\"user_message\", _payload), do: \"user message received\"\n\n  defp humanize_codex_wrapper_event(\"item_started\", payload) do\n    case wrapper_payload_type(payload) do\n      \"token_count\" -> humanize_codex_wrapper_event(\"token_count\", payload)\n      type when is_binary(type) -> \"item started (#{humanize_item_type(type)})\"\n      _ -> \"item started\"\n    end\n  end\n\n  defp humanize_codex_wrapper_event(\"item_completed\", payload) do\n    case wrapper_payload_type(payload) do\n      \"token_count\" -> humanize_codex_wrapper_event(\"token_count\", payload)\n      type when is_binary(type) -> \"item completed (#{humanize_item_type(type)})\"\n      _ -> \"item completed\"\n    end\n  end\n\n  defp humanize_codex_wrapper_event(\"agent_message_delta\", payload),\n    do: humanize_streaming_event(\"agent message streaming\", payload)\n\n  defp humanize_codex_wrapper_event(\"agent_message_content_delta\", payload),\n    do: humanize_streaming_event(\"agent message content streaming\", payload)\n\n  defp humanize_codex_wrapper_event(\"agent_reasoning_delta\", payload),\n    do: humanize_streaming_event(\"reasoning streaming\", payload)\n\n  defp humanize_codex_wrapper_event(\"reasoning_content_delta\", payload),\n    do: humanize_streaming_event(\"reasoning content streaming\", payload)\n\n  defp humanize_codex_wrapper_event(\"agent_reasoning_section_break\", _payload), do: \"reasoning section break\"\n  defp humanize_codex_wrapper_event(\"agent_reasoning\", payload), do: humanize_reasoning_update(payload)\n  defp humanize_codex_wrapper_event(\"turn_diff\", _payload), do: \"turn diff updated\"\n  defp humanize_codex_wrapper_event(\"exec_command_begin\", payload), do: humanize_exec_command_begin(payload)\n  defp humanize_codex_wrapper_event(\"exec_command_end\", payload), do: humanize_exec_command_end(payload)\n  defp humanize_codex_wrapper_event(\"exec_command_output_delta\", _payload), do: \"command output streaming\"\n  defp humanize_codex_wrapper_event(\"mcp_tool_call_begin\", _payload), do: \"mcp tool call started\"\n  defp humanize_codex_wrapper_event(\"mcp_tool_call_end\", _payload), do: \"mcp tool call completed\"\n\n  defp humanize_codex_wrapper_event(\"token_count\", payload) do\n    usage = extract_first_path(payload, token_usage_paths())\n\n    case format_usage_counts(usage) do\n      nil -> \"token count update\"\n      usage_text -> \"token count update (#{usage_text})\"\n    end\n  end\n\n  defp humanize_codex_wrapper_event(other, payload) do\n    msg_type =\n      map_path(payload, [\"params\", \"msg\", \"type\"]) ||\n        map_path(payload, [:params, :msg, :type])\n\n    if is_binary(msg_type) do\n      \"#{other} (#{msg_type})\"\n    else\n      other\n    end\n  end\n\n  defp humanize_exec_command_begin(payload) do\n    command =\n      map_path(payload, [\"params\", \"msg\", \"command\"]) ||\n        map_path(payload, [:params, :msg, :command]) ||\n        map_path(payload, [\"params\", \"msg\", \"parsed_cmd\"]) ||\n        map_path(payload, [:params, :msg, :parsed_cmd])\n\n    command = normalize_command(command)\n\n    if is_binary(command) do\n      command\n    else\n      \"command started\"\n    end\n  end\n\n  defp humanize_exec_command_end(payload) do\n    exit_code =\n      map_path(payload, [\"params\", \"msg\", \"exit_code\"]) ||\n        map_path(payload, [:params, :msg, :exit_code]) ||\n        map_path(payload, [\"params\", \"msg\", \"exitCode\"]) ||\n        map_path(payload, [:params, :msg, :exitCode])\n\n    if is_integer(exit_code) do\n      \"command completed (exit #{exit_code})\"\n    else\n      \"command completed\"\n    end\n  end\n\n  defp format_usage_counts(usage) when is_map(usage) do\n    input =\n      parse_integer(\n        map_value(usage, [\n          \"input_tokens\",\n          :input_tokens,\n          \"prompt_tokens\",\n          :prompt_tokens,\n          \"inputTokens\",\n          :inputTokens,\n          \"promptTokens\",\n          :promptTokens\n        ])\n      )\n\n    output =\n      parse_integer(\n        map_value(usage, [\n          \"output_tokens\",\n          :output_tokens,\n          \"completion_tokens\",\n          :completion_tokens,\n          \"outputTokens\",\n          :outputTokens,\n          \"completionTokens\",\n          :completionTokens\n        ])\n      )\n\n    total =\n      parse_integer(\n        map_value(usage, [\n          \"total_tokens\",\n          :total_tokens,\n          \"total\",\n          :total,\n          \"totalTokens\",\n          :totalTokens\n        ])\n      )\n\n    parts =\n      []\n      |> append_usage_part(\"in\", input)\n      |> append_usage_part(\"out\", output)\n      |> append_usage_part(\"total\", total)\n\n    case parts do\n      [] -> nil\n      _ -> Enum.join(parts, \", \")\n    end\n  end\n\n  defp format_usage_counts(_usage), do: nil\n\n  defp append_usage_part(parts, _label, value) when not is_integer(value), do: parts\n  defp append_usage_part(parts, label, value), do: parts ++ [\"#{label} #{format_count(value)}\"]\n\n  defp format_rate_limits_summary(nil), do: \"n/a\"\n\n  defp format_rate_limits_summary(rate_limits) when is_map(rate_limits) do\n    primary = map_value(rate_limits, [\"primary\", :primary])\n    secondary = map_value(rate_limits, [\"secondary\", :secondary])\n\n    primary_text = format_rate_limit_bucket_summary(primary)\n    secondary_text = format_rate_limit_bucket_summary(secondary)\n\n    cond do\n      primary_text != nil and secondary_text != nil -> \"primary #{primary_text}; secondary #{secondary_text}\"\n      primary_text != nil -> \"primary #{primary_text}\"\n      secondary_text != nil -> \"secondary #{secondary_text}\"\n      true -> \"n/a\"\n    end\n  end\n\n  defp format_rate_limits_summary(_rate_limits), do: \"n/a\"\n\n  defp format_rate_limit_bucket_summary(bucket) when is_map(bucket) do\n    used_percent = map_value(bucket, [\"usedPercent\", :usedPercent])\n    window_mins = map_value(bucket, [\"windowDurationMins\", :windowDurationMins])\n\n    cond do\n      is_number(used_percent) and is_integer(window_mins) ->\n        \"#{used_percent}% / #{window_mins}m\"\n\n      is_number(used_percent) ->\n        \"#{used_percent}% used\"\n\n      true ->\n        nil\n    end\n  end\n\n  defp format_rate_limit_bucket_summary(_bucket), do: nil\n\n  defp format_error_value(%{\"message\" => message}) when is_binary(message), do: message\n  defp format_error_value(%{message: message}) when is_binary(message), do: message\n  defp format_error_value(error), do: inspect(error, limit: 10)\n\n  defp format_reason(message) when is_map(message) do\n    case map_value(message, [\"reason\", :reason]) do\n      nil ->\n        message\n        |> inspect(limit: 10)\n        |> inline_text()\n\n      reason ->\n        format_error_value(reason)\n    end\n  end\n\n  defp format_reason(other), do: format_error_value(other)\n\n  defp humanize_streaming_event(label, payload) do\n    case extract_delta_preview(payload) do\n      nil -> label\n      preview -> \"#{label}: #{preview}\"\n    end\n  end\n\n  defp humanize_reasoning_update(payload) do\n    case extract_reasoning_focus(payload) do\n      nil -> \"reasoning update\"\n      focus -> \"reasoning update: #{focus}\"\n    end\n  end\n\n  defp extract_reasoning_focus(payload) do\n    value = extract_first_path(payload, reasoning_focus_paths())\n\n    if is_binary(value) do\n      trimmed = String.trim(value)\n      if trimmed == \"\", do: nil, else: inline_text(trimmed)\n    else\n      nil\n    end\n  end\n\n  defp extract_delta_preview(payload) do\n    delta = extract_first_path(payload, delta_paths())\n\n    case delta do\n      value when is_binary(value) ->\n        trimmed = String.trim(value)\n        if trimmed == \"\", do: nil, else: inline_text(trimmed)\n\n      _ ->\n        nil\n    end\n  end\n\n  defp extract_command(payload) do\n    payload\n    |> map_path([\"params\", \"parsedCmd\"])\n    |> fallback_command(payload)\n    |> normalize_command()\n  end\n\n  defp fallback_command(nil, payload) do\n    map_path(payload, [\"params\", \"command\"]) ||\n      map_path(payload, [\"params\", \"cmd\"]) ||\n      map_path(payload, [\"params\", \"argv\"]) ||\n      map_path(payload, [\"params\", \"args\"])\n  end\n\n  defp fallback_command(command, _payload), do: command\n\n  defp normalize_command(%{} = command) do\n    binary_command = map_value(command, [\"parsedCmd\", :parsedCmd, \"command\", :command, \"cmd\", :cmd])\n    args = map_value(command, [\"args\", :args, \"argv\", :argv])\n\n    if is_binary(binary_command) and is_list(args) do\n      normalize_command([binary_command | args])\n    else\n      normalize_command(binary_command || args)\n    end\n  end\n\n  defp normalize_command(command) when is_binary(command), do: inline_text(command)\n\n  defp normalize_command(command) when is_list(command) do\n    if Enum.all?(command, &is_binary/1) do\n      command\n      |> Enum.join(\" \")\n      |> inline_text()\n    else\n      nil\n    end\n  end\n\n  defp normalize_command(_command), do: nil\n\n  defp humanize_item_type(nil), do: \"item\"\n\n  defp humanize_item_type(type) when is_binary(type) do\n    type\n    |> String.replace(~r/([a-z0-9])([A-Z])/, \"\\\\1 \\\\2\")\n    |> String.replace(\"_\", \" \")\n    |> String.replace(\"/\", \" \")\n    |> String.downcase()\n    |> String.trim()\n  end\n\n  defp humanize_item_type(type), do: to_string(type)\n\n  defp humanize_status(status) when is_binary(status) do\n    status\n    |> String.replace(\"_\", \" \")\n    |> String.replace(\"-\", \" \")\n    |> String.downcase()\n    |> String.trim()\n  end\n\n  defp humanize_status(_status), do: nil\n\n  defp short_id(id) when is_binary(id) and byte_size(id) > 12, do: String.slice(id, 0, 12)\n  defp short_id(id) when is_binary(id), do: id\n  defp short_id(_id), do: nil\n\n  defp append_if_present(list, value) when is_binary(value) and value != \"\", do: list ++ [value]\n  defp append_if_present(list, _value), do: list\n\n  defp wrapper_payload_type(payload) do\n    map_path(payload, [\"params\", \"msg\", \"payload\", \"type\"]) ||\n      map_path(payload, [:params, :msg, :payload, :type])\n  end\n\n  defp inline_text(text) when is_binary(text) do\n    text\n    |> String.replace(\"\\n\", \" \")\n    |> String.replace(~r/\\s+/, \" \")\n    |> String.trim()\n    |> truncate(80)\n  end\n\n  defp inline_text(other), do: other |> to_string() |> inline_text()\n\n  defp parse_integer(value) when is_integer(value), do: value\n\n  defp parse_integer(value) when is_binary(value) do\n    case Integer.parse(String.trim(value)) do\n      {parsed, \"\"} -> parsed\n      _ -> nil\n    end\n  end\n\n  defp parse_integer(_value), do: nil\n\n  defp token_usage_paths do\n    [\n      [\"params\", \"msg\", \"payload\", \"info\", \"total_token_usage\"],\n      [:params, :msg, :payload, :info, :total_token_usage],\n      [\"params\", \"msg\", \"info\", \"total_token_usage\"],\n      [:params, :msg, :info, :total_token_usage],\n      [\"params\", \"tokenUsage\", \"total\"],\n      [:params, :tokenUsage, :total]\n    ]\n  end\n\n  defp delta_paths do\n    [\n      [\"params\", \"delta\"],\n      [:params, :delta],\n      [\"params\", \"msg\", \"delta\"],\n      [:params, :msg, :delta],\n      [\"params\", \"textDelta\"],\n      [:params, :textDelta],\n      [\"params\", \"msg\", \"textDelta\"],\n      [:params, :msg, :textDelta],\n      [\"params\", \"outputDelta\"],\n      [:params, :outputDelta],\n      [\"params\", \"msg\", \"outputDelta\"],\n      [:params, :msg, :outputDelta],\n      [\"params\", \"text\"],\n      [:params, :text],\n      [\"params\", \"msg\", \"text\"],\n      [:params, :msg, :text],\n      [\"params\", \"summaryText\"],\n      [:params, :summaryText],\n      [\"params\", \"msg\", \"summaryText\"],\n      [:params, :msg, :summaryText],\n      [\"params\", \"msg\", \"content\"],\n      [:params, :msg, :content],\n      [\"params\", \"msg\", \"payload\", \"delta\"],\n      [:params, :msg, :payload, :delta],\n      [\"params\", \"msg\", \"payload\", \"textDelta\"],\n      [:params, :msg, :payload, :textDelta],\n      [\"params\", \"msg\", \"payload\", \"outputDelta\"],\n      [:params, :msg, :payload, :outputDelta],\n      [\"params\", \"msg\", \"payload\", \"text\"],\n      [:params, :msg, :payload, :text],\n      [\"params\", \"msg\", \"payload\", \"summaryText\"],\n      [:params, :msg, :payload, :summaryText],\n      [\"params\", \"msg\", \"payload\", \"content\"],\n      [:params, :msg, :payload, :content]\n    ]\n  end\n\n  defp reasoning_focus_paths do\n    [\n      [\"params\", \"reason\"],\n      [:params, :reason],\n      [\"params\", \"summaryText\"],\n      [:params, :summaryText],\n      [\"params\", \"summary\"],\n      [:params, :summary],\n      [\"params\", \"text\"],\n      [:params, :text],\n      [\"params\", \"msg\", \"reason\"],\n      [:params, :msg, :reason],\n      [\"params\", \"msg\", \"summaryText\"],\n      [:params, :msg, :summaryText],\n      [\"params\", \"msg\", \"summary\"],\n      [:params, :msg, :summary],\n      [\"params\", \"msg\", \"text\"],\n      [:params, :msg, :text],\n      [\"params\", \"msg\", \"payload\", \"reason\"],\n      [:params, :msg, :payload, :reason],\n      [\"params\", \"msg\", \"payload\", \"summaryText\"],\n      [:params, :msg, :payload, :summaryText],\n      [\"params\", \"msg\", \"payload\", \"summary\"],\n      [:params, :msg, :payload, :summary],\n      [\"params\", \"msg\", \"payload\", \"text\"],\n      [:params, :msg, :payload, :text]\n    ]\n  end\n\n  defp extract_first_path(payload, paths) do\n    Enum.find_value(paths, fn path ->\n      map_path(payload, path)\n    end)\n  end\n\n  defp map_path(data, [key | rest]) when is_map(data) do\n    case fetch_map_key(data, key) do\n      {:ok, value} when rest == [] -> value\n      {:ok, value} -> map_path(value, rest)\n      :error -> nil\n    end\n  end\n\n  defp map_path(_data, _path), do: nil\n\n  defp fetch_map_key(map, key) when is_map(map) do\n    case Map.fetch(map, key) do\n      {:ok, value} ->\n        {:ok, value}\n\n      :error ->\n        alternate = alternate_key(key)\n\n        if alternate == key do\n          :error\n        else\n          Map.fetch(map, alternate)\n        end\n    end\n  end\n\n  defp alternate_key(key) when is_binary(key) do\n    String.to_existing_atom(key)\n  rescue\n    ArgumentError -> key\n  end\n\n  defp alternate_key(key) when is_atom(key), do: Atom.to_string(key)\n  defp alternate_key(key), do: key\n\n  defp truncate(value, max) when byte_size(value) > max do\n    value |> String.slice(0, max) |> Kernel.<>(\"...\")\n  end\n\n  defp truncate(value, _max), do: value\n\n  defp dashboard_enabled? do\n    if Code.ensure_loaded?(Mix) and function_exported?(Mix, :env, 0) do\n      try do\n        Mix.env() != :test\n      rescue\n        _ -> true\n      end\n    else\n      true\n    end\n  end\n\n  defp keyword_override(opts, key) do\n    if Keyword.has_key?(opts, key), do: Keyword.fetch!(opts, key), else: nil\n  end\n\n  defp resolve_override(nil, default), do: default\n  defp resolve_override(override, _default), do: override\nend\n"
  },
  {
    "path": "elixir/lib/symphony_elixir/tracker/memory.ex",
    "content": "defmodule SymphonyElixir.Tracker.Memory do\n  @moduledoc \"\"\"\n  In-memory tracker adapter used for tests and local development.\n  \"\"\"\n\n  @behaviour SymphonyElixir.Tracker\n\n  alias SymphonyElixir.Linear.Issue\n\n  @spec fetch_candidate_issues() :: {:ok, [Issue.t()]} | {:error, term()}\n  def fetch_candidate_issues do\n    {:ok, issue_entries()}\n  end\n\n  @spec fetch_issues_by_states([String.t()]) :: {:ok, [Issue.t()]} | {:error, term()}\n  def fetch_issues_by_states(state_names) do\n    normalized_states =\n      state_names\n      |> Enum.map(&normalize_state/1)\n      |> MapSet.new()\n\n    {:ok,\n     Enum.filter(issue_entries(), fn %Issue{state: state} ->\n       MapSet.member?(normalized_states, normalize_state(state))\n     end)}\n  end\n\n  @spec fetch_issue_states_by_ids([String.t()]) :: {:ok, [Issue.t()]} | {:error, term()}\n  def fetch_issue_states_by_ids(issue_ids) do\n    wanted_ids = MapSet.new(issue_ids)\n\n    {:ok,\n     Enum.filter(issue_entries(), fn %Issue{id: id} ->\n       MapSet.member?(wanted_ids, id)\n     end)}\n  end\n\n  @spec create_comment(String.t(), String.t()) :: :ok | {:error, term()}\n  def create_comment(issue_id, body) do\n    send_event({:memory_tracker_comment, issue_id, body})\n    :ok\n  end\n\n  @spec update_issue_state(String.t(), String.t()) :: :ok | {:error, term()}\n  def update_issue_state(issue_id, state_name) do\n    send_event({:memory_tracker_state_update, issue_id, state_name})\n    :ok\n  end\n\n  defp configured_issues do\n    Application.get_env(:symphony_elixir, :memory_tracker_issues, [])\n  end\n\n  defp issue_entries do\n    Enum.filter(configured_issues(), &match?(%Issue{}, &1))\n  end\n\n  defp send_event(message) do\n    case Application.get_env(:symphony_elixir, :memory_tracker_recipient) do\n      pid when is_pid(pid) -> send(pid, message)\n      _ -> :ok\n    end\n  end\n\n  defp normalize_state(state) when is_binary(state) do\n    state\n    |> String.trim()\n    |> String.downcase()\n  end\n\n  defp normalize_state(_state), do: \"\"\nend\n"
  },
  {
    "path": "elixir/lib/symphony_elixir/tracker.ex",
    "content": "defmodule SymphonyElixir.Tracker do\n  @moduledoc \"\"\"\n  Adapter boundary for issue tracker reads and writes.\n  \"\"\"\n\n  alias SymphonyElixir.Config\n\n  @callback fetch_candidate_issues() :: {:ok, [term()]} | {:error, term()}\n  @callback fetch_issues_by_states([String.t()]) :: {:ok, [term()]} | {:error, term()}\n  @callback fetch_issue_states_by_ids([String.t()]) :: {:ok, [term()]} | {:error, term()}\n  @callback create_comment(String.t(), String.t()) :: :ok | {:error, term()}\n  @callback update_issue_state(String.t(), String.t()) :: :ok | {:error, term()}\n\n  @spec fetch_candidate_issues() :: {:ok, [term()]} | {:error, term()}\n  def fetch_candidate_issues do\n    adapter().fetch_candidate_issues()\n  end\n\n  @spec fetch_issues_by_states([String.t()]) :: {:ok, [term()]} | {:error, term()}\n  def fetch_issues_by_states(states) do\n    adapter().fetch_issues_by_states(states)\n  end\n\n  @spec fetch_issue_states_by_ids([String.t()]) :: {:ok, [term()]} | {:error, term()}\n  def fetch_issue_states_by_ids(issue_ids) do\n    adapter().fetch_issue_states_by_ids(issue_ids)\n  end\n\n  @spec create_comment(String.t(), String.t()) :: :ok | {:error, term()}\n  def create_comment(issue_id, body) do\n    adapter().create_comment(issue_id, body)\n  end\n\n  @spec update_issue_state(String.t(), String.t()) :: :ok | {:error, term()}\n  def update_issue_state(issue_id, state_name) do\n    adapter().update_issue_state(issue_id, state_name)\n  end\n\n  @spec adapter() :: module()\n  def adapter do\n    case Config.settings!().tracker.kind do\n      \"memory\" -> SymphonyElixir.Tracker.Memory\n      _ -> SymphonyElixir.Linear.Adapter\n    end\n  end\nend\n"
  },
  {
    "path": "elixir/lib/symphony_elixir/workflow.ex",
    "content": "defmodule SymphonyElixir.Workflow do\n  @moduledoc \"\"\"\n  Loads workflow configuration and prompt from WORKFLOW.md.\n  \"\"\"\n\n  alias SymphonyElixir.WorkflowStore\n\n  @workflow_file_name \"WORKFLOW.md\"\n\n  @spec workflow_file_path() :: Path.t()\n  def workflow_file_path do\n    Application.get_env(:symphony_elixir, :workflow_file_path) ||\n      Path.join(File.cwd!(), @workflow_file_name)\n  end\n\n  @spec set_workflow_file_path(Path.t()) :: :ok\n  def set_workflow_file_path(path) when is_binary(path) do\n    Application.put_env(:symphony_elixir, :workflow_file_path, path)\n    maybe_reload_store()\n    :ok\n  end\n\n  @spec clear_workflow_file_path() :: :ok\n  def clear_workflow_file_path do\n    Application.delete_env(:symphony_elixir, :workflow_file_path)\n    maybe_reload_store()\n    :ok\n  end\n\n  @type loaded_workflow :: %{\n          config: map(),\n          prompt: String.t(),\n          prompt_template: String.t()\n        }\n\n  @spec current() :: {:ok, loaded_workflow()} | {:error, term()}\n  def current do\n    case Process.whereis(WorkflowStore) do\n      pid when is_pid(pid) ->\n        WorkflowStore.current()\n\n      _ ->\n        load()\n    end\n  end\n\n  @spec load() :: {:ok, loaded_workflow()} | {:error, term()}\n  def load do\n    load(workflow_file_path())\n  end\n\n  @spec load(Path.t()) :: {:ok, loaded_workflow()} | {:error, term()}\n  def load(path) when is_binary(path) do\n    case File.read(path) do\n      {:ok, content} ->\n        parse(content)\n\n      {:error, reason} ->\n        {:error, {:missing_workflow_file, path, reason}}\n    end\n  end\n\n  defp parse(content) do\n    {front_matter_lines, prompt_lines} = split_front_matter(content)\n\n    case front_matter_yaml_to_map(front_matter_lines) do\n      {:ok, front_matter} ->\n        prompt = Enum.join(prompt_lines, \"\\n\") |> String.trim()\n\n        {:ok,\n         %{\n           config: front_matter,\n           prompt: prompt,\n           prompt_template: prompt\n         }}\n\n      {:error, :workflow_front_matter_not_a_map} ->\n        {:error, :workflow_front_matter_not_a_map}\n\n      {:error, reason} ->\n        {:error, {:workflow_parse_error, reason}}\n    end\n  end\n\n  defp split_front_matter(content) do\n    lines = String.split(content, ~r/\\R/, trim: false)\n\n    case lines do\n      [\"---\" | tail] ->\n        {front, rest} = Enum.split_while(tail, &(&1 != \"---\"))\n\n        case rest do\n          [\"---\" | prompt_lines] -> {front, prompt_lines}\n          _ -> {front, []}\n        end\n\n      _ ->\n        {[], lines}\n    end\n  end\n\n  defp front_matter_yaml_to_map(lines) do\n    yaml = Enum.join(lines, \"\\n\")\n\n    if String.trim(yaml) == \"\" do\n      {:ok, %{}}\n    else\n      case YamlElixir.read_from_string(yaml) do\n        {:ok, decoded} when is_map(decoded) -> {:ok, decoded}\n        {:ok, _} -> {:error, :workflow_front_matter_not_a_map}\n        {:error, reason} -> {:error, reason}\n      end\n    end\n  end\n\n  defp maybe_reload_store do\n    if Process.whereis(WorkflowStore) do\n      _ = WorkflowStore.force_reload()\n    end\n\n    :ok\n  end\nend\n"
  },
  {
    "path": "elixir/lib/symphony_elixir/workflow_store.ex",
    "content": "defmodule SymphonyElixir.WorkflowStore do\n  @moduledoc \"\"\"\n  Caches the last known good workflow and reloads it when `WORKFLOW.md` changes.\n  \"\"\"\n\n  use GenServer\n  require Logger\n\n  alias SymphonyElixir.Workflow\n\n  @poll_interval_ms 1_000\n\n  defmodule State do\n    @moduledoc false\n\n    defstruct [:path, :stamp, :workflow]\n  end\n\n  @spec start_link(keyword()) :: GenServer.on_start()\n  def start_link(opts \\\\ []) do\n    GenServer.start_link(__MODULE__, opts, name: __MODULE__)\n  end\n\n  @spec current() :: {:ok, Workflow.loaded_workflow()} | {:error, term()}\n  def current do\n    case Process.whereis(__MODULE__) do\n      pid when is_pid(pid) ->\n        GenServer.call(__MODULE__, :current)\n\n      _ ->\n        Workflow.load()\n    end\n  end\n\n  @spec force_reload() :: :ok | {:error, term()}\n  def force_reload do\n    case Process.whereis(__MODULE__) do\n      pid when is_pid(pid) ->\n        GenServer.call(__MODULE__, :force_reload)\n\n      _ ->\n        case Workflow.load() do\n          {:ok, _workflow} -> :ok\n          {:error, reason} -> {:error, reason}\n        end\n    end\n  end\n\n  @impl true\n  def init(_opts) do\n    case load_state(Workflow.workflow_file_path()) do\n      {:ok, state} ->\n        schedule_poll()\n        {:ok, state}\n\n      {:error, reason} ->\n        {:stop, reason}\n    end\n  end\n\n  @impl true\n  def handle_call(:current, _from, %State{} = state) do\n    case reload_state(state) do\n      {:ok, new_state} ->\n        {:reply, {:ok, new_state.workflow}, new_state}\n\n      {:error, _reason, new_state} ->\n        {:reply, {:ok, new_state.workflow}, new_state}\n    end\n  end\n\n  def handle_call(:force_reload, _from, %State{} = state) do\n    case reload_state(state) do\n      {:ok, new_state} ->\n        {:reply, :ok, new_state}\n\n      {:error, reason, new_state} ->\n        {:reply, {:error, reason}, new_state}\n    end\n  end\n\n  @impl true\n  def handle_info(:poll, %State{} = state) do\n    schedule_poll()\n\n    case reload_state(state) do\n      {:ok, new_state} -> {:noreply, new_state}\n      {:error, _reason, new_state} -> {:noreply, new_state}\n    end\n  end\n\n  defp schedule_poll do\n    Process.send_after(self(), :poll, @poll_interval_ms)\n  end\n\n  defp reload_state(%State{} = state) do\n    path = Workflow.workflow_file_path()\n\n    if path != state.path do\n      reload_path(path, state)\n    else\n      reload_current_path(path, state)\n    end\n  end\n\n  defp reload_path(path, state) do\n    case load_state(path) do\n      {:ok, new_state} ->\n        {:ok, new_state}\n\n      {:error, reason} ->\n        log_reload_error(path, reason)\n        {:error, reason, state}\n    end\n  end\n\n  defp reload_current_path(path, state) do\n    case current_stamp(path) do\n      {:ok, stamp} when stamp == state.stamp ->\n        {:ok, state}\n\n      {:ok, _stamp} ->\n        reload_path(path, state)\n\n      {:error, reason} ->\n        log_reload_error(path, reason)\n        {:error, reason, state}\n    end\n  end\n\n  defp load_state(path) do\n    with {:ok, workflow} <- Workflow.load(path),\n         {:ok, stamp} <- current_stamp(path) do\n      {:ok, %State{path: path, stamp: stamp, workflow: workflow}}\n    else\n      {:error, reason} ->\n        {:error, reason}\n    end\n  end\n\n  defp current_stamp(path) when is_binary(path) do\n    with {:ok, stat} <- File.stat(path, time: :posix),\n         {:ok, content} <- File.read(path) do\n      {:ok, {stat.mtime, stat.size, :erlang.phash2(content)}}\n    else\n      {:error, reason} -> {:error, reason}\n    end\n  end\n\n  defp log_reload_error(path, reason) do\n    Logger.error(\"Failed to reload workflow path=#{path} reason=#{inspect(reason)}; keeping last known good configuration\")\n  end\nend\n"
  },
  {
    "path": "elixir/lib/symphony_elixir/workspace.ex",
    "content": "defmodule SymphonyElixir.Workspace do\n  @moduledoc \"\"\"\n  Creates isolated per-issue workspaces for parallel Codex agents.\n  \"\"\"\n\n  require Logger\n  alias SymphonyElixir.{Config, PathSafety, SSH}\n\n  @remote_workspace_marker \"__SYMPHONY_WORKSPACE__\"\n\n  @type worker_host :: String.t() | nil\n\n  @spec create_for_issue(map() | String.t() | nil, worker_host()) ::\n          {:ok, Path.t()} | {:error, term()}\n  def create_for_issue(issue_or_identifier, worker_host \\\\ nil) do\n    issue_context = issue_context(issue_or_identifier)\n\n    try do\n      safe_id = safe_identifier(issue_context.issue_identifier)\n\n      with {:ok, workspace} <- workspace_path_for_issue(safe_id, worker_host),\n           :ok <- validate_workspace_path(workspace, worker_host),\n           {:ok, workspace, created?} <- ensure_workspace(workspace, worker_host),\n           :ok <- maybe_run_after_create_hook(workspace, issue_context, created?, worker_host) do\n        {:ok, workspace}\n      end\n    rescue\n      error in [ArgumentError, ErlangError, File.Error] ->\n        Logger.error(\"Workspace creation failed #{issue_log_context(issue_context)} worker_host=#{worker_host_for_log(worker_host)} error=#{Exception.message(error)}\")\n        {:error, error}\n    end\n  end\n\n  defp ensure_workspace(workspace, nil) do\n    cond do\n      File.dir?(workspace) ->\n        {:ok, workspace, false}\n\n      File.exists?(workspace) ->\n        File.rm_rf!(workspace)\n        create_workspace(workspace)\n\n      true ->\n        create_workspace(workspace)\n    end\n  end\n\n  defp ensure_workspace(workspace, worker_host) when is_binary(worker_host) do\n    script =\n      [\n        \"set -eu\",\n        remote_shell_assign(\"workspace\", workspace),\n        \"if [ -d \\\"$workspace\\\" ]; then\",\n        \"  created=0\",\n        \"elif [ -e \\\"$workspace\\\" ]; then\",\n        \"  rm -rf \\\"$workspace\\\"\",\n        \"  mkdir -p \\\"$workspace\\\"\",\n        \"  created=1\",\n        \"else\",\n        \"  mkdir -p \\\"$workspace\\\"\",\n        \"  created=1\",\n        \"fi\",\n        \"cd \\\"$workspace\\\"\",\n        \"printf '%s\\\\t%s\\\\t%s\\\\n' '#{@remote_workspace_marker}' \\\"$created\\\" \\\"$(pwd -P)\\\"\"\n      ]\n      |> Enum.reject(&(&1 == \"\"))\n      |> Enum.join(\"\\n\")\n\n    case run_remote_command(worker_host, script, Config.settings!().hooks.timeout_ms) do\n      {:ok, {output, 0}} ->\n        parse_remote_workspace_output(output)\n\n      {:ok, {output, status}} ->\n        {:error, {:workspace_prepare_failed, worker_host, status, output}}\n\n      {:error, reason} ->\n        {:error, reason}\n    end\n  end\n\n  defp create_workspace(workspace) do\n    File.rm_rf!(workspace)\n    File.mkdir_p!(workspace)\n    {:ok, workspace, true}\n  end\n\n  @spec remove(Path.t()) :: {:ok, [String.t()]} | {:error, term(), String.t()}\n  def remove(workspace), do: remove(workspace, nil)\n\n  @spec remove(Path.t(), worker_host()) :: {:ok, [String.t()]} | {:error, term(), String.t()}\n  def remove(workspace, nil) do\n    case File.exists?(workspace) do\n      true ->\n        case validate_workspace_path(workspace, nil) do\n          :ok ->\n            maybe_run_before_remove_hook(workspace, nil)\n            File.rm_rf(workspace)\n\n          {:error, reason} ->\n            {:error, reason, \"\"}\n        end\n\n      false ->\n        File.rm_rf(workspace)\n    end\n  end\n\n  def remove(workspace, worker_host) when is_binary(worker_host) do\n    maybe_run_before_remove_hook(workspace, worker_host)\n\n    script =\n      [\n        remote_shell_assign(\"workspace\", workspace),\n        \"rm -rf \\\"$workspace\\\"\"\n      ]\n      |> Enum.join(\"\\n\")\n\n    case run_remote_command(worker_host, script, Config.settings!().hooks.timeout_ms) do\n      {:ok, {_output, 0}} ->\n        {:ok, []}\n\n      {:ok, {output, status}} ->\n        {:error, {:workspace_remove_failed, worker_host, status, output}, \"\"}\n\n      {:error, reason} ->\n        {:error, reason, \"\"}\n    end\n  end\n\n  @spec remove_issue_workspaces(term()) :: :ok\n  def remove_issue_workspaces(identifier), do: remove_issue_workspaces(identifier, nil)\n\n  @spec remove_issue_workspaces(term(), worker_host()) :: :ok\n  def remove_issue_workspaces(identifier, worker_host) when is_binary(identifier) and is_binary(worker_host) do\n    safe_id = safe_identifier(identifier)\n\n    case workspace_path_for_issue(safe_id, worker_host) do\n      {:ok, workspace} -> remove(workspace, worker_host)\n      {:error, _reason} -> :ok\n    end\n\n    :ok\n  end\n\n  def remove_issue_workspaces(identifier, nil) when is_binary(identifier) do\n    safe_id = safe_identifier(identifier)\n\n    case Config.settings!().worker.ssh_hosts do\n      [] ->\n        case workspace_path_for_issue(safe_id, nil) do\n          {:ok, workspace} -> remove(workspace, nil)\n          {:error, _reason} -> :ok\n        end\n\n      worker_hosts ->\n        Enum.each(worker_hosts, &remove_issue_workspaces(identifier, &1))\n    end\n\n    :ok\n  end\n\n  def remove_issue_workspaces(_identifier, _worker_host) do\n    :ok\n  end\n\n  @spec run_before_run_hook(Path.t(), map() | String.t() | nil, worker_host()) ::\n          :ok | {:error, term()}\n  def run_before_run_hook(workspace, issue_or_identifier, worker_host \\\\ nil) when is_binary(workspace) do\n    issue_context = issue_context(issue_or_identifier)\n    hooks = Config.settings!().hooks\n\n    case hooks.before_run do\n      nil ->\n        :ok\n\n      command ->\n        run_hook(command, workspace, issue_context, \"before_run\", worker_host)\n    end\n  end\n\n  @spec run_after_run_hook(Path.t(), map() | String.t() | nil, worker_host()) :: :ok\n  def run_after_run_hook(workspace, issue_or_identifier, worker_host \\\\ nil) when is_binary(workspace) do\n    issue_context = issue_context(issue_or_identifier)\n    hooks = Config.settings!().hooks\n\n    case hooks.after_run do\n      nil ->\n        :ok\n\n      command ->\n        run_hook(command, workspace, issue_context, \"after_run\", worker_host)\n        |> ignore_hook_failure()\n    end\n  end\n\n  defp workspace_path_for_issue(safe_id, nil) when is_binary(safe_id) do\n    Config.settings!().workspace.root\n    |> Path.join(safe_id)\n    |> PathSafety.canonicalize()\n  end\n\n  defp workspace_path_for_issue(safe_id, worker_host) when is_binary(safe_id) and is_binary(worker_host) do\n    {:ok, Path.join(Config.settings!().workspace.root, safe_id)}\n  end\n\n  defp safe_identifier(identifier) do\n    String.replace(identifier || \"issue\", ~r/[^a-zA-Z0-9._-]/, \"_\")\n  end\n\n  defp maybe_run_after_create_hook(workspace, issue_context, created?, worker_host) do\n    hooks = Config.settings!().hooks\n\n    case created? do\n      true ->\n        case hooks.after_create do\n          nil ->\n            :ok\n\n          command ->\n            run_hook(command, workspace, issue_context, \"after_create\", worker_host)\n        end\n\n      false ->\n        :ok\n    end\n  end\n\n  defp maybe_run_before_remove_hook(workspace, nil) do\n    hooks = Config.settings!().hooks\n\n    case File.dir?(workspace) do\n      true ->\n        case hooks.before_remove do\n          nil ->\n            :ok\n\n          command ->\n            run_hook(\n              command,\n              workspace,\n              %{issue_id: nil, issue_identifier: Path.basename(workspace)},\n              \"before_remove\",\n              nil\n            )\n            |> ignore_hook_failure()\n        end\n\n      false ->\n        :ok\n    end\n  end\n\n  defp maybe_run_before_remove_hook(workspace, worker_host) when is_binary(worker_host) do\n    hooks = Config.settings!().hooks\n\n    case hooks.before_remove do\n      nil ->\n        :ok\n\n      command ->\n        script =\n          [\n            remote_shell_assign(\"workspace\", workspace),\n            \"if [ -d \\\"$workspace\\\" ]; then\",\n            \"  cd \\\"$workspace\\\"\",\n            \"  #{command}\",\n            \"fi\"\n          ]\n          |> Enum.join(\"\\n\")\n\n        run_remote_command(worker_host, script, Config.settings!().hooks.timeout_ms)\n        |> case do\n          {:ok, {output, status}} ->\n            handle_hook_command_result(\n              {output, status},\n              workspace,\n              %{issue_id: nil, issue_identifier: Path.basename(workspace)},\n              \"before_remove\"\n            )\n\n          {:error, {:workspace_hook_timeout, \"before_remove\", _timeout_ms} = reason} ->\n            {:error, reason}\n\n          {:error, reason} ->\n            {:error, reason}\n        end\n        |> ignore_hook_failure()\n    end\n  end\n\n  defp ignore_hook_failure(:ok), do: :ok\n  defp ignore_hook_failure({:error, _reason}), do: :ok\n\n  defp run_hook(command, workspace, issue_context, hook_name, nil) do\n    timeout_ms = Config.settings!().hooks.timeout_ms\n\n    Logger.info(\"Running workspace hook hook=#{hook_name} #{issue_log_context(issue_context)} workspace=#{workspace} worker_host=local\")\n\n    task =\n      Task.async(fn ->\n        System.cmd(\"sh\", [\"-lc\", command], cd: workspace, stderr_to_stdout: true)\n      end)\n\n    case Task.yield(task, timeout_ms) do\n      {:ok, cmd_result} ->\n        handle_hook_command_result(cmd_result, workspace, issue_context, hook_name)\n\n      nil ->\n        Task.shutdown(task, :brutal_kill)\n\n        Logger.warning(\"Workspace hook timed out hook=#{hook_name} #{issue_log_context(issue_context)} workspace=#{workspace} worker_host=local timeout_ms=#{timeout_ms}\")\n\n        {:error, {:workspace_hook_timeout, hook_name, timeout_ms}}\n    end\n  end\n\n  defp run_hook(command, workspace, issue_context, hook_name, worker_host) when is_binary(worker_host) do\n    timeout_ms = Config.settings!().hooks.timeout_ms\n\n    Logger.info(\"Running workspace hook hook=#{hook_name} #{issue_log_context(issue_context)} workspace=#{workspace} worker_host=#{worker_host}\")\n\n    case run_remote_command(worker_host, \"cd #{shell_escape(workspace)} && #{command}\", timeout_ms) do\n      {:ok, cmd_result} ->\n        handle_hook_command_result(cmd_result, workspace, issue_context, hook_name)\n\n      {:error, {:workspace_hook_timeout, ^hook_name, _timeout_ms} = reason} ->\n        {:error, reason}\n\n      {:error, reason} ->\n        {:error, reason}\n    end\n  end\n\n  defp handle_hook_command_result({_output, 0}, _workspace, _issue_id, _hook_name) do\n    :ok\n  end\n\n  defp handle_hook_command_result({output, status}, workspace, issue_context, hook_name) do\n    sanitized_output = sanitize_hook_output_for_log(output)\n\n    Logger.warning(\"Workspace hook failed hook=#{hook_name} #{issue_log_context(issue_context)} workspace=#{workspace} status=#{status} output=#{inspect(sanitized_output)}\")\n\n    {:error, {:workspace_hook_failed, hook_name, status, output}}\n  end\n\n  defp sanitize_hook_output_for_log(output, max_bytes \\\\ 2_048) do\n    binary_output = IO.iodata_to_binary(output)\n\n    case byte_size(binary_output) <= max_bytes do\n      true ->\n        binary_output\n\n      false ->\n        binary_part(binary_output, 0, max_bytes) <> \"... (truncated)\"\n    end\n  end\n\n  defp validate_workspace_path(workspace, nil) when is_binary(workspace) do\n    expanded_workspace = Path.expand(workspace)\n    expanded_root = Path.expand(Config.settings!().workspace.root)\n    expanded_root_prefix = expanded_root <> \"/\"\n\n    with {:ok, canonical_workspace} <- PathSafety.canonicalize(expanded_workspace),\n         {:ok, canonical_root} <- PathSafety.canonicalize(expanded_root) do\n      canonical_root_prefix = canonical_root <> \"/\"\n\n      cond do\n        canonical_workspace == canonical_root ->\n          {:error, {:workspace_equals_root, canonical_workspace, canonical_root}}\n\n        String.starts_with?(canonical_workspace <> \"/\", canonical_root_prefix) ->\n          :ok\n\n        String.starts_with?(expanded_workspace <> \"/\", expanded_root_prefix) ->\n          {:error, {:workspace_symlink_escape, expanded_workspace, canonical_root}}\n\n        true ->\n          {:error, {:workspace_outside_root, canonical_workspace, canonical_root}}\n      end\n    else\n      {:error, {:path_canonicalize_failed, path, reason}} ->\n        {:error, {:workspace_path_unreadable, path, reason}}\n    end\n  end\n\n  defp validate_workspace_path(workspace, worker_host)\n       when is_binary(workspace) and is_binary(worker_host) do\n    cond do\n      String.trim(workspace) == \"\" ->\n        {:error, {:workspace_path_unreadable, workspace, :empty}}\n\n      String.contains?(workspace, [\"\\n\", \"\\r\", <<0>>]) ->\n        {:error, {:workspace_path_unreadable, workspace, :invalid_characters}}\n\n      true ->\n        :ok\n    end\n  end\n\n  defp remote_shell_assign(variable_name, raw_path)\n       when is_binary(variable_name) and is_binary(raw_path) do\n    [\n      \"#{variable_name}=#{shell_escape(raw_path)}\",\n      \"case \\\"$#{variable_name}\\\" in\",\n      \"  '~') #{variable_name}=\\\"$HOME\\\" ;;\",\n      \"  '~/'*) \" <> variable_name <> \"=\\\"$HOME/${\" <> variable_name <> \"#~/}\\\" ;;\",\n      \"esac\"\n    ]\n    |> Enum.join(\"\\n\")\n  end\n\n  defp parse_remote_workspace_output(output) do\n    lines = String.split(IO.iodata_to_binary(output), \"\\n\", trim: true)\n\n    payload =\n      Enum.find_value(lines, fn line ->\n        case String.split(line, \"\\t\", parts: 3) do\n          [@remote_workspace_marker, created, path] when created in [\"0\", \"1\"] and path != \"\" ->\n            {created == \"1\", path}\n\n          _ ->\n            nil\n        end\n      end)\n\n    case payload do\n      {created?, workspace} when is_boolean(created?) and is_binary(workspace) ->\n        {:ok, workspace, created?}\n\n      _ ->\n        {:error, {:workspace_prepare_failed, :invalid_output, output}}\n    end\n  end\n\n  defp run_remote_command(worker_host, script, timeout_ms)\n       when is_binary(worker_host) and is_binary(script) and is_integer(timeout_ms) and timeout_ms > 0 do\n    task =\n      Task.async(fn ->\n        SSH.run(worker_host, script, stderr_to_stdout: true)\n      end)\n\n    case Task.yield(task, timeout_ms) do\n      {:ok, result} ->\n        result\n\n      nil ->\n        Task.shutdown(task, :brutal_kill)\n        {:error, {:workspace_hook_timeout, \"remote_command\", timeout_ms}}\n    end\n  end\n\n  defp shell_escape(value) when is_binary(value) do\n    \"'\" <> String.replace(value, \"'\", \"'\\\"'\\\"'\") <> \"'\"\n  end\n\n  defp worker_host_for_log(nil), do: \"local\"\n  defp worker_host_for_log(worker_host), do: worker_host\n\n  defp issue_context(%{id: issue_id, identifier: identifier}) do\n    %{\n      issue_id: issue_id,\n      issue_identifier: identifier || \"issue\"\n    }\n  end\n\n  defp issue_context(identifier) when is_binary(identifier) do\n    %{\n      issue_id: nil,\n      issue_identifier: identifier\n    }\n  end\n\n  defp issue_context(_identifier) do\n    %{\n      issue_id: nil,\n      issue_identifier: \"issue\"\n    }\n  end\n\n  defp issue_log_context(%{issue_id: issue_id, issue_identifier: issue_identifier}) do\n    \"issue_id=#{issue_id || \"n/a\"} issue_identifier=#{issue_identifier || \"issue\"}\"\n  end\nend\n"
  },
  {
    "path": "elixir/lib/symphony_elixir.ex",
    "content": "defmodule SymphonyElixir do\n  @moduledoc \"\"\"\n  Entry point for the Symphony orchestrator.\n  \"\"\"\n\n  @doc \"\"\"\n  Start the orchestrator in the current BEAM node.\n  \"\"\"\n  @spec start_link(keyword()) :: GenServer.on_start()\n  def start_link(opts \\\\ []) do\n    SymphonyElixir.Orchestrator.start_link(opts)\n  end\nend\n\ndefmodule SymphonyElixir.Application do\n  @moduledoc \"\"\"\n  OTP application entrypoint that starts core supervisors and workers.\n  \"\"\"\n\n  use Application\n\n  @impl true\n  def start(_type, _args) do\n    :ok = SymphonyElixir.LogFile.configure()\n\n    children = [\n      {Phoenix.PubSub, name: SymphonyElixir.PubSub},\n      {Task.Supervisor, name: SymphonyElixir.TaskSupervisor},\n      SymphonyElixir.WorkflowStore,\n      SymphonyElixir.Orchestrator,\n      SymphonyElixir.HttpServer,\n      SymphonyElixir.StatusDashboard\n    ]\n\n    Supervisor.start_link(\n      children,\n      strategy: :one_for_one,\n      name: SymphonyElixir.Supervisor\n    )\n  end\n\n  @impl true\n  def stop(_state) do\n    SymphonyElixir.StatusDashboard.render_offline_status()\n    :ok\n  end\nend\n"
  },
  {
    "path": "elixir/lib/symphony_elixir_web/components/layouts.ex",
    "content": "defmodule SymphonyElixirWeb.Layouts do\n  @moduledoc \"\"\"\n  Shared layouts for the observability dashboard.\n  \"\"\"\n\n  use Phoenix.Component\n\n  @spec root(map()) :: Phoenix.LiveView.Rendered.t()\n  def root(assigns) do\n    assigns = assign(assigns, :csrf_token, Plug.CSRFProtection.get_csrf_token())\n\n    ~H\"\"\"\n    <!DOCTYPE html>\n    <html lang=\"en\">\n      <head>\n        <meta charset=\"utf-8\" />\n        <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n        <meta name=\"csrf-token\" content={@csrf_token} />\n        <title>Symphony Observability</title>\n        <script defer src=\"/vendor/phoenix_html/phoenix_html.js\"></script>\n        <script defer src=\"/vendor/phoenix/phoenix.js\"></script>\n        <script defer src=\"/vendor/phoenix_live_view/phoenix_live_view.js\"></script>\n        <script>\n          window.addEventListener(\"DOMContentLoaded\", function () {\n            var csrfToken = document\n              .querySelector(\"meta[name='csrf-token']\")\n              ?.getAttribute(\"content\");\n\n            if (!window.Phoenix || !window.LiveView) return;\n\n            var liveSocket = new window.LiveView.LiveSocket(\"/live\", window.Phoenix.Socket, {\n              params: {_csrf_token: csrfToken}\n            });\n\n            liveSocket.connect();\n            window.liveSocket = liveSocket;\n          });\n        </script>\n        <link rel=\"stylesheet\" href=\"/dashboard.css\" />\n      </head>\n      <body>\n        {@inner_content}\n      </body>\n    </html>\n    \"\"\"\n  end\n\n  @spec app(map()) :: Phoenix.LiveView.Rendered.t()\n  def app(assigns) do\n    ~H\"\"\"\n    <main class=\"app-shell\">\n      {@inner_content}\n    </main>\n    \"\"\"\n  end\nend\n"
  },
  {
    "path": "elixir/lib/symphony_elixir_web/controllers/observability_api_controller.ex",
    "content": "defmodule SymphonyElixirWeb.ObservabilityApiController do\n  @moduledoc \"\"\"\n  JSON API for Symphony observability data.\n  \"\"\"\n\n  use Phoenix.Controller, formats: [:json]\n\n  alias Plug.Conn\n  alias SymphonyElixirWeb.{Endpoint, Presenter}\n\n  @spec state(Conn.t(), map()) :: Conn.t()\n  def state(conn, _params) do\n    json(conn, Presenter.state_payload(orchestrator(), snapshot_timeout_ms()))\n  end\n\n  @spec issue(Conn.t(), map()) :: Conn.t()\n  def issue(conn, %{\"issue_identifier\" => issue_identifier}) do\n    case Presenter.issue_payload(issue_identifier, orchestrator(), snapshot_timeout_ms()) do\n      {:ok, payload} ->\n        json(conn, payload)\n\n      {:error, :issue_not_found} ->\n        error_response(conn, 404, \"issue_not_found\", \"Issue not found\")\n    end\n  end\n\n  @spec refresh(Conn.t(), map()) :: Conn.t()\n  def refresh(conn, _params) do\n    case Presenter.refresh_payload(orchestrator()) do\n      {:ok, payload} ->\n        conn\n        |> put_status(202)\n        |> json(payload)\n\n      {:error, :unavailable} ->\n        error_response(conn, 503, \"orchestrator_unavailable\", \"Orchestrator is unavailable\")\n    end\n  end\n\n  @spec method_not_allowed(Conn.t(), map()) :: Conn.t()\n  def method_not_allowed(conn, _params) do\n    error_response(conn, 405, \"method_not_allowed\", \"Method not allowed\")\n  end\n\n  @spec not_found(Conn.t(), map()) :: Conn.t()\n  def not_found(conn, _params) do\n    error_response(conn, 404, \"not_found\", \"Route not found\")\n  end\n\n  defp error_response(conn, status, code, message) do\n    conn\n    |> put_status(status)\n    |> json(%{error: %{code: code, message: message}})\n  end\n\n  defp orchestrator do\n    Endpoint.config(:orchestrator) || SymphonyElixir.Orchestrator\n  end\n\n  defp snapshot_timeout_ms do\n    Endpoint.config(:snapshot_timeout_ms) || 15_000\n  end\nend\n"
  },
  {
    "path": "elixir/lib/symphony_elixir_web/controllers/static_asset_controller.ex",
    "content": "defmodule SymphonyElixirWeb.StaticAssetController do\n  @moduledoc \"\"\"\n  Serves the dashboard's embedded CSS and JavaScript assets.\n  \"\"\"\n\n  use Phoenix.Controller, formats: []\n\n  alias Plug.Conn\n  alias SymphonyElixirWeb.StaticAssets\n\n  @spec dashboard_css(Conn.t(), map()) :: Conn.t()\n  def dashboard_css(conn, _params), do: serve(conn, \"/dashboard.css\")\n\n  @spec phoenix_html_js(Conn.t(), map()) :: Conn.t()\n  def phoenix_html_js(conn, _params), do: serve(conn, \"/vendor/phoenix_html/phoenix_html.js\")\n\n  @spec phoenix_js(Conn.t(), map()) :: Conn.t()\n  def phoenix_js(conn, _params), do: serve(conn, \"/vendor/phoenix/phoenix.js\")\n\n  @spec phoenix_live_view_js(Conn.t(), map()) :: Conn.t()\n  def phoenix_live_view_js(conn, _params), do: serve(conn, \"/vendor/phoenix_live_view/phoenix_live_view.js\")\n\n  defp serve(conn, path) do\n    case StaticAssets.fetch(path) do\n      {:ok, content_type, body} ->\n        conn\n        |> put_resp_content_type(content_type)\n        |> put_resp_header(\"cache-control\", \"public, max-age=31536000\")\n        |> send_resp(200, body)\n\n      :error ->\n        send_resp(conn, 404, \"Not Found\")\n    end\n  end\nend\n"
  },
  {
    "path": "elixir/lib/symphony_elixir_web/endpoint.ex",
    "content": "defmodule SymphonyElixirWeb.Endpoint do\n  @moduledoc \"\"\"\n  Phoenix endpoint for Symphony's optional observability UI and API.\n  \"\"\"\n\n  use Phoenix.Endpoint, otp_app: :symphony_elixir\n\n  @session_options [\n    store: :cookie,\n    key: \"_symphony_elixir_key\",\n    signing_salt: \"symphony-session\"\n  ]\n\n  socket(\"/live\", Phoenix.LiveView.Socket,\n    websocket: [connect_info: [session: @session_options]],\n    longpoll: false\n  )\n\n  plug(Plug.RequestId)\n  plug(Plug.Telemetry, event_prefix: [:phoenix, :endpoint])\n\n  plug(Plug.Parsers,\n    parsers: [:urlencoded, :multipart, :json],\n    pass: [\"*/*\"],\n    json_decoder: Jason\n  )\n\n  plug(Plug.MethodOverride)\n  plug(Plug.Head)\n  plug(Plug.Session, @session_options)\n  plug(SymphonyElixirWeb.Router)\nend\n"
  },
  {
    "path": "elixir/lib/symphony_elixir_web/error_html.ex",
    "content": "defmodule SymphonyElixirWeb.ErrorHTML do\n  @moduledoc false\n\n  @spec render(String.t(), map()) :: String.t()\n  def render(template, _assigns) do\n    Phoenix.Controller.status_message_from_template(template)\n  end\nend\n"
  },
  {
    "path": "elixir/lib/symphony_elixir_web/error_json.ex",
    "content": "defmodule SymphonyElixirWeb.ErrorJSON do\n  @moduledoc false\n\n  @spec render(String.t(), map()) :: map()\n  def render(template, _assigns) do\n    %{error: %{code: \"request_failed\", message: Phoenix.Controller.status_message_from_template(template)}}\n  end\nend\n"
  },
  {
    "path": "elixir/lib/symphony_elixir_web/live/dashboard_live.ex",
    "content": "defmodule SymphonyElixirWeb.DashboardLive do\n  @moduledoc \"\"\"\n  Live observability dashboard for Symphony.\n  \"\"\"\n\n  use Phoenix.LiveView, layout: {SymphonyElixirWeb.Layouts, :app}\n\n  alias SymphonyElixirWeb.{Endpoint, ObservabilityPubSub, Presenter}\n  @runtime_tick_ms 1_000\n\n  @impl true\n  def mount(_params, _session, socket) do\n    socket =\n      socket\n      |> assign(:payload, load_payload())\n      |> assign(:now, DateTime.utc_now())\n\n    if connected?(socket) do\n      :ok = ObservabilityPubSub.subscribe()\n      schedule_runtime_tick()\n    end\n\n    {:ok, socket}\n  end\n\n  @impl true\n  def handle_info(:runtime_tick, socket) do\n    schedule_runtime_tick()\n    {:noreply, assign(socket, :now, DateTime.utc_now())}\n  end\n\n  @impl true\n  def handle_info(:observability_updated, socket) do\n    {:noreply,\n     socket\n     |> assign(:payload, load_payload())\n     |> assign(:now, DateTime.utc_now())}\n  end\n\n  @impl true\n  def render(assigns) do\n    ~H\"\"\"\n    <section class=\"dashboard-shell\">\n      <header class=\"hero-card\">\n        <div class=\"hero-grid\">\n          <div>\n            <p class=\"eyebrow\">\n              Symphony Observability\n            </p>\n            <h1 class=\"hero-title\">\n              Operations Dashboard\n            </h1>\n            <p class=\"hero-copy\">\n              Current state, retry pressure, token usage, and orchestration health for the active Symphony runtime.\n            </p>\n          </div>\n\n          <div class=\"status-stack\">\n            <span class=\"status-badge status-badge-live\">\n              <span class=\"status-badge-dot\"></span>\n              Live\n            </span>\n            <span class=\"status-badge status-badge-offline\">\n              <span class=\"status-badge-dot\"></span>\n              Offline\n            </span>\n          </div>\n        </div>\n      </header>\n\n      <%= if @payload[:error] do %>\n        <section class=\"error-card\">\n          <h2 class=\"error-title\">\n            Snapshot unavailable\n          </h2>\n          <p class=\"error-copy\">\n            <strong><%= @payload.error.code %>:</strong> <%= @payload.error.message %>\n          </p>\n        </section>\n      <% else %>\n        <section class=\"metric-grid\">\n          <article class=\"metric-card\">\n            <p class=\"metric-label\">Running</p>\n            <p class=\"metric-value numeric\"><%= @payload.counts.running %></p>\n            <p class=\"metric-detail\">Active issue sessions in the current runtime.</p>\n          </article>\n\n          <article class=\"metric-card\">\n            <p class=\"metric-label\">Retrying</p>\n            <p class=\"metric-value numeric\"><%= @payload.counts.retrying %></p>\n            <p class=\"metric-detail\">Issues waiting for the next retry window.</p>\n          </article>\n\n          <article class=\"metric-card\">\n            <p class=\"metric-label\">Total tokens</p>\n            <p class=\"metric-value numeric\"><%= format_int(@payload.codex_totals.total_tokens) %></p>\n            <p class=\"metric-detail numeric\">\n              In <%= format_int(@payload.codex_totals.input_tokens) %> / Out <%= format_int(@payload.codex_totals.output_tokens) %>\n            </p>\n          </article>\n\n          <article class=\"metric-card\">\n            <p class=\"metric-label\">Runtime</p>\n            <p class=\"metric-value numeric\"><%= format_runtime_seconds(total_runtime_seconds(@payload, @now)) %></p>\n            <p class=\"metric-detail\">Total Codex runtime across completed and active sessions.</p>\n          </article>\n        </section>\n\n        <section class=\"section-card\">\n          <div class=\"section-header\">\n            <div>\n              <h2 class=\"section-title\">Rate limits</h2>\n              <p class=\"section-copy\">Latest upstream rate-limit snapshot, when available.</p>\n            </div>\n          </div>\n\n          <pre class=\"code-panel\"><%= pretty_value(@payload.rate_limits) %></pre>\n        </section>\n\n        <section class=\"section-card\">\n          <div class=\"section-header\">\n            <div>\n              <h2 class=\"section-title\">Running sessions</h2>\n              <p class=\"section-copy\">Active issues, last known agent activity, and token usage.</p>\n            </div>\n          </div>\n\n          <%= if @payload.running == [] do %>\n            <p class=\"empty-state\">No active sessions.</p>\n          <% else %>\n            <div class=\"table-wrap\">\n              <table class=\"data-table data-table-running\">\n                <colgroup>\n                  <col style=\"width: 12rem;\" />\n                  <col style=\"width: 8rem;\" />\n                  <col style=\"width: 7.5rem;\" />\n                  <col style=\"width: 8.5rem;\" />\n                  <col />\n                  <col style=\"width: 10rem;\" />\n                </colgroup>\n                <thead>\n                  <tr>\n                    <th>Issue</th>\n                    <th>State</th>\n                    <th>Session</th>\n                    <th>Runtime / turns</th>\n                    <th>Codex update</th>\n                    <th>Tokens</th>\n                  </tr>\n                </thead>\n                <tbody>\n                  <tr :for={entry <- @payload.running}>\n                    <td>\n                      <div class=\"issue-stack\">\n                        <span class=\"issue-id\"><%= entry.issue_identifier %></span>\n                        <a class=\"issue-link\" href={\"/api/v1/#{entry.issue_identifier}\"}>JSON details</a>\n                      </div>\n                    </td>\n                    <td>\n                      <span class={state_badge_class(entry.state)}>\n                        <%= entry.state %>\n                      </span>\n                    </td>\n                    <td>\n                      <div class=\"session-stack\">\n                        <%= if entry.session_id do %>\n                          <button\n                            type=\"button\"\n                            class=\"subtle-button\"\n                            data-label=\"Copy ID\"\n                            data-copy={entry.session_id}\n                            onclick=\"navigator.clipboard.writeText(this.dataset.copy); this.textContent = 'Copied'; clearTimeout(this._copyTimer); this._copyTimer = setTimeout(() => { this.textContent = this.dataset.label }, 1200);\"\n                          >\n                            Copy ID\n                          </button>\n                        <% else %>\n                          <span class=\"muted\">n/a</span>\n                        <% end %>\n                      </div>\n                    </td>\n                    <td class=\"numeric\"><%= format_runtime_and_turns(entry.started_at, entry.turn_count, @now) %></td>\n                    <td>\n                      <div class=\"detail-stack\">\n                        <span\n                          class=\"event-text\"\n                          title={entry.last_message || to_string(entry.last_event || \"n/a\")}\n                        ><%= entry.last_message || to_string(entry.last_event || \"n/a\") %></span>\n                        <span class=\"muted event-meta\">\n                          <%= entry.last_event || \"n/a\" %>\n                          <%= if entry.last_event_at do %>\n                            · <span class=\"mono numeric\"><%= entry.last_event_at %></span>\n                          <% end %>\n                        </span>\n                      </div>\n                    </td>\n                    <td>\n                      <div class=\"token-stack numeric\">\n                        <span>Total: <%= format_int(entry.tokens.total_tokens) %></span>\n                        <span class=\"muted\">In <%= format_int(entry.tokens.input_tokens) %> / Out <%= format_int(entry.tokens.output_tokens) %></span>\n                      </div>\n                    </td>\n                  </tr>\n                </tbody>\n              </table>\n            </div>\n          <% end %>\n        </section>\n\n        <section class=\"section-card\">\n          <div class=\"section-header\">\n            <div>\n              <h2 class=\"section-title\">Retry queue</h2>\n              <p class=\"section-copy\">Issues waiting for the next retry window.</p>\n            </div>\n          </div>\n\n          <%= if @payload.retrying == [] do %>\n            <p class=\"empty-state\">No issues are currently backing off.</p>\n          <% else %>\n            <div class=\"table-wrap\">\n              <table class=\"data-table\" style=\"min-width: 680px;\">\n                <thead>\n                  <tr>\n                    <th>Issue</th>\n                    <th>Attempt</th>\n                    <th>Due at</th>\n                    <th>Error</th>\n                  </tr>\n                </thead>\n                <tbody>\n                  <tr :for={entry <- @payload.retrying}>\n                    <td>\n                      <div class=\"issue-stack\">\n                        <span class=\"issue-id\"><%= entry.issue_identifier %></span>\n                        <a class=\"issue-link\" href={\"/api/v1/#{entry.issue_identifier}\"}>JSON details</a>\n                      </div>\n                    </td>\n                    <td><%= entry.attempt %></td>\n                    <td class=\"mono\"><%= entry.due_at || \"n/a\" %></td>\n                    <td><%= entry.error || \"n/a\" %></td>\n                  </tr>\n                </tbody>\n              </table>\n            </div>\n          <% end %>\n        </section>\n      <% end %>\n    </section>\n    \"\"\"\n  end\n\n  defp load_payload do\n    Presenter.state_payload(orchestrator(), snapshot_timeout_ms())\n  end\n\n  defp orchestrator do\n    Endpoint.config(:orchestrator) || SymphonyElixir.Orchestrator\n  end\n\n  defp snapshot_timeout_ms do\n    Endpoint.config(:snapshot_timeout_ms) || 15_000\n  end\n\n  defp completed_runtime_seconds(payload) do\n    payload.codex_totals.seconds_running || 0\n  end\n\n  defp total_runtime_seconds(payload, now) do\n    completed_runtime_seconds(payload) +\n      Enum.reduce(payload.running, 0, fn entry, total ->\n        total + runtime_seconds_from_started_at(entry.started_at, now)\n      end)\n  end\n\n  defp format_runtime_and_turns(started_at, turn_count, now) when is_integer(turn_count) and turn_count > 0 do\n    \"#{format_runtime_seconds(runtime_seconds_from_started_at(started_at, now))} / #{turn_count}\"\n  end\n\n  defp format_runtime_and_turns(started_at, _turn_count, now),\n    do: format_runtime_seconds(runtime_seconds_from_started_at(started_at, now))\n\n  defp format_runtime_seconds(seconds) when is_number(seconds) do\n    whole_seconds = max(trunc(seconds), 0)\n    mins = div(whole_seconds, 60)\n    secs = rem(whole_seconds, 60)\n    \"#{mins}m #{secs}s\"\n  end\n\n  defp runtime_seconds_from_started_at(%DateTime{} = started_at, %DateTime{} = now) do\n    DateTime.diff(now, started_at, :second)\n  end\n\n  defp runtime_seconds_from_started_at(started_at, %DateTime{} = now) when is_binary(started_at) do\n    case DateTime.from_iso8601(started_at) do\n      {:ok, parsed, _offset} -> runtime_seconds_from_started_at(parsed, now)\n      _ -> 0\n    end\n  end\n\n  defp runtime_seconds_from_started_at(_started_at, _now), do: 0\n\n  defp format_int(value) when is_integer(value) do\n    value\n    |> Integer.to_string()\n    |> String.reverse()\n    |> String.replace(~r/.{3}(?=.)/, \"\\\\0,\")\n    |> String.reverse()\n  end\n\n  defp format_int(_value), do: \"n/a\"\n\n  defp state_badge_class(state) do\n    base = \"state-badge\"\n    normalized = state |> to_string() |> String.downcase()\n\n    cond do\n      String.contains?(normalized, [\"progress\", \"running\", \"active\"]) -> \"#{base} state-badge-active\"\n      String.contains?(normalized, [\"blocked\", \"error\", \"failed\"]) -> \"#{base} state-badge-danger\"\n      String.contains?(normalized, [\"todo\", \"queued\", \"pending\", \"retry\"]) -> \"#{base} state-badge-warning\"\n      true -> base\n    end\n  end\n\n  defp schedule_runtime_tick do\n    Process.send_after(self(), :runtime_tick, @runtime_tick_ms)\n  end\n\n  defp pretty_value(nil), do: \"n/a\"\n  defp pretty_value(value), do: inspect(value, pretty: true, limit: :infinity)\nend\n"
  },
  {
    "path": "elixir/lib/symphony_elixir_web/observability_pubsub.ex",
    "content": "defmodule SymphonyElixirWeb.ObservabilityPubSub do\n  @moduledoc \"\"\"\n  PubSub helpers for observability dashboard updates.\n  \"\"\"\n\n  @pubsub SymphonyElixir.PubSub\n  @topic \"observability:dashboard\"\n  @update_message :observability_updated\n\n  @spec subscribe() :: :ok | {:error, term()}\n  def subscribe do\n    Phoenix.PubSub.subscribe(@pubsub, @topic)\n  end\n\n  @spec broadcast_update() :: :ok\n  def broadcast_update do\n    case Process.whereis(@pubsub) do\n      pid when is_pid(pid) ->\n        Phoenix.PubSub.broadcast(@pubsub, @topic, @update_message)\n\n      _ ->\n        :ok\n    end\n  end\nend\n"
  },
  {
    "path": "elixir/lib/symphony_elixir_web/presenter.ex",
    "content": "defmodule SymphonyElixirWeb.Presenter do\n  @moduledoc \"\"\"\n  Shared projections for the observability API and dashboard.\n  \"\"\"\n\n  alias SymphonyElixir.{Config, Orchestrator, StatusDashboard}\n\n  @spec state_payload(GenServer.name(), timeout()) :: map()\n  def state_payload(orchestrator, snapshot_timeout_ms) do\n    generated_at = DateTime.utc_now() |> DateTime.truncate(:second) |> DateTime.to_iso8601()\n\n    case Orchestrator.snapshot(orchestrator, snapshot_timeout_ms) do\n      %{} = snapshot ->\n        %{\n          generated_at: generated_at,\n          counts: %{\n            running: length(snapshot.running),\n            retrying: length(snapshot.retrying)\n          },\n          running: Enum.map(snapshot.running, &running_entry_payload/1),\n          retrying: Enum.map(snapshot.retrying, &retry_entry_payload/1),\n          codex_totals: snapshot.codex_totals,\n          rate_limits: snapshot.rate_limits\n        }\n\n      :timeout ->\n        %{generated_at: generated_at, error: %{code: \"snapshot_timeout\", message: \"Snapshot timed out\"}}\n\n      :unavailable ->\n        %{generated_at: generated_at, error: %{code: \"snapshot_unavailable\", message: \"Snapshot unavailable\"}}\n    end\n  end\n\n  @spec issue_payload(String.t(), GenServer.name(), timeout()) :: {:ok, map()} | {:error, :issue_not_found}\n  def issue_payload(issue_identifier, orchestrator, snapshot_timeout_ms) when is_binary(issue_identifier) do\n    case Orchestrator.snapshot(orchestrator, snapshot_timeout_ms) do\n      %{} = snapshot ->\n        running = Enum.find(snapshot.running, &(&1.identifier == issue_identifier))\n        retry = Enum.find(snapshot.retrying, &(&1.identifier == issue_identifier))\n\n        if is_nil(running) and is_nil(retry) do\n          {:error, :issue_not_found}\n        else\n          {:ok, issue_payload_body(issue_identifier, running, retry)}\n        end\n\n      _ ->\n        {:error, :issue_not_found}\n    end\n  end\n\n  @spec refresh_payload(GenServer.name()) :: {:ok, map()} | {:error, :unavailable}\n  def refresh_payload(orchestrator) do\n    case Orchestrator.request_refresh(orchestrator) do\n      :unavailable ->\n        {:error, :unavailable}\n\n      payload ->\n        {:ok, Map.update!(payload, :requested_at, &DateTime.to_iso8601/1)}\n    end\n  end\n\n  defp issue_payload_body(issue_identifier, running, retry) do\n    %{\n      issue_identifier: issue_identifier,\n      issue_id: issue_id_from_entries(running, retry),\n      status: issue_status(running, retry),\n      workspace: %{\n        path: workspace_path(issue_identifier, running, retry),\n        host: workspace_host(running, retry)\n      },\n      attempts: %{\n        restart_count: restart_count(retry),\n        current_retry_attempt: retry_attempt(retry)\n      },\n      running: running && running_issue_payload(running),\n      retry: retry && retry_issue_payload(retry),\n      logs: %{\n        codex_session_logs: []\n      },\n      recent_events: (running && recent_events_payload(running)) || [],\n      last_error: retry && retry.error,\n      tracked: %{}\n    }\n  end\n\n  defp issue_id_from_entries(running, retry),\n    do: (running && running.issue_id) || (retry && retry.issue_id)\n\n  defp restart_count(retry), do: max(retry_attempt(retry) - 1, 0)\n  defp retry_attempt(nil), do: 0\n  defp retry_attempt(retry), do: retry.attempt || 0\n\n  defp issue_status(_running, nil), do: \"running\"\n  defp issue_status(nil, _retry), do: \"retrying\"\n  defp issue_status(_running, _retry), do: \"running\"\n\n  defp running_entry_payload(entry) do\n    %{\n      issue_id: entry.issue_id,\n      issue_identifier: entry.identifier,\n      state: entry.state,\n      worker_host: Map.get(entry, :worker_host),\n      workspace_path: Map.get(entry, :workspace_path),\n      session_id: entry.session_id,\n      turn_count: Map.get(entry, :turn_count, 0),\n      last_event: entry.last_codex_event,\n      last_message: summarize_message(entry.last_codex_message),\n      started_at: iso8601(entry.started_at),\n      last_event_at: iso8601(entry.last_codex_timestamp),\n      tokens: %{\n        input_tokens: entry.codex_input_tokens,\n        output_tokens: entry.codex_output_tokens,\n        total_tokens: entry.codex_total_tokens\n      }\n    }\n  end\n\n  defp retry_entry_payload(entry) do\n    %{\n      issue_id: entry.issue_id,\n      issue_identifier: entry.identifier,\n      attempt: entry.attempt,\n      due_at: due_at_iso8601(entry.due_in_ms),\n      error: entry.error,\n      worker_host: Map.get(entry, :worker_host),\n      workspace_path: Map.get(entry, :workspace_path)\n    }\n  end\n\n  defp running_issue_payload(running) do\n    %{\n      worker_host: Map.get(running, :worker_host),\n      workspace_path: Map.get(running, :workspace_path),\n      session_id: running.session_id,\n      turn_count: Map.get(running, :turn_count, 0),\n      state: running.state,\n      started_at: iso8601(running.started_at),\n      last_event: running.last_codex_event,\n      last_message: summarize_message(running.last_codex_message),\n      last_event_at: iso8601(running.last_codex_timestamp),\n      tokens: %{\n        input_tokens: running.codex_input_tokens,\n        output_tokens: running.codex_output_tokens,\n        total_tokens: running.codex_total_tokens\n      }\n    }\n  end\n\n  defp retry_issue_payload(retry) do\n    %{\n      attempt: retry.attempt,\n      due_at: due_at_iso8601(retry.due_in_ms),\n      error: retry.error,\n      worker_host: Map.get(retry, :worker_host),\n      workspace_path: Map.get(retry, :workspace_path)\n    }\n  end\n\n  defp workspace_path(issue_identifier, running, retry) do\n    (running && Map.get(running, :workspace_path)) ||\n      (retry && Map.get(retry, :workspace_path)) ||\n      Path.join(Config.settings!().workspace.root, issue_identifier)\n  end\n\n  defp workspace_host(running, retry) do\n    (running && Map.get(running, :worker_host)) || (retry && Map.get(retry, :worker_host))\n  end\n\n  defp recent_events_payload(running) do\n    [\n      %{\n        at: iso8601(running.last_codex_timestamp),\n        event: running.last_codex_event,\n        message: summarize_message(running.last_codex_message)\n      }\n    ]\n    |> Enum.reject(&is_nil(&1.at))\n  end\n\n  defp summarize_message(nil), do: nil\n  defp summarize_message(message), do: StatusDashboard.humanize_codex_message(message)\n\n  defp due_at_iso8601(due_in_ms) when is_integer(due_in_ms) do\n    DateTime.utc_now()\n    |> DateTime.add(div(due_in_ms, 1_000), :second)\n    |> DateTime.truncate(:second)\n    |> DateTime.to_iso8601()\n  end\n\n  defp due_at_iso8601(_due_in_ms), do: nil\n\n  defp iso8601(%DateTime{} = datetime) do\n    datetime\n    |> DateTime.truncate(:second)\n    |> DateTime.to_iso8601()\n  end\n\n  defp iso8601(_datetime), do: nil\nend\n"
  },
  {
    "path": "elixir/lib/symphony_elixir_web/router.ex",
    "content": "defmodule SymphonyElixirWeb.Router do\n  @moduledoc \"\"\"\n  Router for Symphony's observability dashboard and API.\n  \"\"\"\n\n  use Phoenix.Router\n  import Phoenix.LiveView.Router\n\n  pipeline :browser do\n    plug(:fetch_session)\n    plug(:fetch_live_flash)\n    plug(:put_root_layout, html: {SymphonyElixirWeb.Layouts, :root})\n    plug(:protect_from_forgery)\n    plug(:put_secure_browser_headers)\n  end\n\n  scope \"/\", SymphonyElixirWeb do\n    get(\"/dashboard.css\", StaticAssetController, :dashboard_css)\n    get(\"/vendor/phoenix_html/phoenix_html.js\", StaticAssetController, :phoenix_html_js)\n    get(\"/vendor/phoenix/phoenix.js\", StaticAssetController, :phoenix_js)\n    get(\"/vendor/phoenix_live_view/phoenix_live_view.js\", StaticAssetController, :phoenix_live_view_js)\n  end\n\n  scope \"/\", SymphonyElixirWeb do\n    pipe_through(:browser)\n\n    live(\"/\", DashboardLive, :index)\n  end\n\n  scope \"/\", SymphonyElixirWeb do\n    get(\"/api/v1/state\", ObservabilityApiController, :state)\n\n    match(:*, \"/\", ObservabilityApiController, :method_not_allowed)\n    match(:*, \"/api/v1/state\", ObservabilityApiController, :method_not_allowed)\n    post(\"/api/v1/refresh\", ObservabilityApiController, :refresh)\n    match(:*, \"/api/v1/refresh\", ObservabilityApiController, :method_not_allowed)\n    get(\"/api/v1/:issue_identifier\", ObservabilityApiController, :issue)\n    match(:*, \"/api/v1/:issue_identifier\", ObservabilityApiController, :method_not_allowed)\n    match(:*, \"/*path\", ObservabilityApiController, :not_found)\n  end\nend\n"
  },
  {
    "path": "elixir/lib/symphony_elixir_web/static_assets.ex",
    "content": "defmodule SymphonyElixirWeb.StaticAssets do\n  @moduledoc false\n\n  @dashboard_css_path Path.expand(\"../../priv/static/dashboard.css\", __DIR__)\n  @phoenix_html_js_path Application.app_dir(:phoenix_html, \"priv/static/phoenix_html.js\")\n  @phoenix_js_path Application.app_dir(:phoenix, \"priv/static/phoenix.js\")\n  @phoenix_live_view_js_path Application.app_dir(:phoenix_live_view, \"priv/static/phoenix_live_view.js\")\n\n  @external_resource @dashboard_css_path\n  @external_resource @phoenix_html_js_path\n  @external_resource @phoenix_js_path\n  @external_resource @phoenix_live_view_js_path\n\n  @dashboard_css File.read!(@dashboard_css_path)\n  @phoenix_html_js File.read!(@phoenix_html_js_path)\n  @phoenix_js File.read!(@phoenix_js_path)\n  @phoenix_live_view_js File.read!(@phoenix_live_view_js_path)\n\n  @assets %{\n    \"/dashboard.css\" => {\"text/css\", @dashboard_css},\n    \"/vendor/phoenix_html/phoenix_html.js\" => {\"application/javascript\", @phoenix_html_js},\n    \"/vendor/phoenix/phoenix.js\" => {\"application/javascript\", @phoenix_js},\n    \"/vendor/phoenix_live_view/phoenix_live_view.js\" => {\"application/javascript\", @phoenix_live_view_js}\n  }\n\n  @spec fetch(String.t()) :: {:ok, String.t(), binary()} | :error\n  def fetch(path) when is_binary(path) do\n    case Map.fetch(@assets, path) do\n      {:ok, {content_type, body}} -> {:ok, content_type, body}\n      :error -> :error\n    end\n  end\nend\n"
  },
  {
    "path": "elixir/mise.toml",
    "content": "[tools]\nerlang = \"28\"\nelixir = \"1.19.5-otp-28\"\n"
  },
  {
    "path": "elixir/mix.exs",
    "content": "defmodule SymphonyElixir.MixProject do\n  use Mix.Project\n\n  def project do\n    [\n      app: :symphony_elixir,\n      version: \"0.1.0\",\n      elixir: \"~> 1.19\",\n      compilers: [:phoenix_live_view] ++ Mix.compilers(),\n      start_permanent: Mix.env() == :prod,\n      test_coverage: [\n        summary: [\n          threshold: 100\n        ],\n        ignore_modules: [\n          SymphonyElixir.Config,\n          SymphonyElixir.Linear.Client,\n          SymphonyElixir.SpecsCheck,\n          SymphonyElixir.Orchestrator,\n          SymphonyElixir.Orchestrator.State,\n          SymphonyElixir.AgentRunner,\n          SymphonyElixir.CLI,\n          SymphonyElixir.Codex.AppServer,\n          SymphonyElixir.Codex.DynamicTool,\n          SymphonyElixir.HttpServer,\n          SymphonyElixir.StatusDashboard,\n          SymphonyElixir.LogFile,\n          SymphonyElixir.Workspace,\n          SymphonyElixirWeb.DashboardLive,\n          SymphonyElixirWeb.Endpoint,\n          SymphonyElixirWeb.ErrorHTML,\n          SymphonyElixirWeb.ErrorJSON,\n          SymphonyElixirWeb.Layouts,\n          SymphonyElixirWeb.ObservabilityApiController,\n          SymphonyElixirWeb.Presenter,\n          SymphonyElixirWeb.StaticAssetController,\n          SymphonyElixirWeb.StaticAssets,\n          SymphonyElixirWeb.Router,\n          SymphonyElixirWeb.Router.Helpers\n        ]\n      ],\n      test_ignore_filters: [\n        \"test/support/snapshot_support.exs\",\n        \"test/support/test_support.exs\"\n      ],\n      dialyzer: [\n        plt_add_apps: [:mix]\n      ],\n      escript: escript(),\n      aliases: aliases(),\n      deps: deps()\n    ]\n  end\n\n  # Run \"mix help compile.app\" to learn about applications.\n  def application do\n    [\n      mod: {SymphonyElixir.Application, []},\n      extra_applications: [:logger]\n    ]\n  end\n\n  # Run \"mix help deps\" to learn about dependencies.\n  defp deps do\n    [\n      {:bandit, \"~> 1.8\"},\n      {:floki, \">= 0.30.0\", only: :test},\n      {:lazy_html, \">= 0.1.0\", only: :test},\n      {:phoenix, \"~> 1.8.0\"},\n      {:phoenix_html, \"~> 4.2\"},\n      {:phoenix_live_view, \"~> 1.1.0\"},\n      {:req, \"~> 0.5\"},\n      {:jason, \"~> 1.4\"},\n      {:yaml_elixir, \"~> 2.12\"},\n      {:solid, \"~> 1.2\"},\n      {:ecto, \"~> 3.13\"},\n      {:credo, \"~> 1.7\", only: [:dev, :test], runtime: false},\n      {:dialyxir, \"~> 1.4\", only: [:dev], runtime: false}\n    ]\n  end\n\n  defp aliases do\n    [\n      setup: [\"deps.get\"],\n      build: [\"escript.build\"],\n      lint: [\"specs.check\", \"credo --strict\"]\n    ]\n  end\n\n  defp escript do\n    [\n      app: nil,\n      main_module: SymphonyElixir.CLI,\n      name: \"symphony\",\n      path: \"bin/symphony\"\n    ]\n  end\nend\n"
  },
  {
    "path": "elixir/priv/static/dashboard.css",
    "content": ":root {\n  color-scheme: light;\n  --page: #f7f7f8;\n  --page-soft: #fbfbfc;\n  --page-deep: #ececf1;\n  --card: rgba(255, 255, 255, 0.94);\n  --card-muted: #f3f4f6;\n  --ink: #202123;\n  --muted: #6e6e80;\n  --line: #ececf1;\n  --line-strong: #d9d9e3;\n  --accent: #10a37f;\n  --accent-ink: #0f513f;\n  --accent-soft: #e8faf4;\n  --danger: #b42318;\n  --danger-soft: #fef3f2;\n  --shadow-sm: 0 1px 2px rgba(16, 24, 40, 0.05);\n  --shadow-lg: 0 20px 50px rgba(15, 23, 42, 0.08);\n}\n\n* {\n  box-sizing: border-box;\n}\n\nhtml {\n  background: var(--page);\n}\n\nbody {\n  margin: 0;\n  min-height: 100vh;\n  background:\n    radial-gradient(circle at top, rgba(16, 163, 127, 0.12) 0%, rgba(16, 163, 127, 0) 30%),\n    linear-gradient(180deg, var(--page-soft) 0%, var(--page) 24%, #f3f4f6 100%);\n  color: var(--ink);\n  font-family: \"Sohne\", \"SF Pro Text\", \"Helvetica Neue\", \"Segoe UI\", sans-serif;\n  line-height: 1.5;\n}\n\na {\n  color: var(--ink);\n  text-decoration: none;\n  transition: color 140ms ease;\n}\n\na:hover {\n  color: var(--accent);\n}\n\nbutton {\n  appearance: none;\n  border: 1px solid var(--accent);\n  background: var(--accent);\n  color: white;\n  border-radius: 999px;\n  padding: 0.72rem 1.08rem;\n  cursor: pointer;\n  font: inherit;\n  font-weight: 600;\n  letter-spacing: -0.01em;\n  box-shadow: 0 8px 20px rgba(16, 163, 127, 0.18);\n  transition:\n    transform 140ms ease,\n    box-shadow 140ms ease,\n    background 140ms ease,\n    border-color 140ms ease;\n}\n\nbutton:hover {\n  transform: translateY(-1px);\n  box-shadow: 0 12px 24px rgba(16, 163, 127, 0.22);\n}\n\nbutton.secondary {\n  background: var(--card);\n  color: var(--ink);\n  border-color: var(--line-strong);\n  box-shadow: var(--shadow-sm);\n}\n\nbutton.secondary:hover {\n  box-shadow: 0 6px 16px rgba(15, 23, 42, 0.08);\n}\n\n.subtle-button {\n  appearance: none;\n  border: 1px solid var(--line-strong);\n  background: rgba(255, 255, 255, 0.72);\n  color: var(--muted);\n  border-radius: 999px;\n  padding: 0.34rem 0.72rem;\n  cursor: pointer;\n  font: inherit;\n  font-size: 0.82rem;\n  font-weight: 600;\n  letter-spacing: 0.01em;\n  box-shadow: none;\n  transition:\n    background 140ms ease,\n    border-color 140ms ease,\n    color 140ms ease;\n}\n\n.subtle-button:hover {\n  transform: none;\n  box-shadow: none;\n  background: white;\n  border-color: var(--muted);\n  color: var(--ink);\n}\n\npre {\n  margin: 0;\n  white-space: pre-wrap;\n  word-break: break-word;\n}\n\ncode,\npre,\n.mono {\n  font-family: \"Sohne Mono\", \"SFMono-Regular\", \"SF Mono\", Consolas, \"Liberation Mono\", monospace;\n}\n\n.mono,\n.numeric {\n  font-variant-numeric: tabular-nums slashed-zero;\n  font-feature-settings: \"tnum\" 1, \"zero\" 1;\n}\n\n.app-shell {\n  max-width: 1280px;\n  margin: 0 auto;\n  padding: 2rem 1rem 3.5rem;\n}\n\n.dashboard-shell {\n  display: grid;\n  gap: 1rem;\n}\n\n.hero-card,\n.section-card,\n.metric-card,\n.error-card {\n  background: var(--card);\n  border: 1px solid rgba(217, 217, 227, 0.82);\n  box-shadow: var(--shadow-sm);\n  backdrop-filter: blur(18px);\n}\n\n.hero-card {\n  border-radius: 28px;\n  padding: clamp(1.25rem, 3vw, 2rem);\n  box-shadow: var(--shadow-lg);\n}\n\n.hero-grid {\n  display: grid;\n  grid-template-columns: minmax(0, 1fr) auto;\n  gap: 1.25rem;\n  align-items: start;\n}\n\n.eyebrow {\n  margin: 0;\n  color: var(--muted);\n  text-transform: uppercase;\n  letter-spacing: 0.08em;\n  font-size: 0.76rem;\n  font-weight: 600;\n}\n\n.hero-title {\n  margin: 0.35rem 0 0;\n  font-size: clamp(2rem, 4vw, 3.3rem);\n  line-height: 0.98;\n  letter-spacing: -0.04em;\n}\n\n.hero-copy {\n  margin: 0.75rem 0 0;\n  max-width: 46rem;\n  color: var(--muted);\n  font-size: 1rem;\n}\n\n.status-stack {\n  display: grid;\n  justify-items: end;\n  align-content: start;\n  min-width: min(100%, 9rem);\n}\n\n.status-badge {\n  display: inline-flex;\n  align-items: center;\n  gap: 0.45rem;\n  min-height: 2rem;\n  padding: 0.35rem 0.78rem;\n  border-radius: 999px;\n  border: 1px solid var(--line);\n  background: var(--card-muted);\n  color: var(--muted);\n  font-size: 0.82rem;\n  font-weight: 700;\n  letter-spacing: 0.01em;\n}\n\n.status-badge-dot {\n  width: 0.52rem;\n  height: 0.52rem;\n  border-radius: 999px;\n  background: currentColor;\n  opacity: 0.9;\n}\n\n.status-badge-live {\n  display: none;\n  background: var(--accent-soft);\n  border-color: rgba(16, 163, 127, 0.18);\n  color: var(--accent-ink);\n}\n\n.status-badge-offline {\n  background: #f5f5f7;\n  border-color: var(--line-strong);\n  color: var(--muted);\n}\n\n[data-phx-main].phx-connected .status-badge-live {\n  display: inline-flex;\n}\n\n[data-phx-main].phx-connected .status-badge-offline {\n  display: none;\n}\n\n.metric-grid {\n  display: grid;\n  gap: 0.85rem;\n  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));\n}\n\n.metric-card {\n  border-radius: 22px;\n  padding: 1rem 1.05rem 1.1rem;\n}\n\n.metric-label {\n  margin: 0;\n  color: var(--muted);\n  font-size: 0.82rem;\n  font-weight: 600;\n  letter-spacing: 0.01em;\n}\n\n.metric-value {\n  margin: 0.35rem 0 0;\n  font-size: clamp(1.6rem, 2vw, 2.1rem);\n  line-height: 1.05;\n  letter-spacing: -0.03em;\n}\n\n.metric-detail {\n  margin: 0.45rem 0 0;\n  color: var(--muted);\n  font-size: 0.88rem;\n}\n\n.section-card {\n  border-radius: 24px;\n  padding: 1.15rem;\n}\n\n.section-header {\n  display: flex;\n  justify-content: space-between;\n  align-items: flex-start;\n  gap: 1rem;\n  flex-wrap: wrap;\n}\n\n.section-title {\n  margin: 0;\n  font-size: 1.08rem;\n  line-height: 1.2;\n  letter-spacing: -0.02em;\n}\n\n.section-copy {\n  margin: 0.35rem 0 0;\n  color: var(--muted);\n  font-size: 0.94rem;\n}\n\n.table-wrap {\n  overflow-x: auto;\n  margin-top: 1rem;\n}\n\n.data-table {\n  width: 100%;\n  min-width: 720px;\n  border-collapse: collapse;\n}\n\n.data-table-running {\n  table-layout: fixed;\n  min-width: 980px;\n}\n\n.data-table th {\n  padding: 0 0.5rem 0.75rem 0;\n  text-align: left;\n  color: var(--muted);\n  font-size: 0.78rem;\n  font-weight: 600;\n  text-transform: uppercase;\n  letter-spacing: 0.04em;\n}\n\n.data-table td {\n  padding: 0.9rem 0.5rem 0.9rem 0;\n  border-top: 1px solid var(--line);\n  vertical-align: top;\n  font-size: 0.94rem;\n}\n\n.issue-stack,\n.session-stack,\n.detail-stack,\n.token-stack {\n  display: grid;\n  gap: 0.24rem;\n  min-width: 0;\n}\n\n.event-text {\n  font-weight: 500;\n  line-height: 1.45;\n  max-width: 100%;\n  overflow: hidden;\n  text-overflow: ellipsis;\n  white-space: nowrap;\n}\n\n.event-meta {\n  max-width: 100%;\n  overflow: hidden;\n  text-overflow: ellipsis;\n  white-space: nowrap;\n}\n\n.state-badge {\n  display: inline-flex;\n  align-items: center;\n  min-height: 1.85rem;\n  padding: 0.3rem 0.68rem;\n  border-radius: 999px;\n  border: 1px solid var(--line);\n  background: var(--card-muted);\n  color: var(--ink);\n  font-size: 0.8rem;\n  font-weight: 600;\n  line-height: 1;\n}\n\n.state-badge-active {\n  background: var(--accent-soft);\n  border-color: rgba(16, 163, 127, 0.18);\n  color: var(--accent-ink);\n}\n\n.state-badge-warning {\n  background: #fff7e8;\n  border-color: #f1d8a6;\n  color: #8a5a00;\n}\n\n.state-badge-danger {\n  background: var(--danger-soft);\n  border-color: #f6d3cf;\n  color: var(--danger);\n}\n\n.issue-id {\n  font-weight: 600;\n  letter-spacing: -0.01em;\n}\n\n.issue-link {\n  color: var(--muted);\n  font-size: 0.86rem;\n}\n\n.muted {\n  color: var(--muted);\n}\n\n.code-panel {\n  margin-top: 1rem;\n  padding: 1rem;\n  border-radius: 18px;\n  background: #f5f5f7;\n  border: 1px solid var(--line);\n  color: #353740;\n  font-size: 0.9rem;\n}\n\n.empty-state {\n  margin: 1rem 0 0;\n  color: var(--muted);\n}\n\n.error-card {\n  border-radius: 24px;\n  padding: 1.25rem;\n  background: linear-gradient(180deg, #fff8f7 0%, var(--danger-soft) 100%);\n  border-color: #f6d3cf;\n}\n\n.error-title {\n  margin: 0;\n  color: var(--danger);\n  font-size: 1.15rem;\n  letter-spacing: -0.02em;\n}\n\n.error-copy {\n  margin: 0.45rem 0 0;\n  color: var(--danger);\n}\n\n@media (max-width: 860px) {\n  .app-shell {\n    padding: 1rem 0.85rem 2rem;\n  }\n\n  .hero-grid {\n    grid-template-columns: 1fr;\n  }\n\n  .status-stack {\n    justify-items: start;\n  }\n\n  .metric-grid {\n    grid-template-columns: repeat(2, minmax(0, 1fr));\n  }\n}\n\n@media (max-width: 560px) {\n  .metric-grid {\n    grid-template-columns: 1fr;\n  }\n\n  .section-card,\n  .hero-card,\n  .error-card {\n    border-radius: 20px;\n    padding: 1rem;\n  }\n}\n"
  },
  {
    "path": "elixir/test/fixtures/status_dashboard_snapshots/backoff_queue.evidence.md",
    "content": "```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 20,200\n│ Rate Limits: gpt-5 | primary 0/20,000 reset 95s | secondary 0/60 reset 45s | credits none\n│ Project: https://linear.app/project/project/issues\n│ Next refresh: n/a\n├─ Running\n│\n│   ID       STAGE          PID      AGE / TURN   TOKENS     SESSION        EVENT                                  \n│   ───────────────────────────────────────────────────────────────────────────────────────────────────────────────\n│ ● MT-638   retrying       4242     20m 25s / 7      14,200 thre...567890  agent message streaming: waiting on ...\n│\n├─ Backoff queue\n│\n│  ↻ MT-450 attempt=4 in 1.250s error=rate limit exhausted\n│  ↻ MT-451 attempt=2 in 3.900s error=retrying after API timeout with jitter\n│  ↻ MT-452 attempt=6 in 8.100s error=worker crashed restarting cleanly\n│  ↻ MT-453 attempt=1 in 11.000s error=fourth queued retry should also render after removing the top-three limit\n╰─\n```\n"
  },
  {
    "path": "elixir/test/fixtures/status_dashboard_snapshots/backoff_queue.snapshot.txt",
    "content": "\\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 tps\\e[0m\n\\e[1m│ Runtime: \\e[0m\\e[35m45m 0s\\e[0m\n\\e[1m│ Tokens: \\e[0m\\e[33min 18,000\\e[0m\\e[90m | \\e[0m\\e[33mout 2,200\\e[0m\\e[90m | \\e[0m\\e[33mtotal 20,200\\e[0m\n\\e[1m│ Rate Limits: \\e[0m\\e[33mgpt-5\\e[0m\\e[90m | \\e[0m\\e[36mprimary 0/20,000 reset 95s\\e[0m\\e[90m | \\e[0m\\e[36msecondary 0/60 reset 45s\\e[0m\\e[90m | \\e[0m\\e[32mcredits none\\e[0m\n\\e[1m│ Project: \\e[0m\\e[36mhttps://linear.app/project/project/issues\\e[0m\n\\e[1m│ Next refresh: \\e[0m\\e[90mn/a\\e[0m\n\\e[1m├─ Running\\e[0m\n│\n│   \\e[90mID       STAGE          PID      AGE / TURN   TOKENS     SESSION        EVENT                                  \\e[0m\n│   \\e[90m───────────────────────────────────────────────────────────────────────────────────────────────────────────────\\e[0m\n│ \\e[34m●\\e[0m \\e[36mMT-638  \\e[0m \\e[34mretrying      \\e[0m \\e[33m4242    \\e[0m \\e[35m20m 25s / 7 \\e[0m \\e[33m    14,200\\e[0m \\e[36mthre...567890 \\e[0m \\e[34magent message streaming: waiting on ...\\e[0m\n│\n\\e[1m├─ Backoff queue\\e[0m\n│\n│  \\e[33m↻\\e[0m \\e[31mMT-450\\e[0m \\e[33mattempt=4\\e[0m\\e[2m in \\e[0m\\e[36m1.250s\\e[0m \\e[2merror=rate limit exhausted\\e[0m\n│  \\e[33m↻\\e[0m \\e[31mMT-451\\e[0m \\e[33mattempt=2\\e[0m\\e[2m in \\e[0m\\e[36m3.900s\\e[0m \\e[2merror=retrying after API timeout with jitter\\e[0m\n│  \\e[33m↻\\e[0m \\e[31mMT-452\\e[0m \\e[33mattempt=6\\e[0m\\e[2m in \\e[0m\\e[36m8.100s\\e[0m \\e[2merror=worker crashed restarting cleanly\\e[0m\n│  \\e[33m↻\\e[0m \\e[31mMT-453\\e[0m \\e[33mattempt=1\\e[0m\\e[2m in \\e[0m\\e[36m11.000s\\e[0m \\e[2merror=fourth queued retry should also render after removing the top-three limit\\e[0m\n╰─\n"
  },
  {
    "path": "elixir/test/fixtures/status_dashboard_snapshots/credits_unlimited.evidence.md",
    "content": "```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│ Rate Limits: priority-tier | primary 100/100 reset 1s | secondary 500/500 reset 1s | credits unlimited\n│ Project: https://linear.app/project/project/issues\n│ Next refresh: n/a\n├─ Running\n│\n│   ID       STAGE          PID      AGE / TURN   TOKENS     SESSION        EVENT                                  \n│   ───────────────────────────────────────────────────────────────────────────────────────────────────────────────\n│ ● MT-777   running        4242     1m 15s / 7        3,200 thre...567890  thread token usage updated (in 90, o...\n│\n├─ Backoff queue\n│\n│  No queued retries\n╰─\n```\n"
  },
  {
    "path": "elixir/test/fixtures/status_dashboard_snapshots/credits_unlimited.snapshot.txt",
    "content": "\\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 tps\\e[0m\n\\e[1m│ Runtime: \\e[0m\\e[35m1m 15s\\e[0m\n\\e[1m│ Tokens: \\e[0m\\e[33min 90\\e[0m\\e[90m | \\e[0m\\e[33mout 12\\e[0m\\e[90m | \\e[0m\\e[33mtotal 102\\e[0m\n\\e[1m│ Rate Limits: \\e[0m\\e[33mpriority-tier\\e[0m\\e[90m | \\e[0m\\e[36mprimary 100/100 reset 1s\\e[0m\\e[90m | \\e[0m\\e[36msecondary 500/500 reset 1s\\e[0m\\e[90m | \\e[0m\\e[32mcredits unlimited\\e[0m\n\\e[1m│ Project: \\e[0m\\e[36mhttps://linear.app/project/project/issues\\e[0m\n\\e[1m│ Next refresh: \\e[0m\\e[90mn/a\\e[0m\n\\e[1m├─ Running\\e[0m\n│\n│   \\e[90mID       STAGE          PID      AGE / TURN   TOKENS     SESSION        EVENT                                  \\e[0m\n│   \\e[90m───────────────────────────────────────────────────────────────────────────────────────────────────────────────\\e[0m\n│ \\e[33m●\\e[0m \\e[36mMT-777  \\e[0m \\e[33mrunning       \\e[0m \\e[33m4242    \\e[0m \\e[35m1m 15s / 7  \\e[0m \\e[33m     3,200\\e[0m \\e[36mthre...567890 \\e[0m \\e[33mthread token usage updated (in 90, o...\\e[0m\n│\n\\e[1m├─ Backoff queue\\e[0m\n│\n│  \\e[90mNo queued retries\\e[0m\n╰─\n"
  },
  {
    "path": "elixir/test/fixtures/status_dashboard_snapshots/idle.evidence.md",
    "content": "```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 Limits: unavailable\n│ Project: https://linear.app/project/project/issues\n│ Next refresh: n/a\n├─ Running\n│\n│   ID       STAGE          PID      AGE / TURN   TOKENS     SESSION        EVENT                                  \n│   ───────────────────────────────────────────────────────────────────────────────────────────────────────────────\n│  No active agents\n│\n├─ Backoff queue\n│\n│  No queued retries\n╰─\n```\n"
  },
  {
    "path": "elixir/test/fixtures/status_dashboard_snapshots/idle.snapshot.txt",
    "content": "\\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 tps\\e[0m\n\\e[1m│ Runtime: \\e[0m\\e[35m0m 0s\\e[0m\n\\e[1m│ Tokens: \\e[0m\\e[33min 0\\e[0m\\e[90m | \\e[0m\\e[33mout 0\\e[0m\\e[90m | \\e[0m\\e[33mtotal 0\\e[0m\n\\e[1m│ Rate Limits: \\e[0m\\e[90munavailable\\e[0m\n\\e[1m│ Project: \\e[0m\\e[36mhttps://linear.app/project/project/issues\\e[0m\n\\e[1m│ Next refresh: \\e[0m\\e[90mn/a\\e[0m\n\\e[1m├─ Running\\e[0m\n│\n│   \\e[90mID       STAGE          PID      AGE / TURN   TOKENS     SESSION        EVENT                                  \\e[0m\n│   \\e[90m───────────────────────────────────────────────────────────────────────────────────────────────────────────────\\e[0m\n│  \\e[90mNo active agents\\e[0m\n│\n\\e[1m├─ Backoff queue\\e[0m\n│\n│  \\e[90mNo queued retries\\e[0m\n╰─\n"
  },
  {
    "path": "elixir/test/fixtures/status_dashboard_snapshots/idle_with_dashboard_url.evidence.md",
    "content": "```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 Limits: unavailable\n│ Project: https://linear.app/project/project/issues\n│ Dashboard: http://127.0.0.1:4000/\n│ Next refresh: n/a\n├─ Running\n│\n│   ID       STAGE          PID      AGE / TURN   TOKENS     SESSION        EVENT                                  \n│   ───────────────────────────────────────────────────────────────────────────────────────────────────────────────\n│  No active agents\n│\n├─ Backoff queue\n│\n│  No queued retries\n╰─\n```\n"
  },
  {
    "path": "elixir/test/fixtures/status_dashboard_snapshots/idle_with_dashboard_url.snapshot.txt",
    "content": "\\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 tps\\e[0m\n\\e[1m│ Runtime: \\e[0m\\e[35m0m 0s\\e[0m\n\\e[1m│ Tokens: \\e[0m\\e[33min 0\\e[0m\\e[90m | \\e[0m\\e[33mout 0\\e[0m\\e[90m | \\e[0m\\e[33mtotal 0\\e[0m\n\\e[1m│ Rate Limits: \\e[0m\\e[90munavailable\\e[0m\n\\e[1m│ Project: \\e[0m\\e[36mhttps://linear.app/project/project/issues\\e[0m\n\\e[1m│ Dashboard: \\e[0m\\e[36mhttp://127.0.0.1:4000/\\e[0m\n\\e[1m│ Next refresh: \\e[0m\\e[90mn/a\\e[0m\n\\e[1m├─ Running\\e[0m\n│\n│   \\e[90mID       STAGE          PID      AGE / TURN   TOKENS     SESSION        EVENT                                  \\e[0m\n│   \\e[90m───────────────────────────────────────────────────────────────────────────────────────────────────────────────\\e[0m\n│  \\e[90mNo active agents\\e[0m\n│\n\\e[1m├─ Backoff queue\\e[0m\n│\n│  \\e[90mNo queued retries\\e[0m\n╰─\n"
  },
  {
    "path": "elixir/test/fixtures/status_dashboard_snapshots/super_busy.evidence.md",
    "content": "```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 | total 268,500\n│ Rate Limits: gpt-5 | primary 12,345/20,000 reset 30s | secondary 45/60 reset 12s | credits 9876.50\n│ Project: https://linear.app/project/project/issues\n│ Next refresh: n/a\n├─ Running\n│\n│   ID       STAGE          PID      AGE / TURN   TOKENS     SESSION        EVENT                                  \n│   ───────────────────────────────────────────────────────────────────────────────────────────────────────────────\n│ ● MT-101   running        4242     13m 5s / 11     120,450 thre...567890  turn completed (completed)             \n│ ● MT-102   running        5252     6m 52s / 4       89,200 thre...567890  mix test --cover                       \n│\n├─ Backoff queue\n│\n│  No queued retries\n╰─\n```\n"
  },
  {
    "path": "elixir/test/fixtures/status_dashboard_snapshots/super_busy.snapshot.txt",
    "content": "\\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,842 tps\\e[0m\n\\e[1m│ Runtime: \\e[0m\\e[35m72m 1s\\e[0m\n\\e[1m│ Tokens: \\e[0m\\e[33min 250,000\\e[0m\\e[90m | \\e[0m\\e[33mout 18,500\\e[0m\\e[90m | \\e[0m\\e[33mtotal 268,500\\e[0m\n\\e[1m│ Rate Limits: \\e[0m\\e[33mgpt-5\\e[0m\\e[90m | \\e[0m\\e[36mprimary 12,345/20,000 reset 30s\\e[0m\\e[90m | \\e[0m\\e[36msecondary 45/60 reset 12s\\e[0m\\e[90m | \\e[0m\\e[32mcredits 9876.50\\e[0m\n\\e[1m│ Project: \\e[0m\\e[36mhttps://linear.app/project/project/issues\\e[0m\n\\e[1m│ Next refresh: \\e[0m\\e[90mn/a\\e[0m\n\\e[1m├─ Running\\e[0m\n│\n│   \\e[90mID       STAGE          PID      AGE / TURN   TOKENS     SESSION        EVENT                                  \\e[0m\n│   \\e[90m───────────────────────────────────────────────────────────────────────────────────────────────────────────────\\e[0m\n│ \\e[35m●\\e[0m \\e[36mMT-101  \\e[0m \\e[35mrunning       \\e[0m \\e[33m4242    \\e[0m \\e[35m13m 5s / 11 \\e[0m \\e[33m   120,450\\e[0m \\e[36mthre...567890 \\e[0m \\e[35mturn completed (completed)             \\e[0m\n│ \\e[32m●\\e[0m \\e[36mMT-102  \\e[0m \\e[32mrunning       \\e[0m \\e[33m5252    \\e[0m \\e[35m6m 52s / 4  \\e[0m \\e[33m    89,200\\e[0m \\e[36mthre...567890 \\e[0m \\e[32mmix test --cover                       \\e[0m\n│\n\\e[1m├─ Backoff queue\\e[0m\n│\n│  \\e[90mNo queued retries\\e[0m\n╰─\n"
  },
  {
    "path": "elixir/test/mix/tasks/pr_body_check_test.exs",
    "content": "defmodule Mix.Tasks.PrBody.CheckTest do\n  use ExUnit.Case, async: false\n\n  alias Mix.Tasks.PrBody.Check\n\n  import ExUnit.CaptureIO\n\n  @template \"\"\"\n  #### Context\n\n  <!-- Why is this change needed? -->\n\n  #### TL;DR\n\n  *<!-- A short summary -->*\n\n  #### Summary\n\n  - <!-- Summary bullet -->\n\n  #### Alternatives\n\n  - <!-- Alternative bullet -->\n\n  #### Test Plan\n\n  - [ ] <!-- Test checkbox -->\n  \"\"\"\n\n  @valid_body \"\"\"\n  #### Context\n\n  Context text.\n\n  #### TL;DR\n\n  Short summary.\n\n  #### Summary\n\n  - First change.\n\n  #### Alternatives\n\n  - Alternative considered.\n\n  #### Test Plan\n\n  - [x] Ran targeted checks.\n  \"\"\"\n\n  setup do\n    Mix.Task.reenable(\"pr_body.check\")\n    :ok\n  end\n\n  test \"prints help\" do\n    output = capture_io(fn -> Check.run([\"--help\"]) end)\n    assert output =~ \"mix pr_body.check --file /path/to/pr_body.md\"\n  end\n\n  test \"fails on invalid options\" do\n    assert_raise Mix.Error, ~r/Invalid option/, fn ->\n      Check.run([\"lint\", \"--wat\"])\n    end\n  end\n\n  test \"fails when file option is missing\" do\n    assert_raise Mix.Error, ~r/Missing required option --file/, fn ->\n      Check.run([\"lint\"])\n    end\n  end\n\n  test \"fails when template is missing\" do\n    in_temp_repo(fn ->\n      File.write!(\"body.md\", @valid_body)\n\n      assert_raise Mix.Error, ~r/Unable to read PR template/, fn ->\n        Check.run([\"lint\", \"--file\", \"body.md\"])\n      end\n    end)\n  end\n\n  test \"fails when template has no headings\" do\n    in_temp_repo(fn ->\n      write_template!(\"no headings here\")\n      File.write!(\"body.md\", @valid_body)\n\n      assert_raise Mix.Error, ~r/No markdown headings found/, fn ->\n        Check.run([\"lint\", \"--file\", \"body.md\"])\n      end\n    end)\n  end\n\n  test \"fails when body file is missing\" do\n    in_temp_repo(fn ->\n      write_template!(@template)\n\n      assert_raise Mix.Error, ~r/Unable to read missing\\.md/, fn ->\n        Check.run([\"lint\", \"--file\", \"missing.md\"])\n      end\n    end)\n  end\n\n  test \"fails when body still has placeholders\" do\n    in_temp_repo(fn ->\n      write_template!(@template)\n      File.write!(\"body.md\", @template)\n\n      error_output =\n        capture_io(:stderr, fn ->\n          assert_raise Mix.Error, ~r/PR body format invalid/, fn ->\n            Check.run([\"lint\", \"--file\", \"body.md\"])\n          end\n        end)\n\n      assert error_output =~ \"PR description still contains template placeholder comments\"\n    end)\n  end\n\n  test \"fails when heading is missing\" do\n    in_temp_repo(fn ->\n      write_template!(@template)\n\n      missing_heading = String.replace(@valid_body, \"#### Alternatives\\n\\n- Alternative considered.\\n\\n\", \"\")\n      File.write!(\"body.md\", missing_heading)\n\n      error_output =\n        capture_io(:stderr, fn ->\n          assert_raise Mix.Error, ~r/PR body format invalid/, fn ->\n            Check.run([\"lint\", \"--file\", \"body.md\"])\n          end\n        end)\n\n      assert error_output =~ \"Missing required heading: #### Alternatives\"\n    end)\n  end\n\n  test \"fails when headings are out of order\" do\n    in_temp_repo(fn ->\n      write_template!(@template)\n\n      out_of_order = \"\"\"\n      #### TL;DR\n\n      Short summary.\n\n      #### Context\n\n      Context text.\n\n      #### Summary\n\n      - First change.\n\n      #### Alternatives\n\n      - Alternative considered.\n\n      #### Test Plan\n\n      - [x] Ran targeted checks.\n      \"\"\"\n\n      File.write!(\"body.md\", out_of_order)\n\n      error_output =\n        capture_io(:stderr, fn ->\n          assert_raise Mix.Error, ~r/PR body format invalid/, fn ->\n            Check.run([\"lint\", \"--file\", \"body.md\"])\n          end\n        end)\n\n      assert error_output =~ \"Required headings are out of order.\"\n    end)\n  end\n\n  test \"fails on empty section\" do\n    in_temp_repo(fn ->\n      write_template!(@template)\n\n      empty_context = String.replace(@valid_body, \"Context text.\", \"\")\n      File.write!(\"body.md\", empty_context)\n\n      error_output =\n        capture_io(:stderr, fn ->\n          assert_raise Mix.Error, ~r/PR body format invalid/, fn ->\n            Check.run([\"lint\", \"--file\", \"body.md\"])\n          end\n        end)\n\n      assert error_output =~ \"Section cannot be empty: #### Context\"\n    end)\n  end\n\n  test \"fails when a middle section is blank before the next heading\" do\n    in_temp_repo(fn ->\n      write_template!(@template)\n\n      blank_alternatives = \"\"\"\n      #### Context\n\n      Context text.\n\n      #### TL;DR\n\n      Short summary.\n\n      #### Summary\n\n      - First change.\n\n      #### Alternatives\n\n\n      #### Test Plan\n\n      - [x] Ran targeted checks.\n      \"\"\"\n\n      File.write!(\"body.md\", blank_alternatives)\n\n      error_output =\n        capture_io(:stderr, fn ->\n          assert_raise Mix.Error, ~r/PR body format invalid/, fn ->\n            Check.run([\"lint\", \"--file\", \"body.md\"])\n          end\n        end)\n\n      assert error_output =~ \"Section cannot be empty: #### Alternatives\"\n    end)\n  end\n\n  test \"fails when bullet and checkbox expectations are not met\" do\n    in_temp_repo(fn ->\n      write_template!(@template)\n\n      invalid_body = \"\"\"\n      #### Context\n\n      Context text.\n\n      #### TL;DR\n\n      Short summary.\n\n      #### Summary\n\n      Not a bullet.\n\n      #### Alternatives\n\n      Also not a bullet.\n\n      #### Test Plan\n\n      No checkbox.\n      \"\"\"\n\n      File.write!(\"body.md\", invalid_body)\n\n      error_output =\n        capture_io(:stderr, fn ->\n          assert_raise Mix.Error, ~r/PR body format invalid/, fn ->\n            Check.run([\"lint\", \"--file\", \"body.md\"])\n          end\n        end)\n\n      assert error_output =~ \"Section must include at least one bullet item: #### Summary\"\n      assert error_output =~ \"Section must include at least one bullet item: #### Alternatives\"\n      assert error_output =~ \"Section must include at least one bullet item: #### Test Plan\"\n      assert error_output =~ \"Section must include at least one checkbox item: #### Test Plan\"\n    end)\n  end\n\n  test \"fails when heading has no content delimiter\" do\n    in_temp_repo(fn ->\n      write_template!(@template)\n      File.write!(\"body.md\", \"#### Context\\nContext text.\")\n\n      capture_io(:stderr, fn ->\n        assert_raise Mix.Error, ~r/PR body format invalid/, fn ->\n          Check.run([\"lint\", \"--file\", \"body.md\"])\n        end\n      end)\n    end)\n  end\n\n  test \"fails when heading appears at end of file\" do\n    in_temp_repo(fn ->\n      write_template!(@template)\n      File.write!(\"body.md\", \"#### Context\")\n\n      error_output =\n        capture_io(:stderr, fn ->\n          assert_raise Mix.Error, ~r/PR body format invalid/, fn ->\n            Check.run([\"lint\", \"--file\", \"body.md\"])\n          end\n        end)\n\n      assert error_output =~ \"Section cannot be empty: #### Context\"\n    end)\n  end\n\n  test \"passes for valid body\" do\n    in_temp_repo(fn ->\n      write_template!(@template)\n      File.write!(\"body.md\", @valid_body)\n\n      output =\n        capture_io(fn ->\n          Check.run([\"lint\", \"--file\", \"body.md\"])\n        end)\n\n      assert output =~ \"PR body format OK\"\n    end)\n  end\n\n  defp in_temp_repo(fun) do\n    unique = System.unique_integer([:positive, :monotonic])\n    root = Path.join(System.tmp_dir!(), \"validate-pr-body-task-test-#{unique}\")\n\n    File.rm_rf!(root)\n    File.mkdir_p!(root)\n\n    original_cwd = File.cwd!()\n\n    try do\n      File.cd!(root)\n      fun.()\n    after\n      File.cd!(original_cwd)\n      File.rm_rf!(root)\n    end\n  end\n\n  defp write_template!(content) do\n    File.mkdir_p!(\".github\")\n    File.write!(\".github/pull_request_template.md\", content)\n  end\nend\n"
  },
  {
    "path": "elixir/test/mix/tasks/specs_check_task_test.exs",
    "content": "defmodule Mix.Tasks.Specs.CheckTaskTest do\n  use ExUnit.Case, async: false\n\n  import ExUnit.CaptureIO\n\n  alias Mix.Tasks.Specs.Check\n\n  setup do\n    Mix.Task.reenable(\"specs.check\")\n    :ok\n  end\n\n  test \"uses the default lib path when all public functions have specs\" do\n    in_temp_project(fn ->\n      write_module!(\"lib/sample.ex\", \"\"\"\n      defmodule Sample do\n        @spec ok(term()) :: term()\n        def ok(arg), do: arg\n      end\n      \"\"\")\n\n      output =\n        capture_io(fn ->\n          assert :ok = Check.run([])\n        end)\n\n      assert output =~ \"specs.check: all public functions have @spec or exemption\"\n    end)\n  end\n\n  test \"raises when an explicit path contains missing specs\" do\n    in_temp_project(fn ->\n      write_module!(\"src/sample.ex\", \"\"\"\n      defmodule Sample do\n        def missing(arg), do: arg\n      end\n      \"\"\")\n\n      error_output =\n        capture_io(:stderr, fn ->\n          assert_raise Mix.Error, ~r/specs.check failed with 1 missing @spec declaration/, fn ->\n            Check.run([\"--paths\", \"src\"])\n          end\n        end)\n\n      assert error_output =~ \"src/sample.ex:2 missing @spec for Sample.missing/1\"\n    end)\n  end\n\n  test \"loads exemptions from a file and ignores comments and blank lines\" do\n    in_temp_project(fn ->\n      write_module!(\"lib/sample.ex\", \"\"\"\n      defmodule Sample do\n        def legacy(arg), do: arg\n      end\n      \"\"\")\n\n      File.mkdir_p!(\"config\")\n\n      File.write!(\"config/specs_exemptions.txt\", \"\"\"\n      # existing exemptions\n\n      Sample.legacy/1\n      \"\"\")\n\n      output =\n        capture_io(fn ->\n          assert :ok = Check.run([\"--paths\", \"lib\", \"--exemptions-file\", \"config/specs_exemptions.txt\"])\n        end)\n\n      assert output =~ \"specs.check: all public functions have @spec or exemption\"\n    end)\n  end\n\n  test \"treats a missing exemptions file as empty\" do\n    in_temp_project(fn ->\n      write_module!(\"lib/sample.ex\", \"\"\"\n      defmodule Sample do\n        @spec ok(term()) :: term()\n        def ok(arg), do: arg\n      end\n      \"\"\")\n\n      output =\n        capture_io(fn ->\n          assert :ok = Check.run([\"--exemptions-file\", \"config/missing.txt\"])\n        end)\n\n      assert output =~ \"specs.check: all public functions have @spec or exemption\"\n    end)\n  end\n\n  defp in_temp_project(fun) do\n    root = Path.join(System.tmp_dir!(), \"specs-check-task-test-#{System.unique_integer([:positive, :monotonic])}\")\n    original_cwd = File.cwd!()\n\n    File.rm_rf!(root)\n    File.mkdir_p!(root)\n\n    try do\n      File.cd!(root, fun)\n    after\n      File.cd!(original_cwd)\n      File.rm_rf!(root)\n    end\n  end\n\n  defp write_module!(path, source) do\n    File.mkdir_p!(Path.dirname(path))\n    File.write!(path, source)\n  end\nend\n"
  },
  {
    "path": "elixir/test/mix/tasks/workspace_before_remove_test.exs",
    "content": "defmodule Mix.Tasks.Workspace.BeforeRemoveTest do\n  use ExUnit.Case, async: false\n\n  alias Mix.Tasks.Workspace.BeforeRemove\n\n  import ExUnit.CaptureIO\n\n  setup do\n    Mix.Task.reenable(\"workspace.before_remove\")\n    :ok\n  end\n\n  test \"prints help\" do\n    output =\n      capture_io(fn ->\n        BeforeRemove.run([\"--help\"])\n      end)\n\n    assert output =~ \"mix workspace.before_remove\"\n  end\n\n  test \"fails on invalid options\" do\n    assert_raise Mix.Error, ~r/Invalid option/, fn ->\n      BeforeRemove.run([\"--wat\"])\n    end\n  end\n\n  test \"no-ops when branch is unavailable\" do\n    with_path([], fn ->\n      in_temp_dir(fn ->\n        output =\n          capture_io(fn ->\n            BeforeRemove.run([])\n          end)\n\n        assert output == \"\"\n      end)\n    end)\n  end\n\n  test \"no-ops when gh is unavailable\" do\n    with_path([], fn ->\n      output =\n        capture_io(fn ->\n          BeforeRemove.run([\"--branch\", \"feature/no-gh\"])\n        end)\n\n      assert output == \"\"\n    end)\n  end\n\n  test \"uses current branch for lookup when branch option is omitted\" do\n    with_fake_gh_and_git(\n      \"\"\"\n      #!/bin/sh\n      printf '%s\\n' \"$*\" >> \"$GH_LOG\"\n\n      if [ \"$1\" = \"auth\" ] && [ \"$2\" = \"status\" ]; then\n        exit 0\n      fi\n\n      if [ \"$1\" = \"pr\" ] && [ \"$2\" = \"list\" ]; then\n        printf '101\\n102\\n'\n        exit 0\n      fi\n\n      if [ \"$1\" = \"pr\" ] && [ \"$2\" = \"close\" ] && [ \"$3\" = \"101\" ]; then\n        exit 0\n      fi\n\n      if [ \"$1\" = \"pr\" ] && [ \"$2\" = \"close\" ] && [ \"$3\" = \"102\" ]; then\n        printf 'boom\\n' >&2\n        exit 17\n      fi\n\n      exit 99\n      \"\"\",\n      \"\"\"\n      #!/bin/sh\n      printf 'feature/workpad\\n'\n      exit 0\n      \"\"\",\n      fn log_path ->\n        {output, error_output} =\n          capture_task_output(fn ->\n            BeforeRemove.run([])\n          end)\n\n        assert output =~ \"Closed PR #101 for branch feature/workpad\"\n        assert error_output =~ \"Failed to close PR #102 for branch feature/workpad\"\n\n        log = File.read!(log_path)\n\n        assert log =~\n                 \"pr list --repo openai/symphony --head feature/workpad --state open --json number --jq .[].number\"\n\n        assert log =~ \"pr close 101 --repo openai/symphony\"\n        assert log =~ \"pr close 102 --repo openai/symphony\"\n      end\n    )\n  end\n\n  test \"closes open pull requests for the branch and tolerates close failures\" do\n    with_fake_gh(fn log_path ->\n      File.write!(log_path, \"\")\n\n      {output, error_output} =\n        capture_task_output(fn ->\n          BeforeRemove.run([\"--branch\", \"feature/workpad\"])\n        end)\n\n      assert output =~ \"Closed PR #101 for branch feature/workpad\"\n      assert error_output =~ \"Failed to close PR #102 for branch feature/workpad\"\n\n      log = File.read!(log_path)\n\n      assert log =~ \"auth status\"\n      assert log =~ \"pr list --repo openai/symphony --head feature/workpad --state open --json number --jq .[].number\"\n      assert log =~ \"pr close 101 --repo openai/symphony\"\n      assert log =~ \"pr close 102 --repo openai/symphony\"\n\n      {second_output, error_output} =\n        capture_task_output(fn ->\n          Mix.Task.reenable(\"workspace.before_remove\")\n          BeforeRemove.run([\"--branch\", \"feature/workpad\"])\n        end)\n\n      assert second_output =~ \"Closed PR #101 for branch feature/workpad\"\n      assert error_output =~ \"Failed to close PR #102 for branch feature/workpad\"\n    end)\n  end\n\n  test \"formats close failures without command stderr output\" do\n    with_fake_gh(\n      \"\"\"\n      #!/bin/sh\n      printf '%s\\n' \"$*\" >> \"$GH_LOG\"\n\n      if [ \"$1\" = \"auth\" ] && [ \"$2\" = \"status\" ]; then\n        exit 0\n      fi\n\n      if [ \"$1\" = \"pr\" ] && [ \"$2\" = \"list\" ]; then\n        printf '102\\n'\n        exit 0\n      fi\n\n      if [ \"$1\" = \"pr\" ] && [ \"$2\" = \"close\" ] && [ \"$3\" = \"102\" ]; then\n        exit 17\n      fi\n\n      exit 99\n      \"\"\",\n      fn log_path ->\n        error_output =\n          capture_io(:stderr, fn ->\n            Mix.Task.reenable(\"workspace.before_remove\")\n            BeforeRemove.run([\"--branch\", \"feature/no-output\"])\n          end)\n\n        assert error_output =~ \"Failed to close PR #102 for branch feature/no-output: exit 17\"\n        refute error_output =~ \"output=\"\n        log = File.read!(log_path)\n        assert log =~ \"pr list --repo openai/symphony --head feature/no-output --state open --json number --jq .[].number\"\n        assert log =~ \"pr close 102 --repo openai/symphony\"\n      end\n    )\n  end\n\n  test \"no-ops when PR list fails for current branch\" do\n    with_fake_gh(\n      \"\"\"\n      #!/bin/sh\n      printf '%s\\n' \"$*\" >> \"$GH_LOG\"\n\n      if [ \"$1\" = \"auth\" ] && [ \"$2\" = \"status\" ]; then\n        exit 0\n      fi\n\n      if [ \"$1\" = \"pr\" ] && [ \"$2\" = \"list\" ]; then\n        exit 1\n      fi\n\n      exit 99\n      \"\"\",\n      fn log_path ->\n        output =\n          capture_io(fn ->\n            BeforeRemove.run([\"--branch\", \"feature/list-fails\"])\n          end)\n\n        assert output == \"\"\n\n        log = File.read!(log_path)\n        assert log =~ \"auth status\"\n\n        assert log =~\n                 \"pr list --repo openai/symphony --head feature/list-fails --state open --json number --jq .[].number\"\n\n        refute log =~ \"pr close\"\n      end\n    )\n  end\n\n  test \"no-ops when git current branch is blank\" do\n    with_fake_gh_and_git(\n      \"\"\"\n      #!/bin/sh\n      printf '%s\\n' \"$*\" >> \"$GH_LOG\"\n\n      if [ \"$1\" = \"auth\" ] && [ \"$2\" = \"status\" ]; then\n        exit 0\n      fi\n\n      exit 99\n      \"\"\",\n      \"\"\"\n      #!/bin/sh\n      printf '\\n'\n      exit 0\n      \"\"\",\n      fn log_path ->\n        output =\n          capture_io(fn ->\n            BeforeRemove.run([])\n          end)\n\n        assert output == \"\"\n\n        log = File.read!(log_path)\n        assert log == \"\"\n        refute log =~ \"pr list\"\n      end\n    )\n  end\n\n  test \"no-ops when gh auth is unavailable\" do\n    with_fake_gh(\n      \"\"\"\n      #!/bin/sh\n      printf '%s\\n' \"$*\" >> \"$GH_LOG\"\n      if [ \"$1\" = \"auth\" ] && [ \"$2\" = \"status\" ]; then\n        exit 1\n      fi\n      exit 99\n      \"\"\",\n      fn log_path ->\n        BeforeRemove.run([\"--branch\", \"feature/no-auth\"])\n\n        log = File.read!(log_path)\n        assert log =~ \"auth status\"\n        refute log =~ \"pr list\"\n      end\n    )\n  end\n\n  defp with_fake_gh(fun) do\n    with_fake_binaries(\n      %{\n        \"gh\" => \"\"\"\n        #!/bin/sh\n        printf '%s\\n' \"$*\" >> \"$GH_LOG\"\n\n        if [ \"$1\" = \"auth\" ] && [ \"$2\" = \"status\" ]; then\n          exit 0\n        fi\n\n        if [ \"$1\" = \"pr\" ] && [ \"$2\" = \"list\" ]; then\n          printf '101\\n102\\n'\n          exit 0\n        fi\n\n        if [ \"$1\" = \"pr\" ] && [ \"$2\" = \"close\" ] && [ \"$3\" = \"101\" ]; then\n          exit 0\n        fi\n\n        if [ \"$1\" = \"pr\" ] && [ \"$2\" = \"close\" ] && [ \"$3\" = \"102\" ]; then\n          printf 'boom\\n' >&2\n          exit 17\n        fi\n\n        exit 99\n        \"\"\"\n      },\n      fun\n    )\n  end\n\n  defp with_fake_gh(script, fun) do\n    with_fake_binaries(%{\"gh\" => script}, fun)\n  end\n\n  defp with_fake_gh_and_git(gh_script, git_script, fun) do\n    with_fake_binaries(%{\"gh\" => gh_script, \"git\" => git_script}, fun)\n  end\n\n  defp with_fake_binaries(scripts, fun) do\n    unique = System.unique_integer([:positive, :monotonic])\n    root = Path.join(System.tmp_dir!(), \"workspace-before-remove-task-test-#{unique}\")\n    bin_dir = Path.join(root, \"bin\")\n    log_path = Path.join(root, \"gh.log\")\n\n    try do\n      File.rm_rf!(root)\n      File.mkdir_p!(bin_dir)\n      File.write!(log_path, \"\")\n      original_path = System.get_env(\"PATH\") || \"\"\n      path_with_binaries = Enum.join([bin_dir, original_path], \":\")\n\n      Enum.each(scripts, fn {name, script} ->\n        path = Path.join(bin_dir, name)\n        File.write!(path, script)\n        File.chmod!(path, 0o755)\n      end)\n\n      with_env(\n        %{\n          \"GH_LOG\" => log_path,\n          \"PATH\" => path_with_binaries\n        },\n        fn ->\n          fun.(log_path)\n        end\n      )\n    after\n      File.rm_rf!(root)\n    end\n  end\n\n  defp with_path(paths, fun) do\n    with_env(%{\"PATH\" => Enum.join(paths, \":\")}, fun)\n  end\n\n  defp with_env(overrides, fun) do\n    keys = Map.keys(overrides)\n    previous = Map.new(keys, fn key -> {key, System.get_env(key)} end)\n\n    try do\n      Enum.each(overrides, fn {key, value} -> System.put_env(key, value) end)\n      fun.()\n    after\n      Enum.each(previous, fn\n        {key, nil} -> System.delete_env(key)\n        {key, value} -> System.put_env(key, value)\n      end)\n    end\n  end\n\n  defp in_temp_dir(fun) do\n    unique = System.unique_integer([:positive, :monotonic])\n    root = Path.join(System.tmp_dir!(), \"workspace-before-remove-empty-dir-#{unique}\")\n\n    File.rm_rf!(root)\n    File.mkdir_p!(root)\n\n    original_cwd = File.cwd!()\n\n    try do\n      File.cd!(root)\n      fun.()\n    after\n      File.cd!(original_cwd)\n      File.rm_rf!(root)\n    end\n  end\n\n  defp capture_task_output(fun) do\n    parent = self()\n    ref = make_ref()\n\n    error_output =\n      capture_io(:stderr, fn ->\n        output =\n          capture_io(fn ->\n            fun.()\n          end)\n\n        send(parent, {ref, output})\n      end)\n\n    output =\n      receive do\n        {^ref, output} -> output\n      after\n        1_000 -> flunk(\"Timed out waiting for captured task output\")\n      end\n\n    {output, error_output}\n  end\nend\n"
  },
  {
    "path": "elixir/test/support/live_e2e_docker/Dockerfile",
    "content": "FROM node:20-bookworm-slim\n\nRUN apt-get update && apt-get install -y --no-install-recommends \\\n    bash \\\n    ca-certificates \\\n    git \\\n    openssh-server \\\n    python3 \\\n    ripgrep \\\n  && rm -rf /var/lib/apt/lists/*\n\nRUN install -d -m 700 /root/.ssh /root/.codex /run/symphony/ssh /var/run/sshd\n\nRUN npm install --global @openai/codex\n\nCOPY symphony-live-worker.conf /etc/ssh/sshd_config.d/symphony-live-worker.conf\nCOPY live_worker_entrypoint.sh /usr/local/bin/symphony-live-worker\nRUN chmod 755 /usr/local/bin/symphony-live-worker\n\nEXPOSE 22\n\nENTRYPOINT [\"/usr/local/bin/symphony-live-worker\"]\n"
  },
  {
    "path": "elixir/test/support/live_e2e_docker/docker-compose.yml",
    "content": "services:\n  worker1:\n    build:\n      context: .\n      dockerfile: Dockerfile\n    ports:\n      - \"${SYMPHONY_LIVE_DOCKER_WORKER_1_PORT}:22\"\n    volumes:\n      - ${SYMPHONY_LIVE_DOCKER_AUTHORIZED_KEY}:/run/symphony/ssh/authorized_key.pub:ro\n      - ${SYMPHONY_LIVE_DOCKER_AUTH_JSON}:/root/.codex/auth.json:ro\n\n  worker2:\n    build:\n      context: .\n      dockerfile: Dockerfile\n    ports:\n      - \"${SYMPHONY_LIVE_DOCKER_WORKER_2_PORT}:22\"\n    volumes:\n      - ${SYMPHONY_LIVE_DOCKER_AUTHORIZED_KEY}:/run/symphony/ssh/authorized_key.pub:ro\n      - ${SYMPHONY_LIVE_DOCKER_AUTH_JSON}:/root/.codex/auth.json:ro\n"
  },
  {
    "path": "elixir/test/support/live_e2e_docker/live_worker_entrypoint.sh",
    "content": "#!/bin/sh\nset -eu\n\ninstall -d -m 700 /root/.ssh /root/.codex\n\nif [ ! -s /run/symphony/ssh/authorized_key.pub ]; then\n  echo \"missing authorized key at /run/symphony/ssh/authorized_key.pub\" >&2\n  exit 1\nfi\n\ninstall -m 600 /run/symphony/ssh/authorized_key.pub /root/.ssh/authorized_keys\n\nexec /usr/sbin/sshd -D -e\n"
  },
  {
    "path": "elixir/test/support/live_e2e_docker/symphony-live-worker.conf",
    "content": "PubkeyAuthentication yes\nPasswordAuthentication no\nKbdInteractiveAuthentication no\nChallengeResponseAuthentication no\nUsePAM no\nPermitRootLogin yes\nAuthorizedKeysFile .ssh/authorized_keys\n"
  },
  {
    "path": "elixir/test/support/snapshot_support.exs",
    "content": "defmodule SymphonyElixir.TestSupport.Snapshot do\n  import ExUnit.Assertions\n\n  @snapshot_root Path.expand(\"../fixtures\", __DIR__)\n  @ansi_regex ~r/\\e\\[[0-9;]*m/\n\n  @update_snapshot_hint \"Run `UPDATE_SNAPSHOTS=1 mix test test/symphony_elixir/status_dashboard_snapshot_test.exs` to create or update fixtures.\"\n\n  def assert_dashboard_snapshot!(name, raw_ansi_content)\n      when is_binary(name) and is_binary(raw_ansi_content) do\n    assert_snapshot!(\n      Path.join(\"status_dashboard_snapshots\", \"#{name}.snapshot.txt\"),\n      escape_ansi(raw_ansi_content)\n    )\n\n    assert_snapshot!(\n      Path.join(\"status_dashboard_snapshots\", \"#{name}.evidence.md\"),\n      evidence_markdown(raw_ansi_content)\n    )\n\n    :ok\n  end\n\n  def assert_snapshot!(relative_path, content)\n      when is_binary(relative_path) and is_binary(content) do\n    path = snapshot_path(relative_path)\n    normalized = normalize_content(content)\n\n    File.mkdir_p!(Path.dirname(path))\n\n    if update_snapshots?() do\n      File.write!(path, normalized)\n      :ok\n    else\n      case File.read(path) do\n        {:ok, expected} ->\n          assert normalized == expected,\n                 \"Snapshot mismatch for `#{relative_path}`. #{@update_snapshot_hint}\"\n\n        {:error, :enoent} ->\n          flunk(\"Missing snapshot fixture `#{relative_path}`. #{@update_snapshot_hint}\")\n\n        {:error, reason} ->\n          flunk(\"Failed reading snapshot fixture `#{relative_path}`: #{inspect(reason)}\")\n      end\n    end\n  end\n\n  def escape_ansi(content) when is_binary(content), do: String.replace(content, <<27>>, \"\\\\e\")\n\n  def strip_ansi(content) when is_binary(content), do: Regex.replace(@ansi_regex, content, \"\")\n\n  def evidence_markdown(raw_ansi_content) when is_binary(raw_ansi_content) do\n    plain =\n      raw_ansi_content\n      |> strip_ansi()\n      |> normalize_content()\n      |> String.trim_trailing(\"\\n\")\n\n    \"```text\\n#{plain}\\n```\\n\"\n  end\n\n  defp snapshot_path(relative_path), do: Path.join(@snapshot_root, relative_path)\n\n  defp update_snapshots? do\n    System.get_env(\"UPDATE_SNAPSHOTS\")\n    |> to_string()\n    |> String.downcase()\n    |> Kernel.in([\"1\", \"true\", \"yes\"])\n  end\n\n  defp normalize_content(content) do\n    content\n    |> String.replace(\"\\r\\n\", \"\\n\")\n    |> String.trim_trailing(\"\\n\")\n    |> Kernel.<>(\"\\n\")\n  end\nend\n"
  },
  {
    "path": "elixir/test/support/test_support.exs",
    "content": "defmodule SymphonyElixir.TestSupport do\n  @workflow_prompt \"You are an agent for this repository.\"\n\n  defmacro __using__(_opts) do\n    quote do\n      use ExUnit.Case\n      import ExUnit.CaptureLog\n\n      alias SymphonyElixir.AgentRunner\n      alias SymphonyElixir.CLI\n      alias SymphonyElixir.Codex.AppServer\n      alias SymphonyElixir.Config\n      alias SymphonyElixir.HttpServer\n      alias SymphonyElixir.Linear.Client\n      alias SymphonyElixir.Linear.Issue\n      alias SymphonyElixir.Orchestrator\n      alias SymphonyElixir.PromptBuilder\n      alias SymphonyElixir.StatusDashboard\n      alias SymphonyElixir.Tracker\n      alias SymphonyElixir.Workflow\n      alias SymphonyElixir.WorkflowStore\n      alias SymphonyElixir.Workspace\n\n      import SymphonyElixir.TestSupport,\n        only: [write_workflow_file!: 1, write_workflow_file!: 2, restore_env: 2, stop_default_http_server: 0]\n\n      setup do\n        workflow_root =\n          Path.join(\n            System.tmp_dir!(),\n            \"symphony-elixir-workflow-#{System.unique_integer([:positive])}\"\n          )\n\n        File.mkdir_p!(workflow_root)\n        workflow_file = Path.join(workflow_root, \"WORKFLOW.md\")\n        write_workflow_file!(workflow_file)\n        Workflow.set_workflow_file_path(workflow_file)\n        if Process.whereis(SymphonyElixir.WorkflowStore), do: SymphonyElixir.WorkflowStore.force_reload()\n        stop_default_http_server()\n\n        on_exit(fn ->\n          Application.delete_env(:symphony_elixir, :workflow_file_path)\n          Application.delete_env(:symphony_elixir, :server_port_override)\n          Application.delete_env(:symphony_elixir, :memory_tracker_issues)\n          Application.delete_env(:symphony_elixir, :memory_tracker_recipient)\n          File.rm_rf(workflow_root)\n        end)\n\n        :ok\n      end\n    end\n  end\n\n  def write_workflow_file!(path, overrides \\\\ []) do\n    workflow = workflow_content(overrides)\n    File.write!(path, workflow)\n\n    if Process.whereis(SymphonyElixir.WorkflowStore) do\n      try do\n        SymphonyElixir.WorkflowStore.force_reload()\n      catch\n        :exit, _reason -> :ok\n      end\n    end\n\n    :ok\n  end\n\n  def restore_env(key, nil), do: System.delete_env(key)\n  def restore_env(key, value), do: System.put_env(key, value)\n\n  def stop_default_http_server do\n    case Enum.find(Supervisor.which_children(SymphonyElixir.Supervisor), fn\n           {SymphonyElixir.HttpServer, _pid, _type, _modules} -> true\n           _child -> false\n         end) do\n      {SymphonyElixir.HttpServer, pid, _type, _modules} when is_pid(pid) ->\n        :ok = Supervisor.terminate_child(SymphonyElixir.Supervisor, SymphonyElixir.HttpServer)\n\n        if Process.alive?(pid) do\n          Process.exit(pid, :normal)\n        end\n\n        :ok\n\n      _ ->\n        :ok\n    end\n  end\n\n  defp workflow_content(overrides) do\n    config =\n      Keyword.merge(\n        [\n          tracker_kind: \"linear\",\n          tracker_endpoint: \"https://api.linear.app/graphql\",\n          tracker_api_token: \"token\",\n          tracker_project_slug: \"project\",\n          tracker_assignee: nil,\n          tracker_active_states: [\"Todo\", \"In Progress\"],\n          tracker_terminal_states: [\"Closed\", \"Cancelled\", \"Canceled\", \"Duplicate\", \"Done\"],\n          poll_interval_ms: 30_000,\n          workspace_root: Path.join(System.tmp_dir!(), \"symphony_workspaces\"),\n          worker_ssh_hosts: [],\n          worker_max_concurrent_agents_per_host: nil,\n          max_concurrent_agents: 10,\n          max_turns: 20,\n          max_retry_backoff_ms: 300_000,\n          max_concurrent_agents_by_state: %{},\n          codex_command: \"codex app-server\",\n          codex_approval_policy: %{reject: %{sandbox_approval: true, rules: true, mcp_elicitations: true}},\n          codex_thread_sandbox: \"workspace-write\",\n          codex_turn_sandbox_policy: nil,\n          codex_turn_timeout_ms: 3_600_000,\n          codex_read_timeout_ms: 5_000,\n          codex_stall_timeout_ms: 300_000,\n          hook_after_create: nil,\n          hook_before_run: nil,\n          hook_after_run: nil,\n          hook_before_remove: nil,\n          hook_timeout_ms: 60_000,\n          observability_enabled: true,\n          observability_refresh_ms: 1_000,\n          observability_render_interval_ms: 16,\n          server_port: nil,\n          server_host: nil,\n          prompt: @workflow_prompt\n        ],\n        overrides\n      )\n\n    tracker_kind = Keyword.get(config, :tracker_kind)\n    tracker_endpoint = Keyword.get(config, :tracker_endpoint)\n    tracker_api_token = Keyword.get(config, :tracker_api_token)\n    tracker_project_slug = Keyword.get(config, :tracker_project_slug)\n    tracker_assignee = Keyword.get(config, :tracker_assignee)\n    tracker_active_states = Keyword.get(config, :tracker_active_states)\n    tracker_terminal_states = Keyword.get(config, :tracker_terminal_states)\n    poll_interval_ms = Keyword.get(config, :poll_interval_ms)\n    workspace_root = Keyword.get(config, :workspace_root)\n    worker_ssh_hosts = Keyword.get(config, :worker_ssh_hosts)\n    worker_max_concurrent_agents_per_host = Keyword.get(config, :worker_max_concurrent_agents_per_host)\n    max_concurrent_agents = Keyword.get(config, :max_concurrent_agents)\n    max_turns = Keyword.get(config, :max_turns)\n    max_retry_backoff_ms = Keyword.get(config, :max_retry_backoff_ms)\n    max_concurrent_agents_by_state = Keyword.get(config, :max_concurrent_agents_by_state)\n    codex_command = Keyword.get(config, :codex_command)\n    codex_approval_policy = Keyword.get(config, :codex_approval_policy)\n    codex_thread_sandbox = Keyword.get(config, :codex_thread_sandbox)\n    codex_turn_sandbox_policy = Keyword.get(config, :codex_turn_sandbox_policy)\n    codex_turn_timeout_ms = Keyword.get(config, :codex_turn_timeout_ms)\n    codex_read_timeout_ms = Keyword.get(config, :codex_read_timeout_ms)\n    codex_stall_timeout_ms = Keyword.get(config, :codex_stall_timeout_ms)\n    hook_after_create = Keyword.get(config, :hook_after_create)\n    hook_before_run = Keyword.get(config, :hook_before_run)\n    hook_after_run = Keyword.get(config, :hook_after_run)\n    hook_before_remove = Keyword.get(config, :hook_before_remove)\n    hook_timeout_ms = Keyword.get(config, :hook_timeout_ms)\n    observability_enabled = Keyword.get(config, :observability_enabled)\n    observability_refresh_ms = Keyword.get(config, :observability_refresh_ms)\n    observability_render_interval_ms = Keyword.get(config, :observability_render_interval_ms)\n    server_port = Keyword.get(config, :server_port)\n    server_host = Keyword.get(config, :server_host)\n    prompt = Keyword.get(config, :prompt)\n\n    sections =\n      [\n        \"---\",\n        \"tracker:\",\n        \"  kind: #{yaml_value(tracker_kind)}\",\n        \"  endpoint: #{yaml_value(tracker_endpoint)}\",\n        \"  api_key: #{yaml_value(tracker_api_token)}\",\n        \"  project_slug: #{yaml_value(tracker_project_slug)}\",\n        \"  assignee: #{yaml_value(tracker_assignee)}\",\n        \"  active_states: #{yaml_value(tracker_active_states)}\",\n        \"  terminal_states: #{yaml_value(tracker_terminal_states)}\",\n        \"polling:\",\n        \"  interval_ms: #{yaml_value(poll_interval_ms)}\",\n        \"workspace:\",\n        \"  root: #{yaml_value(workspace_root)}\",\n        worker_yaml(worker_ssh_hosts, worker_max_concurrent_agents_per_host),\n        \"agent:\",\n        \"  max_concurrent_agents: #{yaml_value(max_concurrent_agents)}\",\n        \"  max_turns: #{yaml_value(max_turns)}\",\n        \"  max_retry_backoff_ms: #{yaml_value(max_retry_backoff_ms)}\",\n        \"  max_concurrent_agents_by_state: #{yaml_value(max_concurrent_agents_by_state)}\",\n        \"codex:\",\n        \"  command: #{yaml_value(codex_command)}\",\n        \"  approval_policy: #{yaml_value(codex_approval_policy)}\",\n        \"  thread_sandbox: #{yaml_value(codex_thread_sandbox)}\",\n        \"  turn_sandbox_policy: #{yaml_value(codex_turn_sandbox_policy)}\",\n        \"  turn_timeout_ms: #{yaml_value(codex_turn_timeout_ms)}\",\n        \"  read_timeout_ms: #{yaml_value(codex_read_timeout_ms)}\",\n        \"  stall_timeout_ms: #{yaml_value(codex_stall_timeout_ms)}\",\n        hooks_yaml(hook_after_create, hook_before_run, hook_after_run, hook_before_remove, hook_timeout_ms),\n        observability_yaml(observability_enabled, observability_refresh_ms, observability_render_interval_ms),\n        server_yaml(server_port, server_host),\n        \"---\",\n        prompt\n      ]\n      |> Enum.reject(&(&1 in [nil, \"\"]))\n\n    Enum.join(sections, \"\\n\") <> \"\\n\"\n  end\n\n  defp yaml_value(value) when is_binary(value) do\n    \"\\\"\" <> String.replace(value, \"\\\"\", \"\\\\\\\"\") <> \"\\\"\"\n  end\n\n  defp yaml_value(value) when is_integer(value), do: to_string(value)\n  defp yaml_value(true), do: \"true\"\n  defp yaml_value(false), do: \"false\"\n  defp yaml_value(nil), do: \"null\"\n\n  defp yaml_value(values) when is_list(values) do\n    \"[\" <> Enum.map_join(values, \", \", &yaml_value/1) <> \"]\"\n  end\n\n  defp yaml_value(values) when is_map(values) do\n    \"{\" <>\n      Enum.map_join(values, \", \", fn {key, value} ->\n        \"#{yaml_value(to_string(key))}: #{yaml_value(value)}\"\n      end) <> \"}\"\n  end\n\n  defp yaml_value(value), do: yaml_value(to_string(value))\n\n  defp hooks_yaml(nil, nil, nil, nil, timeout_ms), do: \"hooks:\\n  timeout_ms: #{yaml_value(timeout_ms)}\"\n\n  defp hooks_yaml(hook_after_create, hook_before_run, hook_after_run, hook_before_remove, timeout_ms) do\n    [\n      \"hooks:\",\n      \"  timeout_ms: #{yaml_value(timeout_ms)}\",\n      hook_entry(\"after_create\", hook_after_create),\n      hook_entry(\"before_run\", hook_before_run),\n      hook_entry(\"after_run\", hook_after_run),\n      hook_entry(\"before_remove\", hook_before_remove)\n    ]\n    |> Enum.reject(&is_nil/1)\n    |> Enum.join(\"\\n\")\n  end\n\n  defp worker_yaml(ssh_hosts, max_concurrent_agents_per_host)\n       when ssh_hosts in [nil, []] and is_nil(max_concurrent_agents_per_host),\n       do: nil\n\n  defp worker_yaml(ssh_hosts, max_concurrent_agents_per_host) do\n    [\n      \"worker:\",\n      ssh_hosts not in [nil, []] && \"  ssh_hosts: #{yaml_value(ssh_hosts)}\",\n      !is_nil(max_concurrent_agents_per_host) &&\n        \"  max_concurrent_agents_per_host: #{yaml_value(max_concurrent_agents_per_host)}\"\n    ]\n    |> Enum.reject(&(&1 in [nil, false]))\n    |> Enum.join(\"\\n\")\n  end\n\n  defp observability_yaml(enabled, refresh_ms, render_interval_ms) do\n    [\n      \"observability:\",\n      \"  dashboard_enabled: #{yaml_value(enabled)}\",\n      \"  refresh_ms: #{yaml_value(refresh_ms)}\",\n      \"  render_interval_ms: #{yaml_value(render_interval_ms)}\"\n    ]\n    |> Enum.join(\"\\n\")\n  end\n\n  defp server_yaml(nil, nil), do: nil\n\n  defp server_yaml(port, host) do\n    [\n      \"server:\",\n      port && \"  port: #{yaml_value(port)}\",\n      host && \"  host: #{yaml_value(host)}\"\n    ]\n    |> Enum.reject(&is_nil/1)\n    |> Enum.join(\"\\n\")\n  end\n\n  defp hook_entry(_name, nil), do: nil\n\n  defp hook_entry(name, command) when is_binary(command) do\n    indented =\n      command\n      |> String.split(\"\\n\")\n      |> Enum.map_join(\"\\n\", &(\"    \" <> &1))\n\n    \"  #{name}: |\\n#{indented}\"\n  end\nend\n"
  },
  {
    "path": "elixir/test/symphony_elixir/app_server_test.exs",
    "content": "defmodule SymphonyElixir.AppServerTest do\n  use SymphonyElixir.TestSupport\n\n  test \"app server rejects the workspace root and paths outside workspace root\" do\n    test_root =\n      Path.join(\n        System.tmp_dir!(),\n        \"symphony-elixir-app-server-cwd-guard-#{System.unique_integer([:positive])}\"\n      )\n\n    try do\n      workspace_root = Path.join(test_root, \"workspaces\")\n      outside_workspace = Path.join(test_root, \"outside\")\n\n      File.mkdir_p!(workspace_root)\n      File.mkdir_p!(outside_workspace)\n\n      write_workflow_file!(Workflow.workflow_file_path(),\n        workspace_root: workspace_root\n      )\n\n      issue = %Issue{\n        id: \"issue-workspace-guard\",\n        identifier: \"MT-999\",\n        title: \"Validate workspace guard\",\n        description: \"Ensure app-server refuses invalid cwd targets\",\n        state: \"In Progress\",\n        url: \"https://example.org/issues/MT-999\",\n        labels: [\"backend\"]\n      }\n\n      assert {:error, {:invalid_workspace_cwd, :workspace_root, _path}} =\n               AppServer.run(workspace_root, \"guard\", issue)\n\n      assert {:error, {:invalid_workspace_cwd, :outside_workspace_root, _path, _root}} =\n               AppServer.run(outside_workspace, \"guard\", issue)\n    after\n      File.rm_rf(test_root)\n    end\n  end\n\n  test \"app server rejects symlink escape cwd paths under the workspace root\" do\n    test_root =\n      Path.join(\n        System.tmp_dir!(),\n        \"symphony-elixir-app-server-symlink-cwd-guard-#{System.unique_integer([:positive])}\"\n      )\n\n    try do\n      workspace_root = Path.join(test_root, \"workspaces\")\n      outside_workspace = Path.join(test_root, \"outside\")\n      symlink_workspace = Path.join(workspace_root, \"MT-1000\")\n\n      File.mkdir_p!(workspace_root)\n      File.mkdir_p!(outside_workspace)\n      File.ln_s!(outside_workspace, symlink_workspace)\n\n      write_workflow_file!(Workflow.workflow_file_path(),\n        workspace_root: workspace_root\n      )\n\n      issue = %Issue{\n        id: \"issue-workspace-symlink-guard\",\n        identifier: \"MT-1000\",\n        title: \"Validate symlink workspace guard\",\n        description: \"Ensure app-server refuses symlink escape cwd targets\",\n        state: \"In Progress\",\n        url: \"https://example.org/issues/MT-1000\",\n        labels: [\"backend\"]\n      }\n\n      assert {:error, {:invalid_workspace_cwd, :symlink_escape, ^symlink_workspace, _root}} =\n               AppServer.run(symlink_workspace, \"guard\", issue)\n    after\n      File.rm_rf(test_root)\n    end\n  end\n\n  test \"app server passes explicit turn sandbox policies through unchanged\" do\n    test_root =\n      Path.join(\n        System.tmp_dir!(),\n        \"symphony-elixir-app-server-supported-turn-policies-#{System.unique_integer([:positive])}\"\n      )\n\n    try do\n      workspace_root = Path.join(test_root, \"workspaces\")\n      workspace = Path.join(workspace_root, \"MT-1001\")\n      codex_binary = Path.join(test_root, \"fake-codex\")\n      trace_file = Path.join(test_root, \"codex-supported-turn-policies.trace\")\n      previous_trace = System.get_env(\"SYMP_TEST_CODEx_TRACE\")\n\n      on_exit(fn ->\n        if is_binary(previous_trace) do\n          System.put_env(\"SYMP_TEST_CODEx_TRACE\", previous_trace)\n        else\n          System.delete_env(\"SYMP_TEST_CODEx_TRACE\")\n        end\n      end)\n\n      System.put_env(\"SYMP_TEST_CODEx_TRACE\", trace_file)\n      File.mkdir_p!(workspace)\n\n      File.write!(codex_binary, \"\"\"\n      #!/bin/sh\n      trace_file=\"${SYMP_TEST_CODEx_TRACE:-/tmp/codex-supported-turn-policies.trace}\"\n      count=0\n\n      while IFS= read -r line; do\n        count=$((count + 1))\n        printf 'JSON:%s\\\\n' \"$line\" >> \"$trace_file\"\n\n        case \"$count\" in\n          1)\n            printf '%s\\\\n' '{\"id\":1,\"result\":{}}'\n            ;;\n          2)\n            printf '%s\\\\n' '{\"id\":2,\"result\":{\"thread\":{\"id\":\"thread-1001\"}}}'\n            ;;\n          3)\n            printf '%s\\\\n' '{\"id\":3,\"result\":{\"turn\":{\"id\":\"turn-1001\"}}}'\n            ;;\n          4)\n            printf '%s\\\\n' '{\"method\":\"turn/completed\"}'\n            exit 0\n            ;;\n          *)\n            exit 0\n            ;;\n        esac\n      done\n      \"\"\")\n\n      File.chmod!(codex_binary, 0o755)\n\n      issue = %Issue{\n        id: \"issue-supported-turn-policies\",\n        identifier: \"MT-1001\",\n        title: \"Validate explicit turn sandbox policy passthrough\",\n        description: \"Ensure runtime startup forwards configured turn sandbox policies unchanged\",\n        state: \"In Progress\",\n        url: \"https://example.org/issues/MT-1001\",\n        labels: [\"backend\"]\n      }\n\n      policy_cases = [\n        %{\"type\" => \"dangerFullAccess\"},\n        %{\"type\" => \"externalSandbox\", \"profile\" => \"remote-ci\"},\n        %{\"type\" => \"workspaceWrite\", \"writableRoots\" => [\"relative/path\"], \"networkAccess\" => true},\n        %{\"type\" => \"futureSandbox\", \"nested\" => %{\"flag\" => true}}\n      ]\n\n      Enum.each(policy_cases, fn configured_policy ->\n        File.rm(trace_file)\n\n        write_workflow_file!(Workflow.workflow_file_path(),\n          workspace_root: workspace_root,\n          codex_command: \"#{codex_binary} app-server\",\n          codex_turn_sandbox_policy: configured_policy\n        )\n\n        assert {:ok, _result} = AppServer.run(workspace, \"Validate supported turn policy\", issue)\n\n        trace = File.read!(trace_file)\n        lines = String.split(trace, \"\\n\", trim: true)\n\n        assert Enum.any?(lines, fn line ->\n                 if String.starts_with?(line, \"JSON:\") do\n                   line\n                   |> String.trim_leading(\"JSON:\")\n                   |> Jason.decode!()\n                   |> then(fn payload ->\n                     payload[\"method\"] == \"turn/start\" &&\n                       get_in(payload, [\"params\", \"sandboxPolicy\"]) == configured_policy\n                   end)\n                 else\n                   false\n                 end\n               end)\n      end)\n    after\n      File.rm_rf(test_root)\n    end\n  end\n\n  test \"app server marks request-for-input events as a hard failure\" do\n    test_root =\n      Path.join(\n        System.tmp_dir!(),\n        \"symphony-elixir-app-server-input-#{System.unique_integer([:positive])}\"\n      )\n\n    try do\n      workspace_root = Path.join(test_root, \"workspaces\")\n      workspace = Path.join(workspace_root, \"MT-88\")\n      codex_binary = Path.join(test_root, \"fake-codex\")\n      trace_file = Path.join(test_root, \"codex-input.trace\")\n      previous_trace = System.get_env(\"SYMP_TEST_CODEx_TRACE\")\n\n      on_exit(fn ->\n        if is_binary(previous_trace) do\n          System.put_env(\"SYMP_TEST_CODEx_TRACE\", previous_trace)\n        else\n          System.delete_env(\"SYMP_TEST_CODEx_TRACE\")\n        end\n      end)\n\n      System.put_env(\"SYMP_TEST_CODEx_TRACE\", trace_file)\n      File.mkdir_p!(workspace)\n\n      File.write!(codex_binary, \"\"\"\n      #!/bin/sh\n      trace_file=\"${SYMP_TEST_CODEx_TRACE:-/tmp/codex-input.trace}\"\n      count=0\n      while IFS= read -r line; do\n        count=$((count + 1))\n        printf 'JSON:%s\\\\n' \\\"$line\\\" >> \\\"$trace_file\\\"\n\n        case \\\"$count\\\" in\n          1)\n            printf '%s\\\\n' '{\\\"id\\\":1,\\\"result\\\":{}}'\n            ;;\n          2)\n            printf '%s\\\\n' '{\\\"id\\\":2,\\\"result\\\":{\\\"thread\\\":{\\\"id\\\":\\\"thread-88\\\"}}}'\n            ;;\n          3)\n            printf '%s\\\\n' '{\\\"id\\\":3,\\\"result\\\":{\\\"turn\\\":{\\\"id\\\":\\\"turn-88\\\"}}}'\n            ;;\n          4)\n            printf '%s\\\\n' '{\\\"method\\\":\\\"turn/input_required\\\",\\\"id\\\":\\\"resp-1\\\",\\\"params\\\":{\\\"requiresInput\\\":true,\\\"reason\\\":\\\"blocked\\\"}}'\n            ;;\n          *)\n            exit 0\n            ;;\n        esac\n      done\n      \"\"\")\n\n      File.chmod!(codex_binary, 0o755)\n\n      write_workflow_file!(Workflow.workflow_file_path(),\n        workspace_root: workspace_root,\n        codex_command: \"#{codex_binary} app-server\"\n      )\n\n      issue = %Issue{\n        id: \"issue-input\",\n        identifier: \"MT-88\",\n        title: \"Input needed\",\n        description: \"Cannot satisfy codex input\",\n        state: \"In Progress\",\n        url: \"https://example.org/issues/MT-88\",\n        labels: [\"backend\"]\n      }\n\n      assert {:error, {:turn_input_required, payload}} =\n               AppServer.run(workspace, \"Needs input\", issue)\n\n      assert payload[\"method\"] == \"turn/input_required\"\n    after\n      File.rm_rf(test_root)\n    end\n  end\n\n  test \"app server fails when command execution approval is required under safer defaults\" do\n    test_root =\n      Path.join(\n        System.tmp_dir!(),\n        \"symphony-elixir-app-server-approval-required-#{System.unique_integer([:positive])}\"\n      )\n\n    try do\n      workspace_root = Path.join(test_root, \"workspaces\")\n      workspace = Path.join(workspace_root, \"MT-89\")\n      codex_binary = Path.join(test_root, \"fake-codex\")\n      File.mkdir_p!(workspace)\n\n      File.write!(codex_binary, \"\"\"\n      #!/bin/sh\n      count=0\n      while IFS= read -r _line; do\n        count=$((count + 1))\n\n        case \"$count\" in\n          1)\n            printf '%s\\\\n' '{\"id\":1,\"result\":{}}'\n            ;;\n          2)\n            printf '%s\\\\n' '{\"id\":2,\"result\":{\"thread\":{\"id\":\"thread-89\"}}}'\n            ;;\n          3)\n            printf '%s\\\\n' '{\"id\":3,\"result\":{\"turn\":{\"id\":\"turn-89\"}}}'\n            printf '%s\\\\n' '{\"id\":99,\"method\":\"item/commandExecution/requestApproval\",\"params\":{\"command\":\"gh pr view\",\"cwd\":\"/tmp\",\"reason\":\"need approval\"}}'\n            ;;\n          *)\n            sleep 1\n            ;;\n        esac\n      done\n      \"\"\")\n\n      File.chmod!(codex_binary, 0o755)\n\n      write_workflow_file!(Workflow.workflow_file_path(),\n        workspace_root: workspace_root,\n        codex_command: \"#{codex_binary} app-server\"\n      )\n\n      issue = %Issue{\n        id: \"issue-approval-required\",\n        identifier: \"MT-89\",\n        title: \"Approval required\",\n        description: \"Ensure safer defaults do not auto approve requests\",\n        state: \"In Progress\",\n        url: \"https://example.org/issues/MT-89\",\n        labels: [\"backend\"]\n      }\n\n      assert {:error, {:approval_required, payload}} =\n               AppServer.run(workspace, \"Handle approval request\", issue)\n\n      assert payload[\"method\"] == \"item/commandExecution/requestApproval\"\n    after\n      File.rm_rf(test_root)\n    end\n  end\n\n  test \"app server auto-approves command execution approval requests when approval policy is never\" do\n    test_root =\n      Path.join(\n        System.tmp_dir!(),\n        \"symphony-elixir-app-server-auto-approve-#{System.unique_integer([:positive])}\"\n      )\n\n    try do\n      workspace_root = Path.join(test_root, \"workspaces\")\n      workspace = Path.join(workspace_root, \"MT-89\")\n      codex_binary = Path.join(test_root, \"fake-codex\")\n      trace_file = Path.join(test_root, \"codex-auto-approve.trace\")\n      previous_trace = System.get_env(\"SYMP_TEST_CODex_TRACE\")\n\n      on_exit(fn ->\n        if is_binary(previous_trace) do\n          System.put_env(\"SYMP_TEST_CODex_TRACE\", previous_trace)\n        else\n          System.delete_env(\"SYMP_TEST_CODex_TRACE\")\n        end\n      end)\n\n      System.put_env(\"SYMP_TEST_CODex_TRACE\", trace_file)\n      File.mkdir_p!(workspace)\n\n      File.write!(codex_binary, \"\"\"\n      #!/bin/sh\n      trace_file=\"${SYMP_TEST_CODex_TRACE:-/tmp/codex-auto-approve.trace}\"\n      count=0\n      while IFS= read -r line; do\n        count=$((count + 1))\n        printf 'JSON:%s\\\\n' \\\"$line\\\" >> \\\"$trace_file\\\"\n\n        case \\\"$count\\\" in\n          1)\n            printf '%s\\\\n' '{\\\"id\\\":1,\\\"result\\\":{}}'\n            ;;\n          2)\n            ;;\n          3)\n            printf '%s\\\\n' '{\\\"id\\\":2,\\\"result\\\":{\\\"thread\\\":{\\\"id\\\":\\\"thread-89\\\"}}}'\n            ;;\n          4)\n            printf '%s\\\\n' '{\\\"id\\\":3,\\\"result\\\":{\\\"turn\\\":{\\\"id\\\":\\\"turn-89\\\"}}}'\n            printf '%s\\\\n' '{\\\"id\\\":99,\\\"method\\\":\\\"item/commandExecution/requestApproval\\\",\\\"params\\\":{\\\"command\\\":\\\"gh pr view\\\",\\\"cwd\\\":\\\"/tmp\\\",\\\"reason\\\":\\\"need approval\\\"}}'\n            ;;\n          5)\n            printf '%s\\\\n' '{\\\"method\\\":\\\"turn/completed\\\"}'\n            exit 0\n            ;;\n          *)\n            exit 0\n            ;;\n        esac\n      done\n      \"\"\")\n\n      File.chmod!(codex_binary, 0o755)\n\n      write_workflow_file!(Workflow.workflow_file_path(),\n        workspace_root: workspace_root,\n        codex_command: \"#{codex_binary} app-server\",\n        codex_approval_policy: \"never\"\n      )\n\n      issue = %Issue{\n        id: \"issue-auto-approve\",\n        identifier: \"MT-89\",\n        title: \"Auto approve request\",\n        description: \"Ensure app-server approval requests are handled automatically\",\n        state: \"In Progress\",\n        url: \"https://example.org/issues/MT-89\",\n        labels: [\"backend\"]\n      }\n\n      assert {:ok, _result} = AppServer.run(workspace, \"Handle approval request\", issue)\n\n      trace = File.read!(trace_file)\n      lines = String.split(trace, \"\\n\", trim: true)\n\n      assert Enum.any?(lines, fn line ->\n               if String.starts_with?(line, \"JSON:\") do\n                 payload =\n                   line\n                   |> String.trim_leading(\"JSON:\")\n                   |> Jason.decode!()\n\n                 payload[\"id\"] == 1 and\n                   get_in(payload, [\"params\", \"capabilities\", \"experimentalApi\"]) == true\n               else\n                 false\n               end\n             end)\n\n      assert Enum.any?(lines, fn line ->\n               if String.starts_with?(line, \"JSON:\") do\n                 payload =\n                   line\n                   |> String.trim_leading(\"JSON:\")\n                   |> Jason.decode!()\n\n                 payload[\"id\"] == 2 and\n                   case get_in(payload, [\"params\", \"dynamicTools\"]) do\n                     [\n                       %{\n                         \"description\" => description,\n                         \"inputSchema\" => %{\"required\" => [\"query\"]},\n                         \"name\" => \"linear_graphql\"\n                       }\n                     ] ->\n                       description =~ \"Linear\"\n\n                     _ ->\n                       false\n                   end\n               else\n                 false\n               end\n             end)\n\n      assert Enum.any?(lines, fn line ->\n               if String.starts_with?(line, \"JSON:\") do\n                 payload =\n                   line\n                   |> String.trim_leading(\"JSON:\")\n                   |> Jason.decode!()\n\n                 payload[\"id\"] == 99 and get_in(payload, [\"result\", \"decision\"]) == \"acceptForSession\"\n               else\n                 false\n               end\n             end)\n    after\n      File.rm_rf(test_root)\n    end\n  end\n\n  test \"app server auto-approves MCP tool approval prompts when approval policy is never\" do\n    test_root =\n      Path.join(\n        System.tmp_dir!(),\n        \"symphony-elixir-app-server-tool-user-input-auto-approve-#{System.unique_integer([:positive])}\"\n      )\n\n    try do\n      workspace_root = Path.join(test_root, \"workspaces\")\n      workspace = Path.join(workspace_root, \"MT-717\")\n      codex_binary = Path.join(test_root, \"fake-codex\")\n      trace_file = Path.join(test_root, \"codex-tool-user-input-auto-approve.trace\")\n      previous_trace = System.get_env(\"SYMP_TEST_CODEx_TRACE\")\n\n      on_exit(fn ->\n        if is_binary(previous_trace) do\n          System.put_env(\"SYMP_TEST_CODEx_TRACE\", previous_trace)\n        else\n          System.delete_env(\"SYMP_TEST_CODEx_TRACE\")\n        end\n      end)\n\n      System.put_env(\"SYMP_TEST_CODEx_TRACE\", trace_file)\n      File.mkdir_p!(workspace)\n\n      File.write!(codex_binary, \"\"\"\n      #!/bin/sh\n      trace_file=\"${SYMP_TEST_CODEx_TRACE:-/tmp/codex-tool-user-input-auto-approve.trace}\"\n      count=0\n      while IFS= read -r line; do\n        count=$((count + 1))\n        printf 'JSON:%s\\\\n' \\\"$line\\\" >> \\\"$trace_file\\\"\n\n        case \\\"$count\\\" in\n          1)\n            printf '%s\\\\n' '{\\\"id\\\":1,\\\"result\\\":{}}'\n            ;;\n          2)\n            ;;\n          3)\n            printf '%s\\\\n' '{\\\"id\\\":2,\\\"result\\\":{\\\"thread\\\":{\\\"id\\\":\\\"thread-717\\\"}}}'\n            ;;\n          4)\n            printf '%s\\\\n' '{\\\"id\\\":3,\\\"result\\\":{\\\"turn\\\":{\\\"id\\\":\\\"turn-717\\\"}}}'\n            printf '%s\\\\n' '{\\\"id\\\":110,\\\"method\\\":\\\"item/tool/requestUserInput\\\",\\\"params\\\":{\\\"itemId\\\":\\\"call-717\\\",\\\"questions\\\":[{\\\"header\\\":\\\"Approve app tool call?\\\",\\\"id\\\":\\\"mcp_tool_call_approval_call-717\\\",\\\"isOther\\\":false,\\\"isSecret\\\":false,\\\"options\\\":[{\\\"description\\\":\\\"Run the tool and continue.\\\",\\\"label\\\":\\\"Approve Once\\\"},{\\\"description\\\":\\\"Run the tool and remember this choice for this session.\\\",\\\"label\\\":\\\"Approve this Session\\\"},{\\\"description\\\":\\\"Decline this tool call and continue.\\\",\\\"label\\\":\\\"Deny\\\"},{\\\"description\\\":\\\"Cancel this tool call\\\",\\\"label\\\":\\\"Cancel\\\"}],\\\"question\\\":\\\"The linear MCP server wants to run the tool \\\\\\\"Save issue\\\\\\\", which may modify or delete data. Allow this action?\\\"}],\\\"threadId\\\":\\\"thread-717\\\",\\\"turnId\\\":\\\"turn-717\\\"}}'\n            ;;\n          5)\n            printf '%s\\\\n' '{\\\"method\\\":\\\"turn/completed\\\"}'\n            exit 0\n            ;;\n          *)\n            exit 0\n            ;;\n        esac\n      done\n      \"\"\")\n\n      File.chmod!(codex_binary, 0o755)\n\n      write_workflow_file!(Workflow.workflow_file_path(),\n        workspace_root: workspace_root,\n        codex_command: \"#{codex_binary} app-server\",\n        codex_approval_policy: \"never\"\n      )\n\n      issue = %Issue{\n        id: \"issue-tool-user-input-auto-approve\",\n        identifier: \"MT-717\",\n        title: \"Auto approve MCP tool request user input\",\n        description: \"Ensure app tool approval prompts continue automatically\",\n        state: \"In Progress\",\n        url: \"https://example.org/issues/MT-717\",\n        labels: [\"backend\"]\n      }\n\n      assert {:ok, _result} = AppServer.run(workspace, \"Handle tool approval prompt\", issue)\n\n      trace = File.read!(trace_file)\n      lines = String.split(trace, \"\\n\", trim: true)\n\n      assert Enum.any?(lines, fn line ->\n               if String.starts_with?(line, \"JSON:\") do\n                 payload =\n                   line\n                   |> String.trim_leading(\"JSON:\")\n                   |> Jason.decode!()\n\n                 payload[\"id\"] == 110 and\n                   get_in(payload, [\"result\", \"answers\", \"mcp_tool_call_approval_call-717\", \"answers\"]) ==\n                     [\"Approve this Session\"]\n               else\n                 false\n               end\n             end)\n    after\n      File.rm_rf(test_root)\n    end\n  end\n\n  test \"app server sends a generic non-interactive answer for freeform tool input prompts\" do\n    test_root =\n      Path.join(\n        System.tmp_dir!(),\n        \"symphony-elixir-app-server-tool-user-input-required-#{System.unique_integer([:positive])}\"\n      )\n\n    try do\n      workspace_root = Path.join(test_root, \"workspaces\")\n      workspace = Path.join(workspace_root, \"MT-718\")\n      codex_binary = Path.join(test_root, \"fake-codex\")\n      File.mkdir_p!(workspace)\n\n      File.write!(codex_binary, \"\"\"\n      #!/bin/sh\n      count=0\n      while IFS= read -r _line; do\n        count=$((count + 1))\n\n        case \"$count\" in\n          1)\n            printf '%s\\\\n' '{\"id\":1,\"result\":{}}'\n            ;;\n          2)\n            ;;\n          3)\n            printf '%s\\\\n' '{\"id\":2,\"result\":{\"thread\":{\"id\":\"thread-718\"}}}'\n            ;;\n          4)\n            printf '%s\\\\n' '{\"id\":3,\"result\":{\"turn\":{\"id\":\"turn-718\"}}}'\n            printf '%s\\\\n' '{\"id\":111,\"method\":\"item/tool/requestUserInput\",\"params\":{\"itemId\":\"call-718\",\"questions\":[{\"header\":\"Provide context\",\"id\":\"freeform-718\",\"isOther\":false,\"isSecret\":false,\"options\":null,\"question\":\"What comment should I post back to the issue?\"}],\"threadId\":\"thread-718\",\"turnId\":\"turn-718\"}}'\n            ;;\n          5)\n            printf '%s\\\\n' '{\"method\":\"turn/completed\"}'\n            exit 0\n            ;;\n          *)\n            exit 0\n            ;;\n        esac\n      done\n      \"\"\")\n\n      File.chmod!(codex_binary, 0o755)\n\n      write_workflow_file!(Workflow.workflow_file_path(),\n        workspace_root: workspace_root,\n        codex_command: \"#{codex_binary} app-server\",\n        codex_approval_policy: \"never\"\n      )\n\n      issue = %Issue{\n        id: \"issue-tool-user-input-required\",\n        identifier: \"MT-718\",\n        title: \"Non interactive tool input answer\",\n        description: \"Ensure arbitrary tool prompts receive a generic answer\",\n        state: \"In Progress\",\n        url: \"https://example.org/issues/MT-718\",\n        labels: [\"backend\"]\n      }\n\n      on_message = fn message -> send(self(), {:app_server_message, message}) end\n\n      assert {:ok, _result} =\n               AppServer.run(workspace, \"Handle generic tool input\", issue, on_message: on_message)\n\n      assert_received {:app_server_message,\n                       %{\n                         event: :tool_input_auto_answered,\n                         answer: \"This is a non-interactive session. Operator input is unavailable.\"\n                       }}\n    after\n      File.rm_rf(test_root)\n    end\n  end\n\n  test \"app server sends a generic non-interactive answer for option-based tool input prompts\" do\n    test_root =\n      Path.join(\n        System.tmp_dir!(),\n        \"symphony-elixir-app-server-tool-user-input-options-#{System.unique_integer([:positive])}\"\n      )\n\n    try do\n      workspace_root = Path.join(test_root, \"workspaces\")\n      workspace = Path.join(workspace_root, \"MT-719\")\n      codex_binary = Path.join(test_root, \"fake-codex\")\n      trace_file = Path.join(test_root, \"codex-tool-user-input-options.trace\")\n      previous_trace = System.get_env(\"SYMP_TEST_CODEx_TRACE\")\n\n      on_exit(fn ->\n        if is_binary(previous_trace) do\n          System.put_env(\"SYMP_TEST_CODEx_TRACE\", previous_trace)\n        else\n          System.delete_env(\"SYMP_TEST_CODEx_TRACE\")\n        end\n      end)\n\n      System.put_env(\"SYMP_TEST_CODEx_TRACE\", trace_file)\n      File.mkdir_p!(workspace)\n\n      File.write!(codex_binary, \"\"\"\n      #!/bin/sh\n      trace_file=\"${SYMP_TEST_CODEx_TRACE:-/tmp/codex-tool-user-input-options.trace}\"\n      count=0\n      while IFS= read -r line; do\n        count=$((count + 1))\n        printf 'JSON:%s\\\\n' \\\"$line\\\" >> \\\"$trace_file\\\"\n\n        case \\\"$count\\\" in\n          1)\n            printf '%s\\\\n' '{\\\"id\\\":1,\\\"result\\\":{}}'\n            ;;\n          2)\n            ;;\n          3)\n            printf '%s\\\\n' '{\\\"id\\\":2,\\\"result\\\":{\\\"thread\\\":{\\\"id\\\":\\\"thread-719\\\"}}}'\n            ;;\n          4)\n            printf '%s\\\\n' '{\\\"id\\\":3,\\\"result\\\":{\\\"turn\\\":{\\\"id\\\":\\\"turn-719\\\"}}}'\n            printf '%s\\\\n' '{\\\"id\\\":112,\\\"method\\\":\\\"item/tool/requestUserInput\\\",\\\"params\\\":{\\\"itemId\\\":\\\"call-719\\\",\\\"questions\\\":[{\\\"header\\\":\\\"Choose an action\\\",\\\"id\\\":\\\"options-719\\\",\\\"isOther\\\":false,\\\"isSecret\\\":false,\\\"options\\\":[{\\\"description\\\":\\\"Use the default behavior.\\\",\\\"label\\\":\\\"Use default\\\"},{\\\"description\\\":\\\"Skip this step.\\\",\\\"label\\\":\\\"Skip\\\"}],\\\"question\\\":\\\"How should I proceed?\\\"}],\\\"threadId\\\":\\\"thread-719\\\",\\\"turnId\\\":\\\"turn-719\\\"}}'\n            ;;\n          5)\n            printf '%s\\\\n' '{\\\"method\\\":\\\"turn/completed\\\"}'\n            exit 0\n            ;;\n          *)\n            exit 0\n            ;;\n        esac\n      done\n      \"\"\")\n\n      File.chmod!(codex_binary, 0o755)\n\n      write_workflow_file!(Workflow.workflow_file_path(),\n        workspace_root: workspace_root,\n        codex_command: \"#{codex_binary} app-server\"\n      )\n\n      issue = %Issue{\n        id: \"issue-tool-user-input-options\",\n        identifier: \"MT-719\",\n        title: \"Option based tool input answer\",\n        description: \"Ensure option prompts receive a generic non-interactive answer\",\n        state: \"In Progress\",\n        url: \"https://example.org/issues/MT-719\",\n        labels: [\"backend\"]\n      }\n\n      assert {:ok, _result} =\n               AppServer.run(workspace, \"Handle option based tool input\", issue)\n\n      trace = File.read!(trace_file)\n      lines = String.split(trace, \"\\n\", trim: true)\n\n      assert Enum.any?(lines, fn line ->\n               if String.starts_with?(line, \"JSON:\") do\n                 payload =\n                   line\n                   |> String.trim_leading(\"JSON:\")\n                   |> Jason.decode!()\n\n                 payload[\"id\"] == 112 and\n                   get_in(payload, [\"result\", \"answers\", \"options-719\", \"answers\"]) == [\n                     \"This is a non-interactive session. Operator input is unavailable.\"\n                   ]\n               else\n                 false\n               end\n             end)\n    after\n      File.rm_rf(test_root)\n    end\n  end\n\n  test \"app server rejects unsupported dynamic tool calls without stalling\" do\n    test_root =\n      Path.join(\n        System.tmp_dir!(),\n        \"symphony-elixir-app-server-tool-call-#{System.unique_integer([:positive])}\"\n      )\n\n    try do\n      workspace_root = Path.join(test_root, \"workspaces\")\n      workspace = Path.join(workspace_root, \"MT-90\")\n      codex_binary = Path.join(test_root, \"fake-codex\")\n      trace_file = Path.join(test_root, \"codex-tool-call.trace\")\n      previous_trace = System.get_env(\"SYMP_TEST_CODEx_TRACE\")\n\n      on_exit(fn ->\n        if is_binary(previous_trace) do\n          System.put_env(\"SYMP_TEST_CODEx_TRACE\", previous_trace)\n        else\n          System.delete_env(\"SYMP_TEST_CODEx_TRACE\")\n        end\n      end)\n\n      System.put_env(\"SYMP_TEST_CODEx_TRACE\", trace_file)\n      File.mkdir_p!(workspace)\n\n      File.write!(codex_binary, \"\"\"\n      #!/bin/sh\n      trace_file=\"${SYMP_TEST_CODEx_TRACE:-/tmp/codex-tool-call.trace}\"\n      count=0\n      while IFS= read -r line; do\n        count=$((count + 1))\n        printf 'JSON:%s\\\\n' \\\"$line\\\" >> \\\"$trace_file\\\"\n\n        case \\\"$count\\\" in\n          1)\n            printf '%s\\\\n' '{\\\"id\\\":1,\\\"result\\\":{}}'\n            ;;\n          2)\n            ;;\n          3)\n            printf '%s\\\\n' '{\\\"id\\\":2,\\\"result\\\":{\\\"thread\\\":{\\\"id\\\":\\\"thread-90\\\"}}}'\n            ;;\n          4)\n            printf '%s\\\\n' '{\\\"id\\\":3,\\\"result\\\":{\\\"turn\\\":{\\\"id\\\":\\\"turn-90\\\"}}}'\n            printf '%s\\\\n' '{\\\"id\\\":101,\\\"method\\\":\\\"item/tool/call\\\",\\\"params\\\":{\\\"tool\\\":\\\"some_tool\\\",\\\"callId\\\":\\\"call-90\\\",\\\"threadId\\\":\\\"thread-90\\\",\\\"turnId\\\":\\\"turn-90\\\",\\\"arguments\\\":{}}}'\n            ;;\n          5)\n            printf '%s\\\\n' '{\\\"method\\\":\\\"turn/completed\\\"}'\n            exit 0\n            ;;\n          *)\n            exit 0\n            ;;\n        esac\n      done\n      \"\"\")\n\n      File.chmod!(codex_binary, 0o755)\n\n      write_workflow_file!(Workflow.workflow_file_path(),\n        workspace_root: workspace_root,\n        codex_command: \"#{codex_binary} app-server\"\n      )\n\n      issue = %Issue{\n        id: \"issue-tool-call\",\n        identifier: \"MT-90\",\n        title: \"Unsupported tool call\",\n        description: \"Ensure unsupported tool calls do not stall a turn\",\n        state: \"In Progress\",\n        url: \"https://example.org/issues/MT-90\",\n        labels: [\"backend\"]\n      }\n\n      assert {:ok, _result} = AppServer.run(workspace, \"Reject unsupported tool calls\", issue)\n\n      trace = File.read!(trace_file)\n      lines = String.split(trace, \"\\n\", trim: true)\n\n      assert Enum.any?(lines, fn line ->\n               if String.starts_with?(line, \"JSON:\") do\n                 payload =\n                   line\n                   |> String.trim_leading(\"JSON:\")\n                   |> Jason.decode!()\n\n                 payload[\"id\"] == 101 and\n                   get_in(payload, [\"result\", \"success\"]) == false and\n                   String.contains?(\n                     get_in(payload, [\"result\", \"output\"]),\n                     \"Unsupported dynamic tool\"\n                   )\n               else\n                 false\n               end\n             end)\n    after\n      File.rm_rf(test_root)\n    end\n  end\n\n  test \"app server executes supported dynamic tool calls and returns the tool result\" do\n    test_root =\n      Path.join(\n        System.tmp_dir!(),\n        \"symphony-elixir-app-server-supported-tool-call-#{System.unique_integer([:positive])}\"\n      )\n\n    try do\n      workspace_root = Path.join(test_root, \"workspaces\")\n      workspace = Path.join(workspace_root, \"MT-90A\")\n      codex_binary = Path.join(test_root, \"fake-codex\")\n      trace_file = Path.join(test_root, \"codex-supported-tool-call.trace\")\n      previous_trace = System.get_env(\"SYMP_TEST_CODEx_TRACE\")\n\n      on_exit(fn ->\n        if is_binary(previous_trace) do\n          System.put_env(\"SYMP_TEST_CODEx_TRACE\", previous_trace)\n        else\n          System.delete_env(\"SYMP_TEST_CODEx_TRACE\")\n        end\n      end)\n\n      System.put_env(\"SYMP_TEST_CODEx_TRACE\", trace_file)\n      File.mkdir_p!(workspace)\n\n      File.write!(codex_binary, \"\"\"\n      #!/bin/sh\n      trace_file=\"${SYMP_TEST_CODEx_TRACE:-/tmp/codex-supported-tool-call.trace}\"\n      count=0\n      while IFS= read -r line; do\n        count=$((count + 1))\n        printf 'JSON:%s\\\\n' \\\"$line\\\" >> \\\"$trace_file\\\"\n\n        case \\\"$count\\\" in\n          1)\n            printf '%s\\\\n' '{\\\"id\\\":1,\\\"result\\\":{}}'\n            ;;\n          2)\n            ;;\n          3)\n            printf '%s\\\\n' '{\\\"id\\\":2,\\\"result\\\":{\\\"thread\\\":{\\\"id\\\":\\\"thread-90a\\\"}}}'\n            ;;\n          4)\n            printf '%s\\\\n' '{\\\"id\\\":3,\\\"result\\\":{\\\"turn\\\":{\\\"id\\\":\\\"turn-90a\\\"}}}'\n            printf '%s\\\\n' '{\\\"id\\\":102,\\\"method\\\":\\\"item/tool/call\\\",\\\"params\\\":{\\\"name\\\":\\\"linear_graphql\\\",\\\"callId\\\":\\\"call-90a\\\",\\\"threadId\\\":\\\"thread-90a\\\",\\\"turnId\\\":\\\"turn-90a\\\",\\\"arguments\\\":{\\\"query\\\":\\\"query Viewer { viewer { id } }\\\",\\\"variables\\\":{\\\"includeTeams\\\":false}}}}'\n            ;;\n          5)\n            printf '%s\\\\n' '{\\\"method\\\":\\\"turn/completed\\\"}'\n            exit 0\n            ;;\n          *)\n            exit 0\n            ;;\n        esac\n      done\n      \"\"\")\n\n      File.chmod!(codex_binary, 0o755)\n\n      write_workflow_file!(Workflow.workflow_file_path(),\n        workspace_root: workspace_root,\n        codex_command: \"#{codex_binary} app-server\"\n      )\n\n      issue = %Issue{\n        id: \"issue-supported-tool-call\",\n        identifier: \"MT-90A\",\n        title: \"Supported tool call\",\n        description: \"Ensure supported tool calls return tool output\",\n        state: \"In Progress\",\n        url: \"https://example.org/issues/MT-90A\",\n        labels: [\"backend\"]\n      }\n\n      test_pid = self()\n\n      tool_executor = fn tool, arguments ->\n        send(test_pid, {:tool_called, tool, arguments})\n\n        %{\n          \"success\" => true,\n          \"contentItems\" => [\n            %{\n              \"type\" => \"inputText\",\n              \"text\" => ~s({\"data\":{\"viewer\":{\"id\":\"usr_123\"}}})\n            }\n          ]\n        }\n      end\n\n      assert {:ok, _result} =\n               AppServer.run(workspace, \"Handle supported tool calls\", issue, tool_executor: tool_executor)\n\n      assert_received {:tool_called, \"linear_graphql\",\n                       %{\n                         \"query\" => \"query Viewer { viewer { id } }\",\n                         \"variables\" => %{\"includeTeams\" => false}\n                       }}\n\n      trace = File.read!(trace_file)\n      lines = String.split(trace, \"\\n\", trim: true)\n\n      assert Enum.any?(lines, fn line ->\n               if String.starts_with?(line, \"JSON:\") do\n                 payload =\n                   line\n                   |> String.trim_leading(\"JSON:\")\n                   |> Jason.decode!()\n\n                 payload[\"id\"] == 102 and\n                   get_in(payload, [\"result\", \"success\"]) == true and\n                   get_in(payload, [\"result\", \"output\"]) ==\n                     ~s({\"data\":{\"viewer\":{\"id\":\"usr_123\"}}})\n               else\n                 false\n               end\n             end)\n    after\n      File.rm_rf(test_root)\n    end\n  end\n\n  test \"app server emits tool_call_failed for supported tool failures\" do\n    test_root =\n      Path.join(\n        System.tmp_dir!(),\n        \"symphony-elixir-app-server-tool-call-failed-#{System.unique_integer([:positive])}\"\n      )\n\n    try do\n      workspace_root = Path.join(test_root, \"workspaces\")\n      workspace = Path.join(workspace_root, \"MT-90B\")\n      codex_binary = Path.join(test_root, \"fake-codex\")\n      trace_file = Path.join(test_root, \"codex-tool-call-failed.trace\")\n      previous_trace = System.get_env(\"SYMP_TEST_CODEx_TRACE\")\n\n      on_exit(fn ->\n        if is_binary(previous_trace) do\n          System.put_env(\"SYMP_TEST_CODEx_TRACE\", previous_trace)\n        else\n          System.delete_env(\"SYMP_TEST_CODEx_TRACE\")\n        end\n      end)\n\n      System.put_env(\"SYMP_TEST_CODEx_TRACE\", trace_file)\n      File.mkdir_p!(workspace)\n\n      File.write!(codex_binary, \"\"\"\n      #!/bin/sh\n      trace_file=\"${SYMP_TEST_CODEx_TRACE:-/tmp/codex-tool-call-failed.trace}\"\n      count=0\n      while IFS= read -r line; do\n        count=$((count + 1))\n        printf 'JSON:%s\\\\n' \\\"$line\\\" >> \\\"$trace_file\\\"\n\n        case \\\"$count\\\" in\n          1)\n            printf '%s\\\\n' '{\\\"id\\\":1,\\\"result\\\":{}}'\n            ;;\n          2)\n            ;;\n          3)\n            printf '%s\\\\n' '{\\\"id\\\":2,\\\"result\\\":{\\\"thread\\\":{\\\"id\\\":\\\"thread-90b\\\"}}}'\n            ;;\n          4)\n            printf '%s\\\\n' '{\\\"id\\\":3,\\\"result\\\":{\\\"turn\\\":{\\\"id\\\":\\\"turn-90b\\\"}}}'\n            printf '%s\\\\n' '{\\\"id\\\":103,\\\"method\\\":\\\"item/tool/call\\\",\\\"params\\\":{\\\"tool\\\":\\\"linear_graphql\\\",\\\"callId\\\":\\\"call-90b\\\",\\\"threadId\\\":\\\"thread-90b\\\",\\\"turnId\\\":\\\"turn-90b\\\",\\\"arguments\\\":{\\\"query\\\":\\\"query Viewer { viewer { id } }\\\"}}}'\n            ;;\n          5)\n            printf '%s\\\\n' '{\\\"method\\\":\\\"turn/completed\\\"}'\n            exit 0\n            ;;\n          *)\n            exit 0\n            ;;\n        esac\n      done\n      \"\"\")\n\n      File.chmod!(codex_binary, 0o755)\n\n      write_workflow_file!(Workflow.workflow_file_path(),\n        workspace_root: workspace_root,\n        codex_command: \"#{codex_binary} app-server\"\n      )\n\n      issue = %Issue{\n        id: \"issue-tool-call-failed\",\n        identifier: \"MT-90B\",\n        title: \"Tool call failed\",\n        description: \"Ensure supported tool failures emit a distinct event\",\n        state: \"In Progress\",\n        url: \"https://example.org/issues/MT-90B\",\n        labels: [\"backend\"]\n      }\n\n      test_pid = self()\n\n      tool_executor = fn tool, arguments ->\n        send(test_pid, {:tool_called, tool, arguments})\n\n        %{\n          \"success\" => false,\n          \"contentItems\" => [\n            %{\n              \"type\" => \"inputText\",\n              \"text\" => ~s({\"error\":{\"message\":\"boom\"}})\n            }\n          ]\n        }\n      end\n\n      on_message = fn message -> send(test_pid, {:app_server_message, message}) end\n\n      assert {:ok, _result} =\n               AppServer.run(workspace, \"Handle failed tool calls\", issue,\n                 on_message: on_message,\n                 tool_executor: tool_executor\n               )\n\n      assert_received {:tool_called, \"linear_graphql\", %{\"query\" => \"query Viewer { viewer { id } }\"}}\n\n      assert_received {:app_server_message, %{event: :tool_call_failed, payload: %{\"params\" => %{\"tool\" => \"linear_graphql\"}}}}\n    after\n      File.rm_rf(test_root)\n    end\n  end\n\n  test \"app server buffers partial JSON lines until newline terminator\" do\n    test_root =\n      Path.join(\n        System.tmp_dir!(),\n        \"symphony-elixir-app-server-partial-line-#{System.unique_integer([:positive])}\"\n      )\n\n    try do\n      workspace_root = Path.join(test_root, \"workspaces\")\n      workspace = Path.join(workspace_root, \"MT-91\")\n      codex_binary = Path.join(test_root, \"fake-codex\")\n      File.mkdir_p!(workspace)\n\n      File.write!(codex_binary, \"\"\"\n      #!/bin/sh\n      count=0\n      while IFS= read -r line; do\n        count=$((count + 1))\n\n        case \"$count\" in\n          1)\n            padding=$(printf '%*s' 1100000 '' | tr ' ' a)\n            printf '{\"id\":1,\"result\":{},\"padding\":\"%s\"}\\\\n' \"$padding\"\n            ;;\n          2)\n            printf '%s\\\\n' '{\"id\":2,\"result\":{\"thread\":{\"id\":\"thread-91\"}}}'\n            ;;\n          3)\n            printf '%s\\\\n' '{\"id\":3,\"result\":{\"turn\":{\"id\":\"turn-91\"}}}'\n            ;;\n          4)\n            printf '%s\\\\n' '{\"method\":\"turn/completed\"}'\n            exit 0\n            ;;\n          *)\n            exit 0\n            ;;\n        esac\n      done\n      \"\"\")\n\n      File.chmod!(codex_binary, 0o755)\n\n      write_workflow_file!(Workflow.workflow_file_path(),\n        workspace_root: workspace_root,\n        codex_command: \"#{codex_binary} app-server\"\n      )\n\n      issue = %Issue{\n        id: \"issue-partial-line\",\n        identifier: \"MT-91\",\n        title: \"Partial line decode\",\n        description: \"Ensure JSON parsing waits for newline-delimited messages\",\n        state: \"In Progress\",\n        url: \"https://example.org/issues/MT-91\",\n        labels: [\"backend\"]\n      }\n\n      assert {:ok, _result} = AppServer.run(workspace, \"Validate newline-delimited buffering\", issue)\n    after\n      File.rm_rf(test_root)\n    end\n  end\n\n  test \"app server captures codex side output and logs it through Logger\" do\n    test_root =\n      Path.join(\n        System.tmp_dir!(),\n        \"symphony-elixir-app-server-stderr-#{System.unique_integer([:positive])}\"\n      )\n\n    try do\n      workspace_root = Path.join(test_root, \"workspaces\")\n      workspace = Path.join(workspace_root, \"MT-92\")\n      codex_binary = Path.join(test_root, \"fake-codex\")\n      File.mkdir_p!(workspace)\n\n      File.write!(codex_binary, \"\"\"\n      #!/bin/sh\n      count=0\n      while IFS= read -r line; do\n        count=$((count + 1))\n\n        case \"$count\" in\n          1)\n            printf '%s\\\\n' '{\"id\":1,\"result\":{}}'\n            ;;\n          2)\n            printf '%s\\\\n' '{\"id\":2,\"result\":{\"thread\":{\"id\":\"thread-92\"}}}'\n            ;;\n          3)\n            printf '%s\\\\n' '{\"id\":3,\"result\":{\"turn\":{\"id\":\"turn-92\"}}}'\n            ;;\n          4)\n            printf '%s\\\\n' 'warning: this is stderr noise' >&2\n            printf '%s\\\\n' '{\"method\":\"turn/completed\"}'\n            exit 0\n            ;;\n          *)\n            exit 0\n            ;;\n        esac\n      done\n      \"\"\")\n\n      File.chmod!(codex_binary, 0o755)\n\n      write_workflow_file!(Workflow.workflow_file_path(),\n        workspace_root: workspace_root,\n        codex_command: \"#{codex_binary} app-server\"\n      )\n\n      issue = %Issue{\n        id: \"issue-stderr\",\n        identifier: \"MT-92\",\n        title: \"Capture stderr\",\n        description: \"Ensure codex stderr is captured and logged\",\n        state: \"In Progress\",\n        url: \"https://example.org/issues/MT-92\",\n        labels: [\"backend\"]\n      }\n\n      test_pid = self()\n      on_message = fn message -> send(test_pid, {:app_server_message, message}) end\n\n      log =\n        capture_log(fn ->\n          assert {:ok, _result} =\n                   AppServer.run(workspace, \"Capture stderr log\", issue, on_message: on_message)\n        end)\n\n      assert_received {:app_server_message, %{event: :turn_completed}}\n      refute_received {:app_server_message, %{event: :malformed}}\n      assert log =~ \"Codex turn stream output: warning: this is stderr noise\"\n    after\n      File.rm_rf(test_root)\n    end\n  end\n\n  test \"app server emits malformed events for JSON-like protocol lines that fail to decode\" do\n    test_root =\n      Path.join(\n        System.tmp_dir!(),\n        \"symphony-elixir-app-server-malformed-protocol-#{System.unique_integer([:positive])}\"\n      )\n\n    try do\n      workspace_root = Path.join(test_root, \"workspaces\")\n      workspace = Path.join(workspace_root, \"MT-93\")\n      codex_binary = Path.join(test_root, \"fake-codex\")\n      File.mkdir_p!(workspace)\n\n      File.write!(codex_binary, \"\"\"\n      #!/bin/sh\n      count=0\n      while IFS= read -r line; do\n        count=$((count + 1))\n\n        case \"$count\" in\n          1)\n            printf '%s\\\\n' '{\"id\":1,\"result\":{}}'\n            ;;\n          2)\n            printf '%s\\\\n' '{\"id\":2,\"result\":{\"thread\":{\"id\":\"thread-93\"}}}'\n            ;;\n          3)\n            printf '%s\\\\n' '{\"id\":3,\"result\":{\"turn\":{\"id\":\"turn-93\"}}}'\n            ;;\n          4)\n            printf '%s\\\\n' '{\"method\":\"turn/completed\"'\n            printf '%s\\\\n' '{\"method\":\"turn/completed\"}'\n            exit 0\n            ;;\n          *)\n            exit 0\n            ;;\n        esac\n      done\n      \"\"\")\n\n      File.chmod!(codex_binary, 0o755)\n\n      write_workflow_file!(Workflow.workflow_file_path(),\n        workspace_root: workspace_root,\n        codex_command: \"#{codex_binary} app-server\"\n      )\n\n      issue = %Issue{\n        id: \"issue-malformed-protocol\",\n        identifier: \"MT-93\",\n        title: \"Malformed protocol frame\",\n        description: \"Ensure malformed JSON-like frames are surfaced to the orchestrator\",\n        state: \"In Progress\",\n        url: \"https://example.org/issues/MT-93\",\n        labels: [\"backend\"]\n      }\n\n      test_pid = self()\n      on_message = fn message -> send(test_pid, {:app_server_message, message}) end\n\n      assert {:ok, _result} =\n               AppServer.run(workspace, \"Capture malformed protocol line\", issue, on_message: on_message)\n\n      assert_received {:app_server_message, %{event: :malformed, payload: \"{\\\"method\\\":\\\"turn/completed\\\"\"}}\n      assert_received {:app_server_message, %{event: :turn_completed}}\n    after\n      File.rm_rf(test_root)\n    end\n  end\n\n  test \"app server launches over ssh for remote workers\" do\n    test_root =\n      Path.join(\n        System.tmp_dir!(),\n        \"symphony-elixir-app-server-remote-ssh-#{System.unique_integer([:positive])}\"\n      )\n\n    previous_path = System.get_env(\"PATH\")\n    previous_trace = System.get_env(\"SYMP_TEST_SSH_TRACE\")\n\n    on_exit(fn ->\n      restore_env(\"PATH\", previous_path)\n      restore_env(\"SYMP_TEST_SSH_TRACE\", previous_trace)\n    end)\n\n    try do\n      trace_file = Path.join(test_root, \"ssh.trace\")\n      fake_ssh = Path.join(test_root, \"ssh\")\n      remote_workspace = \"/remote/workspaces/MT-REMOTE\"\n\n      File.mkdir_p!(test_root)\n      System.put_env(\"SYMP_TEST_SSH_TRACE\", trace_file)\n      System.put_env(\"PATH\", test_root <> \":\" <> (previous_path || \"\"))\n\n      File.write!(fake_ssh, \"\"\"\n      #!/bin/sh\n      trace_file=\"${SYMP_TEST_SSH_TRACE:-/tmp/symphony-fake-ssh.trace}\"\n      count=0\n      printf 'ARGV:%s\\\\n' \"$*\" >> \"$trace_file\"\n\n      while IFS= read -r line; do\n        count=$((count + 1))\n        printf 'JSON:%s\\\\n' \"$line\" >> \"$trace_file\"\n\n        case \"$count\" in\n          1)\n            printf '%s\\\\n' '{\"id\":1,\"result\":{}}'\n            ;;\n          2)\n            printf '%s\\\\n' '{\"id\":2,\"result\":{\"thread\":{\"id\":\"thread-remote\"}}}'\n            ;;\n          3)\n            printf '%s\\\\n' '{\"id\":3,\"result\":{\"turn\":{\"id\":\"turn-remote\"}}}'\n            ;;\n          4)\n            printf '%s\\\\n' '{\"method\":\"turn/completed\"}'\n            exit 0\n            ;;\n          *)\n            exit 0\n            ;;\n        esac\n      done\n      \"\"\")\n\n      File.chmod!(fake_ssh, 0o755)\n\n      write_workflow_file!(Workflow.workflow_file_path(),\n        workspace_root: \"/remote/workspaces\",\n        codex_command: \"fake-remote-codex app-server\"\n      )\n\n      issue = %Issue{\n        id: \"issue-remote\",\n        identifier: \"MT-REMOTE\",\n        title: \"Run remote app server\",\n        description: \"Validate ssh-backed codex startup\",\n        state: \"In Progress\",\n        url: \"https://example.org/issues/MT-REMOTE\",\n        labels: [\"backend\"]\n      }\n\n      assert {:ok, _result} =\n               AppServer.run(\n                 remote_workspace,\n                 \"Run remote worker\",\n                 issue,\n                 worker_host: \"worker-01:2200\"\n               )\n\n      trace = File.read!(trace_file)\n      lines = String.split(trace, \"\\n\", trim: true)\n\n      assert argv_line = Enum.find(lines, &String.starts_with?(&1, \"ARGV:\"))\n      assert argv_line =~ \"-T -p 2200 worker-01 bash -lc\"\n      assert argv_line =~ \"cd \"\n      assert argv_line =~ remote_workspace\n      assert argv_line =~ \"exec \"\n      assert argv_line =~ \"fake-remote-codex app-server\"\n\n      expected_turn_policy = %{\n        \"type\" => \"workspaceWrite\",\n        \"writableRoots\" => [remote_workspace],\n        \"readOnlyAccess\" => %{\"type\" => \"fullAccess\"},\n        \"networkAccess\" => false,\n        \"excludeTmpdirEnvVar\" => false,\n        \"excludeSlashTmp\" => false\n      }\n\n      assert Enum.any?(lines, fn line ->\n               if String.starts_with?(line, \"JSON:\") do\n                 line\n                 |> String.trim_leading(\"JSON:\")\n                 |> Jason.decode!()\n                 |> then(fn payload ->\n                   payload[\"method\"] == \"thread/start\" &&\n                     get_in(payload, [\"params\", \"cwd\"]) == remote_workspace\n                 end)\n               else\n                 false\n               end\n             end)\n\n      assert Enum.any?(lines, fn line ->\n               if String.starts_with?(line, \"JSON:\") do\n                 line\n                 |> String.trim_leading(\"JSON:\")\n                 |> Jason.decode!()\n                 |> then(fn payload ->\n                   payload[\"method\"] == \"turn/start\" &&\n                     get_in(payload, [\"params\", \"cwd\"]) == remote_workspace &&\n                     get_in(payload, [\"params\", \"sandboxPolicy\"]) == expected_turn_policy\n                 end)\n               else\n                 false\n               end\n             end)\n    after\n      File.rm_rf(test_root)\n    end\n  end\nend\n"
  },
  {
    "path": "elixir/test/symphony_elixir/cli_test.exs",
    "content": "defmodule SymphonyElixir.CLITest do\n  use ExUnit.Case, async: true\n\n  alias SymphonyElixir.CLI\n\n  @ack_flag \"--i-understand-that-this-will-be-running-without-the-usual-guardrails\"\n\n  test \"returns the guardrails acknowledgement banner when the flag is missing\" do\n    parent = self()\n\n    deps = %{\n      file_regular?: fn _path ->\n        send(parent, :file_checked)\n        true\n      end,\n      set_workflow_file_path: fn _path ->\n        send(parent, :workflow_set)\n        :ok\n      end,\n      set_logs_root: fn _path ->\n        send(parent, :logs_root_set)\n        :ok\n      end,\n      set_server_port_override: fn _port ->\n        send(parent, :port_set)\n        :ok\n      end,\n      ensure_all_started: fn ->\n        send(parent, :started)\n        {:ok, [:symphony_elixir]}\n      end\n    }\n\n    assert {:error, banner} = CLI.evaluate([\"WORKFLOW.md\"], deps)\n    assert banner =~ \"This Symphony implementation is a low key engineering preview.\"\n    assert banner =~ \"Codex will run without any guardrails.\"\n    assert banner =~ \"SymphonyElixir is not a supported product and is presented as-is.\"\n    assert banner =~ @ack_flag\n    refute_received :file_checked\n    refute_received :workflow_set\n    refute_received :logs_root_set\n    refute_received :port_set\n    refute_received :started\n  end\n\n  test \"defaults to WORKFLOW.md when workflow path is missing\" do\n    deps = %{\n      file_regular?: fn path -> Path.basename(path) == \"WORKFLOW.md\" end,\n      set_workflow_file_path: fn _path -> :ok end,\n      set_logs_root: fn _path -> :ok end,\n      set_server_port_override: fn _port -> :ok end,\n      ensure_all_started: fn -> {:ok, [:symphony_elixir]} end\n    }\n\n    assert :ok = CLI.evaluate([@ack_flag], deps)\n  end\n\n  test \"uses an explicit workflow path override when provided\" do\n    parent = self()\n    workflow_path = \"tmp/custom/WORKFLOW.md\"\n    expanded_path = Path.expand(workflow_path)\n\n    deps = %{\n      file_regular?: fn path ->\n        send(parent, {:workflow_checked, path})\n        path == expanded_path\n      end,\n      set_workflow_file_path: fn path ->\n        send(parent, {:workflow_set, path})\n        :ok\n      end,\n      set_logs_root: fn _path -> :ok end,\n      set_server_port_override: fn _port -> :ok end,\n      ensure_all_started: fn -> {:ok, [:symphony_elixir]} end\n    }\n\n    assert :ok = CLI.evaluate([@ack_flag, workflow_path], deps)\n    assert_received {:workflow_checked, ^expanded_path}\n    assert_received {:workflow_set, ^expanded_path}\n  end\n\n  test \"accepts --logs-root and passes an expanded root to runtime deps\" do\n    parent = self()\n\n    deps = %{\n      file_regular?: fn _path -> true end,\n      set_workflow_file_path: fn _path -> :ok end,\n      set_logs_root: fn path ->\n        send(parent, {:logs_root, path})\n        :ok\n      end,\n      set_server_port_override: fn _port -> :ok end,\n      ensure_all_started: fn -> {:ok, [:symphony_elixir]} end\n    }\n\n    assert :ok = CLI.evaluate([@ack_flag, \"--logs-root\", \"tmp/custom-logs\", \"WORKFLOW.md\"], deps)\n    assert_received {:logs_root, expanded_path}\n    assert expanded_path == Path.expand(\"tmp/custom-logs\")\n  end\n\n  test \"returns not found when workflow file does not exist\" do\n    deps = %{\n      file_regular?: fn _path -> false end,\n      set_workflow_file_path: fn _path -> :ok end,\n      set_logs_root: fn _path -> :ok end,\n      set_server_port_override: fn _port -> :ok end,\n      ensure_all_started: fn -> {:ok, [:symphony_elixir]} end\n    }\n\n    assert {:error, message} = CLI.evaluate([@ack_flag, \"WORKFLOW.md\"], deps)\n    assert message =~ \"Workflow file not found:\"\n  end\n\n  test \"returns startup error when app cannot start\" do\n    deps = %{\n      file_regular?: fn _path -> true end,\n      set_workflow_file_path: fn _path -> :ok end,\n      set_logs_root: fn _path -> :ok end,\n      set_server_port_override: fn _port -> :ok end,\n      ensure_all_started: fn -> {:error, :boom} end\n    }\n\n    assert {:error, message} = CLI.evaluate([@ack_flag, \"WORKFLOW.md\"], deps)\n    assert message =~ \"Failed to start Symphony with workflow\"\n    assert message =~ \":boom\"\n  end\n\n  test \"returns ok when workflow exists and app starts\" do\n    deps = %{\n      file_regular?: fn _path -> true end,\n      set_workflow_file_path: fn _path -> :ok end,\n      set_logs_root: fn _path -> :ok end,\n      set_server_port_override: fn _port -> :ok end,\n      ensure_all_started: fn -> {:ok, [:symphony_elixir]} end\n    }\n\n    assert :ok = CLI.evaluate([@ack_flag, \"WORKFLOW.md\"], deps)\n  end\nend\n"
  },
  {
    "path": "elixir/test/symphony_elixir/core_test.exs",
    "content": "defmodule SymphonyElixir.CoreTest do\n  use SymphonyElixir.TestSupport\n\n  test \"config defaults and validation checks\" do\n    write_workflow_file!(Workflow.workflow_file_path(),\n      tracker_api_token: nil,\n      tracker_project_slug: nil,\n      poll_interval_ms: nil,\n      tracker_active_states: nil,\n      tracker_terminal_states: nil,\n      codex_command: nil\n    )\n\n    config = Config.settings!()\n    assert config.polling.interval_ms == 30_000\n    assert config.tracker.active_states == [\"Todo\", \"In Progress\"]\n    assert config.tracker.terminal_states == [\"Closed\", \"Cancelled\", \"Canceled\", \"Duplicate\", \"Done\"]\n    assert config.tracker.assignee == nil\n    assert config.agent.max_turns == 20\n\n    write_workflow_file!(Workflow.workflow_file_path(), poll_interval_ms: \"invalid\")\n\n    assert_raise ArgumentError, ~r/interval_ms/, fn ->\n      Config.settings!().polling.interval_ms\n    end\n\n    assert {:error, {:invalid_workflow_config, message}} = Config.validate!()\n    assert message =~ \"polling.interval_ms\"\n\n    write_workflow_file!(Workflow.workflow_file_path(), poll_interval_ms: 45_000)\n    assert Config.settings!().polling.interval_ms == 45_000\n\n    write_workflow_file!(Workflow.workflow_file_path(), max_turns: 0)\n    assert {:error, {:invalid_workflow_config, message}} = Config.validate!()\n    assert message =~ \"agent.max_turns\"\n\n    write_workflow_file!(Workflow.workflow_file_path(), max_turns: 5)\n    assert Config.settings!().agent.max_turns == 5\n\n    write_workflow_file!(Workflow.workflow_file_path(), tracker_active_states: \"Todo,  Review,\")\n    assert {:error, {:invalid_workflow_config, message}} = Config.validate!()\n    assert message =~ \"tracker.active_states\"\n\n    write_workflow_file!(Workflow.workflow_file_path(),\n      tracker_api_token: \"token\",\n      tracker_project_slug: nil\n    )\n\n    assert {:error, :missing_linear_project_slug} = Config.validate!()\n\n    write_workflow_file!(Workflow.workflow_file_path(),\n      tracker_project_slug: \"project\",\n      codex_command: \"\"\n    )\n\n    assert {:error, {:invalid_workflow_config, message}} = Config.validate!()\n    assert message =~ \"codex.command\"\n    assert message =~ \"can't be blank\"\n\n    write_workflow_file!(Workflow.workflow_file_path(), codex_command: \"   \")\n    assert :ok = Config.validate!()\n    assert Config.settings!().codex.command == \"   \"\n\n    write_workflow_file!(Workflow.workflow_file_path(), codex_command: \"/bin/sh app-server\")\n    assert :ok = Config.validate!()\n\n    write_workflow_file!(Workflow.workflow_file_path(), codex_approval_policy: \"definitely-not-valid\")\n    assert :ok = Config.validate!()\n\n    write_workflow_file!(Workflow.workflow_file_path(), codex_thread_sandbox: \"unsafe-ish\")\n    assert :ok = Config.validate!()\n\n    write_workflow_file!(Workflow.workflow_file_path(),\n      codex_turn_sandbox_policy: %{type: \"workspaceWrite\", writableRoots: [\"relative/path\"]}\n    )\n\n    assert :ok = Config.validate!()\n\n    write_workflow_file!(Workflow.workflow_file_path(), codex_approval_policy: 123)\n    assert {:error, {:invalid_workflow_config, message}} = Config.validate!()\n    assert message =~ \"codex.approval_policy\"\n\n    write_workflow_file!(Workflow.workflow_file_path(), codex_thread_sandbox: 123)\n    assert {:error, {:invalid_workflow_config, message}} = Config.validate!()\n    assert message =~ \"codex.thread_sandbox\"\n\n    write_workflow_file!(Workflow.workflow_file_path(), tracker_kind: \"123\")\n    assert {:error, {:unsupported_tracker_kind, \"123\"}} = Config.validate!()\n  end\n\n  test \"current WORKFLOW.md file is valid and complete\" do\n    original_workflow_path = Workflow.workflow_file_path()\n    on_exit(fn -> Workflow.set_workflow_file_path(original_workflow_path) end)\n    Workflow.clear_workflow_file_path()\n\n    assert {:ok, %{config: config, prompt: prompt}} = Workflow.load()\n    assert is_map(config)\n\n    tracker = Map.get(config, \"tracker\", %{})\n    assert is_map(tracker)\n    assert Map.get(tracker, \"kind\") == \"linear\"\n    assert is_binary(Map.get(tracker, \"project_slug\"))\n    assert is_list(Map.get(tracker, \"active_states\"))\n    assert is_list(Map.get(tracker, \"terminal_states\"))\n\n    hooks = Map.get(config, \"hooks\", %{})\n    assert is_map(hooks)\n    assert Map.get(hooks, \"after_create\") =~ \"git clone --depth 1 https://github.com/openai/symphony .\"\n    assert Map.get(hooks, \"after_create\") =~ \"cd elixir && mise trust\"\n    assert Map.get(hooks, \"after_create\") =~ \"mise exec -- mix deps.get\"\n    assert Map.get(hooks, \"before_remove\") =~ \"cd elixir && mise exec -- mix workspace.before_remove\"\n\n    assert String.trim(prompt) != \"\"\n    assert is_binary(Config.workflow_prompt())\n    assert Config.workflow_prompt() == prompt\n  end\n\n  test \"linear api token resolves from LINEAR_API_KEY env var\" do\n    previous_linear_api_key = System.get_env(\"LINEAR_API_KEY\")\n    env_api_key = \"test-linear-api-key\"\n\n    on_exit(fn -> restore_env(\"LINEAR_API_KEY\", previous_linear_api_key) end)\n    System.put_env(\"LINEAR_API_KEY\", env_api_key)\n\n    write_workflow_file!(Workflow.workflow_file_path(),\n      tracker_api_token: nil,\n      tracker_project_slug: \"project\",\n      codex_command: \"/bin/sh app-server\"\n    )\n\n    assert Config.settings!().tracker.api_key == env_api_key\n    assert Config.settings!().tracker.project_slug == \"project\"\n    assert :ok = Config.validate!()\n  end\n\n  test \"linear assignee resolves from LINEAR_ASSIGNEE env var\" do\n    previous_linear_assignee = System.get_env(\"LINEAR_ASSIGNEE\")\n    env_assignee = \"dev@example.com\"\n\n    on_exit(fn -> restore_env(\"LINEAR_ASSIGNEE\", previous_linear_assignee) end)\n    System.put_env(\"LINEAR_ASSIGNEE\", env_assignee)\n\n    write_workflow_file!(Workflow.workflow_file_path(),\n      tracker_assignee: nil,\n      tracker_project_slug: \"project\",\n      codex_command: \"/bin/sh app-server\"\n    )\n\n    assert Config.settings!().tracker.assignee == env_assignee\n  end\n\n  test \"workflow file path defaults to WORKFLOW.md in the current working directory when app env is unset\" do\n    original_workflow_path = Workflow.workflow_file_path()\n\n    on_exit(fn ->\n      Workflow.set_workflow_file_path(original_workflow_path)\n    end)\n\n    Workflow.clear_workflow_file_path()\n\n    assert Workflow.workflow_file_path() == Path.join(File.cwd!(), \"WORKFLOW.md\")\n  end\n\n  test \"workflow file path resolves from app env when set\" do\n    app_workflow_path = \"/tmp/app/WORKFLOW.md\"\n\n    on_exit(fn ->\n      Workflow.clear_workflow_file_path()\n    end)\n\n    Workflow.set_workflow_file_path(app_workflow_path)\n\n    assert Workflow.workflow_file_path() == app_workflow_path\n  end\n\n  test \"workflow load accepts prompt-only files without front matter\" do\n    workflow_path = Path.join(Path.dirname(Workflow.workflow_file_path()), \"PROMPT_ONLY_WORKFLOW.md\")\n    File.write!(workflow_path, \"Prompt only\\n\")\n\n    assert {:ok, %{config: %{}, prompt: \"Prompt only\", prompt_template: \"Prompt only\"}} =\n             Workflow.load(workflow_path)\n  end\n\n  test \"workflow load accepts unterminated front matter with an empty prompt\" do\n    workflow_path = Path.join(Path.dirname(Workflow.workflow_file_path()), \"UNTERMINATED_WORKFLOW.md\")\n    File.write!(workflow_path, \"---\\ntracker:\\n  kind: linear\\n\")\n\n    assert {:ok, %{config: %{\"tracker\" => %{\"kind\" => \"linear\"}}, prompt: \"\", prompt_template: \"\"}} =\n             Workflow.load(workflow_path)\n  end\n\n  test \"workflow load rejects non-map front matter\" do\n    workflow_path = Path.join(Path.dirname(Workflow.workflow_file_path()), \"INVALID_FRONT_MATTER_WORKFLOW.md\")\n    File.write!(workflow_path, \"---\\n- not-a-map\\n---\\nPrompt body\\n\")\n\n    assert {:error, :workflow_front_matter_not_a_map} = Workflow.load(workflow_path)\n  end\n\n  test \"SymphonyElixir.start_link delegates to the orchestrator\" do\n    write_workflow_file!(Workflow.workflow_file_path(), tracker_kind: \"memory\")\n    Application.put_env(:symphony_elixir, :memory_tracker_issues, [])\n    orchestrator_pid = Process.whereis(SymphonyElixir.Orchestrator)\n\n    on_exit(fn ->\n      if is_nil(Process.whereis(SymphonyElixir.Orchestrator)) do\n        case Supervisor.restart_child(SymphonyElixir.Supervisor, SymphonyElixir.Orchestrator) do\n          {:ok, _pid} -> :ok\n          {:error, {:already_started, _pid}} -> :ok\n        end\n      end\n    end)\n\n    if is_pid(orchestrator_pid) do\n      assert :ok = Supervisor.terminate_child(SymphonyElixir.Supervisor, SymphonyElixir.Orchestrator)\n    end\n\n    assert {:ok, pid} = SymphonyElixir.start_link()\n    assert Process.whereis(SymphonyElixir.Orchestrator) == pid\n\n    GenServer.stop(pid)\n  end\n\n  test \"linear issue state reconciliation fetch with no running issues is a no-op\" do\n    assert {:ok, []} = Client.fetch_issue_states_by_ids([])\n  end\n\n  test \"non-active issue state stops running agent without cleaning workspace\" do\n    test_root =\n      Path.join(\n        System.tmp_dir!(),\n        \"symphony-elixir-nonactive-reconcile-#{System.unique_integer([:positive])}\"\n      )\n\n    issue_id = \"issue-1\"\n    issue_identifier = \"MT-555\"\n    workspace = Path.join(test_root, issue_identifier)\n\n    try do\n      write_workflow_file!(Workflow.workflow_file_path(),\n        workspace_root: test_root,\n        tracker_active_states: [\"Todo\", \"In Progress\", \"In Review\"],\n        tracker_terminal_states: [\"Closed\", \"Cancelled\", \"Canceled\", \"Duplicate\"]\n      )\n\n      File.mkdir_p!(test_root)\n      File.mkdir_p!(workspace)\n\n      agent_pid =\n        spawn(fn ->\n          receive do\n            :stop -> :ok\n          end\n        end)\n\n      state = %Orchestrator.State{\n        running: %{\n          issue_id => %{\n            pid: agent_pid,\n            ref: nil,\n            identifier: issue_identifier,\n            issue: %Issue{id: issue_id, state: \"Todo\", identifier: issue_identifier},\n            started_at: DateTime.utc_now()\n          }\n        },\n        claimed: MapSet.new([issue_id]),\n        codex_totals: %{input_tokens: 0, output_tokens: 0, total_tokens: 0, seconds_running: 0},\n        retry_attempts: %{}\n      }\n\n      issue = %Issue{\n        id: issue_id,\n        identifier: issue_identifier,\n        state: \"Backlog\",\n        title: \"Queued\",\n        description: \"Not started\",\n        labels: []\n      }\n\n      updated_state = Orchestrator.reconcile_issue_states_for_test([issue], state)\n\n      refute Map.has_key?(updated_state.running, issue_id)\n      refute MapSet.member?(updated_state.claimed, issue_id)\n      refute Process.alive?(agent_pid)\n      assert File.exists?(workspace)\n    after\n      File.rm_rf(test_root)\n    end\n  end\n\n  test \"terminal issue state stops running agent and cleans workspace\" do\n    test_root =\n      Path.join(\n        System.tmp_dir!(),\n        \"symphony-elixir-terminal-reconcile-#{System.unique_integer([:positive])}\"\n      )\n\n    issue_id = \"issue-2\"\n    issue_identifier = \"MT-556\"\n    workspace = Path.join(test_root, issue_identifier)\n\n    try do\n      write_workflow_file!(Workflow.workflow_file_path(),\n        workspace_root: test_root,\n        tracker_active_states: [\"Todo\", \"In Progress\", \"In Review\"],\n        tracker_terminal_states: [\"Closed\", \"Cancelled\", \"Canceled\", \"Duplicate\"]\n      )\n\n      File.mkdir_p!(test_root)\n      File.mkdir_p!(workspace)\n\n      agent_pid =\n        spawn(fn ->\n          receive do\n            :stop -> :ok\n          end\n        end)\n\n      state = %Orchestrator.State{\n        running: %{\n          issue_id => %{\n            pid: agent_pid,\n            ref: nil,\n            identifier: issue_identifier,\n            issue: %Issue{id: issue_id, state: \"In Progress\", identifier: issue_identifier},\n            started_at: DateTime.utc_now()\n          }\n        },\n        claimed: MapSet.new([issue_id]),\n        codex_totals: %{input_tokens: 0, output_tokens: 0, total_tokens: 0, seconds_running: 0},\n        retry_attempts: %{}\n      }\n\n      issue = %Issue{\n        id: issue_id,\n        identifier: issue_identifier,\n        state: \"Closed\",\n        title: \"Done\",\n        description: \"Completed\",\n        labels: []\n      }\n\n      updated_state = Orchestrator.reconcile_issue_states_for_test([issue], state)\n\n      refute Map.has_key?(updated_state.running, issue_id)\n      refute MapSet.member?(updated_state.claimed, issue_id)\n      refute Process.alive?(agent_pid)\n      refute File.exists?(workspace)\n    after\n      File.rm_rf(test_root)\n    end\n  end\n\n  test \"missing running issues stop active agents without cleaning the workspace\" do\n    test_root =\n      Path.join(\n        System.tmp_dir!(),\n        \"symphony-elixir-missing-running-reconcile-#{System.unique_integer([:positive])}\"\n      )\n\n    previous_memory_issues = Application.get_env(:symphony_elixir, :memory_tracker_issues)\n    issue_id = \"issue-missing\"\n    issue_identifier = \"MT-557\"\n\n    try do\n      write_workflow_file!(Workflow.workflow_file_path(),\n        tracker_kind: \"memory\",\n        workspace_root: test_root,\n        tracker_active_states: [\"Todo\", \"In Progress\", \"In Review\"],\n        tracker_terminal_states: [\"Closed\", \"Cancelled\", \"Canceled\", \"Duplicate\"],\n        poll_interval_ms: 30_000\n      )\n\n      Application.put_env(:symphony_elixir, :memory_tracker_issues, [])\n\n      orchestrator_name = Module.concat(__MODULE__, :MissingRunningIssueOrchestrator)\n      {:ok, pid} = Orchestrator.start_link(name: orchestrator_name)\n\n      on_exit(fn ->\n        restore_app_env(:memory_tracker_issues, previous_memory_issues)\n\n        if Process.alive?(pid) do\n          Process.exit(pid, :normal)\n        end\n      end)\n\n      Process.sleep(50)\n\n      assert {:ok, workspace} =\n               SymphonyElixir.PathSafety.canonicalize(Path.join(test_root, issue_identifier))\n\n      File.mkdir_p!(workspace)\n\n      agent_pid =\n        spawn(fn ->\n          receive do\n            :stop -> :ok\n          end\n        end)\n\n      initial_state = :sys.get_state(pid)\n\n      running_entry = %{\n        pid: agent_pid,\n        ref: nil,\n        identifier: issue_identifier,\n        issue: %Issue{id: issue_id, state: \"In Progress\", identifier: issue_identifier},\n        started_at: DateTime.utc_now()\n      }\n\n      :sys.replace_state(pid, fn _ ->\n        initial_state\n        |> Map.put(:running, %{issue_id => running_entry})\n        |> Map.put(:claimed, MapSet.new([issue_id]))\n        |> Map.put(:retry_attempts, %{})\n      end)\n\n      send(pid, :tick)\n      Process.sleep(100)\n      state = :sys.get_state(pid)\n\n      refute Map.has_key?(state.running, issue_id)\n      refute MapSet.member?(state.claimed, issue_id)\n      refute Process.alive?(agent_pid)\n      assert File.exists?(workspace)\n    after\n      restore_app_env(:memory_tracker_issues, previous_memory_issues)\n      File.rm_rf(test_root)\n    end\n  end\n\n  test \"reconcile updates running issue state for active issues\" do\n    issue_id = \"issue-3\"\n\n    state = %Orchestrator.State{\n      running: %{\n        issue_id => %{\n          pid: self(),\n          ref: nil,\n          identifier: \"MT-557\",\n          issue: %Issue{\n            id: issue_id,\n            identifier: \"MT-557\",\n            state: \"Todo\"\n          },\n          started_at: DateTime.utc_now()\n        }\n      },\n      claimed: MapSet.new([issue_id]),\n      codex_totals: %{input_tokens: 0, output_tokens: 0, total_tokens: 0, seconds_running: 0},\n      retry_attempts: %{}\n    }\n\n    issue = %Issue{\n      id: issue_id,\n      identifier: \"MT-557\",\n      state: \"In Progress\",\n      title: \"Active state refresh\",\n      description: \"State should be refreshed\",\n      labels: []\n    }\n\n    updated_state = Orchestrator.reconcile_issue_states_for_test([issue], state)\n    updated_entry = updated_state.running[issue_id]\n\n    assert Map.has_key?(updated_state.running, issue_id)\n    assert MapSet.member?(updated_state.claimed, issue_id)\n    assert updated_entry.issue.state == \"In Progress\"\n  end\n\n  test \"reconcile stops running issue when it is reassigned away from this worker\" do\n    issue_id = \"issue-reassigned\"\n\n    agent_pid =\n      spawn(fn ->\n        receive do\n          :stop -> :ok\n        end\n      end)\n\n    state = %Orchestrator.State{\n      running: %{\n        issue_id => %{\n          pid: agent_pid,\n          ref: nil,\n          identifier: \"MT-561\",\n          issue: %Issue{\n            id: issue_id,\n            identifier: \"MT-561\",\n            state: \"In Progress\",\n            assigned_to_worker: true\n          },\n          started_at: DateTime.utc_now()\n        }\n      },\n      claimed: MapSet.new([issue_id]),\n      codex_totals: %{input_tokens: 0, output_tokens: 0, total_tokens: 0, seconds_running: 0},\n      retry_attempts: %{}\n    }\n\n    issue = %Issue{\n      id: issue_id,\n      identifier: \"MT-561\",\n      state: \"In Progress\",\n      title: \"Reassigned active issue\",\n      description: \"Worker should stop\",\n      labels: [],\n      assigned_to_worker: false\n    }\n\n    updated_state = Orchestrator.reconcile_issue_states_for_test([issue], state)\n\n    refute Map.has_key?(updated_state.running, issue_id)\n    refute MapSet.member?(updated_state.claimed, issue_id)\n    refute Process.alive?(agent_pid)\n  end\n\n  test \"normal worker exit schedules active-state continuation retry\" do\n    issue_id = \"issue-resume\"\n    ref = make_ref()\n    orchestrator_name = Module.concat(__MODULE__, :ContinuationOrchestrator)\n    {:ok, pid} = Orchestrator.start_link(name: orchestrator_name)\n\n    on_exit(fn ->\n      if Process.alive?(pid) do\n        Process.exit(pid, :normal)\n      end\n    end)\n\n    initial_state = :sys.get_state(pid)\n\n    running_entry = %{\n      pid: self(),\n      ref: ref,\n      identifier: \"MT-558\",\n      issue: %Issue{id: issue_id, identifier: \"MT-558\", state: \"In Progress\"},\n      started_at: DateTime.utc_now()\n    }\n\n    :sys.replace_state(pid, fn _ ->\n      initial_state\n      |> Map.put(:running, %{issue_id => running_entry})\n      |> Map.put(:claimed, MapSet.new([issue_id]))\n      |> Map.put(:retry_attempts, %{})\n    end)\n\n    send(pid, {:DOWN, ref, :process, self(), :normal})\n    Process.sleep(50)\n    state = :sys.get_state(pid)\n\n    refute Map.has_key?(state.running, issue_id)\n    assert MapSet.member?(state.completed, issue_id)\n    assert %{attempt: 1, due_at_ms: due_at_ms} = state.retry_attempts[issue_id]\n    assert is_integer(due_at_ms)\n    assert_due_in_range(due_at_ms, 500, 1_100)\n  end\n\n  test \"abnormal worker exit increments retry attempt progressively\" do\n    issue_id = \"issue-crash\"\n    ref = make_ref()\n    orchestrator_name = Module.concat(__MODULE__, :CrashRetryOrchestrator)\n    {:ok, pid} = Orchestrator.start_link(name: orchestrator_name)\n\n    on_exit(fn ->\n      if Process.alive?(pid) do\n        Process.exit(pid, :normal)\n      end\n    end)\n\n    initial_state = :sys.get_state(pid)\n\n    running_entry = %{\n      pid: self(),\n      ref: ref,\n      identifier: \"MT-559\",\n      retry_attempt: 2,\n      issue: %Issue{id: issue_id, identifier: \"MT-559\", state: \"In Progress\"},\n      started_at: DateTime.utc_now()\n    }\n\n    :sys.replace_state(pid, fn _ ->\n      initial_state\n      |> Map.put(:running, %{issue_id => running_entry})\n      |> Map.put(:claimed, MapSet.new([issue_id]))\n      |> Map.put(:retry_attempts, %{})\n    end)\n\n    send(pid, {:DOWN, ref, :process, self(), :boom})\n    Process.sleep(50)\n    state = :sys.get_state(pid)\n\n    assert %{attempt: 3, due_at_ms: due_at_ms, identifier: \"MT-559\", error: \"agent exited: :boom\"} =\n             state.retry_attempts[issue_id]\n\n    assert_due_in_range(due_at_ms, 39_500, 40_500)\n  end\n\n  test \"first abnormal worker exit waits before retrying\" do\n    issue_id = \"issue-crash-initial\"\n    ref = make_ref()\n    orchestrator_name = Module.concat(__MODULE__, :InitialCrashRetryOrchestrator)\n    {:ok, pid} = Orchestrator.start_link(name: orchestrator_name)\n\n    on_exit(fn ->\n      if Process.alive?(pid) do\n        Process.exit(pid, :normal)\n      end\n    end)\n\n    initial_state = :sys.get_state(pid)\n\n    running_entry = %{\n      pid: self(),\n      ref: ref,\n      identifier: \"MT-560\",\n      issue: %Issue{id: issue_id, identifier: \"MT-560\", state: \"In Progress\"},\n      started_at: DateTime.utc_now()\n    }\n\n    :sys.replace_state(pid, fn _ ->\n      initial_state\n      |> Map.put(:running, %{issue_id => running_entry})\n      |> Map.put(:claimed, MapSet.new([issue_id]))\n      |> Map.put(:retry_attempts, %{})\n    end)\n\n    send(pid, {:DOWN, ref, :process, self(), :boom})\n    Process.sleep(50)\n    state = :sys.get_state(pid)\n\n    assert %{attempt: 1, due_at_ms: due_at_ms, identifier: \"MT-560\", error: \"agent exited: :boom\"} =\n             state.retry_attempts[issue_id]\n\n    assert_due_in_range(due_at_ms, 9_000, 10_500)\n  end\n\n  test \"stale retry timer messages do not consume newer retry entries\" do\n    issue_id = \"issue-stale-retry\"\n    orchestrator_name = Module.concat(__MODULE__, :StaleRetryOrchestrator)\n    {:ok, pid} = Orchestrator.start_link(name: orchestrator_name)\n\n    on_exit(fn ->\n      if Process.alive?(pid) do\n        Process.exit(pid, :normal)\n      end\n    end)\n\n    initial_state = :sys.get_state(pid)\n    current_retry_token = make_ref()\n    stale_retry_token = make_ref()\n\n    :sys.replace_state(pid, fn _ ->\n      initial_state\n      |> Map.put(:retry_attempts, %{\n        issue_id => %{\n          attempt: 2,\n          timer_ref: nil,\n          retry_token: current_retry_token,\n          due_at_ms: System.monotonic_time(:millisecond) + 30_000,\n          identifier: \"MT-561\",\n          error: \"agent exited: :boom\"\n        }\n      })\n    end)\n\n    send(pid, {:retry_issue, issue_id, stale_retry_token})\n    Process.sleep(50)\n\n    assert %{\n             attempt: 2,\n             retry_token: ^current_retry_token,\n             identifier: \"MT-561\",\n             error: \"agent exited: :boom\"\n           } = :sys.get_state(pid).retry_attempts[issue_id]\n  end\n\n  test \"manual refresh coalesces repeated requests and ignores superseded ticks\" do\n    now_ms = System.monotonic_time(:millisecond)\n    stale_tick_token = make_ref()\n\n    state = %Orchestrator.State{\n      poll_interval_ms: 30_000,\n      max_concurrent_agents: 1,\n      next_poll_due_at_ms: now_ms + 30_000,\n      poll_check_in_progress: false,\n      tick_timer_ref: nil,\n      tick_token: stale_tick_token,\n      codex_totals: %{input_tokens: 0, output_tokens: 0, total_tokens: 0, seconds_running: 0},\n      codex_rate_limits: nil\n    }\n\n    assert {:reply, %{queued: true, coalesced: false}, refreshed_state} =\n             Orchestrator.handle_call(:request_refresh, {self(), make_ref()}, state)\n\n    assert is_reference(refreshed_state.tick_timer_ref)\n    assert is_reference(refreshed_state.tick_token)\n    refute refreshed_state.tick_token == stale_tick_token\n    assert refreshed_state.next_poll_due_at_ms <= System.monotonic_time(:millisecond)\n\n    assert {:reply, %{queued: true, coalesced: true}, coalesced_state} =\n             Orchestrator.handle_call(:request_refresh, {self(), make_ref()}, refreshed_state)\n\n    assert coalesced_state.tick_token == refreshed_state.tick_token\n    assert {:noreply, ^coalesced_state} = Orchestrator.handle_info({:tick, stale_tick_token}, coalesced_state)\n  end\n\n  test \"select_worker_host_for_test skips full ssh hosts under the shared per-host cap\" do\n    write_workflow_file!(Workflow.workflow_file_path(),\n      worker_ssh_hosts: [\"worker-a\", \"worker-b\"],\n      worker_max_concurrent_agents_per_host: 1\n    )\n\n    state = %Orchestrator.State{\n      running: %{\n        \"issue-1\" => %{worker_host: \"worker-a\"}\n      }\n    }\n\n    assert Orchestrator.select_worker_host_for_test(state, nil) == \"worker-b\"\n  end\n\n  test \"select_worker_host_for_test returns no_worker_capacity when every ssh host is full\" do\n    write_workflow_file!(Workflow.workflow_file_path(),\n      worker_ssh_hosts: [\"worker-a\", \"worker-b\"],\n      worker_max_concurrent_agents_per_host: 1\n    )\n\n    state = %Orchestrator.State{\n      running: %{\n        \"issue-1\" => %{worker_host: \"worker-a\"},\n        \"issue-2\" => %{worker_host: \"worker-b\"}\n      }\n    }\n\n    assert Orchestrator.select_worker_host_for_test(state, nil) == :no_worker_capacity\n  end\n\n  test \"select_worker_host_for_test keeps the preferred ssh host when it still has capacity\" do\n    write_workflow_file!(Workflow.workflow_file_path(),\n      worker_ssh_hosts: [\"worker-a\", \"worker-b\"],\n      worker_max_concurrent_agents_per_host: 2\n    )\n\n    state = %Orchestrator.State{\n      running: %{\n        \"issue-1\" => %{worker_host: \"worker-a\"},\n        \"issue-2\" => %{worker_host: \"worker-b\"}\n      }\n    }\n\n    assert Orchestrator.select_worker_host_for_test(state, \"worker-a\") == \"worker-a\"\n  end\n\n  defp assert_due_in_range(due_at_ms, min_remaining_ms, max_remaining_ms) do\n    remaining_ms = due_at_ms - System.monotonic_time(:millisecond)\n\n    assert remaining_ms >= min_remaining_ms\n    assert remaining_ms <= max_remaining_ms\n  end\n\n  defp restore_app_env(key, nil), do: Application.delete_env(:symphony_elixir, key)\n  defp restore_app_env(key, value), do: Application.put_env(:symphony_elixir, key, value)\n\n  test \"fetch issues by states with empty state set is a no-op\" do\n    assert {:ok, []} = Client.fetch_issues_by_states([])\n  end\n\n  test \"prompt builder renders issue and attempt values from workflow template\" do\n    workflow_prompt =\n      \"Ticket {{ issue.identifier }} {{ issue.title }} labels={{ issue.labels }} attempt={{ attempt }}\"\n\n    write_workflow_file!(Workflow.workflow_file_path(), prompt: workflow_prompt)\n\n    issue = %Issue{\n      identifier: \"S-1\",\n      title: \"Refactor backend request path\",\n      description: \"Replace transport layer\",\n      state: \"Todo\",\n      url: \"https://example.org/issues/S-1\",\n      labels: [\"backend\"]\n    }\n\n    prompt = PromptBuilder.build_prompt(issue, attempt: 3)\n\n    assert prompt =~ \"Ticket S-1 Refactor backend request path\"\n    assert prompt =~ \"labels=backend\"\n    assert prompt =~ \"attempt=3\"\n  end\n\n  test \"prompt builder renders issue datetime fields without crashing\" do\n    workflow_prompt = \"Ticket {{ issue.identifier }} created={{ issue.created_at }} updated={{ issue.updated_at }}\"\n\n    write_workflow_file!(Workflow.workflow_file_path(), prompt: workflow_prompt)\n\n    created_at = DateTime.from_naive!(~N[2026-02-26 18:06:48], \"Etc/UTC\")\n    updated_at = DateTime.from_naive!(~N[2026-02-26 18:07:03], \"Etc/UTC\")\n\n    issue = %Issue{\n      identifier: \"MT-697\",\n      title: \"Live smoke\",\n      description: \"Prompt should serialize datetimes\",\n      state: \"Todo\",\n      url: \"https://example.org/issues/MT-697\",\n      labels: [],\n      created_at: created_at,\n      updated_at: updated_at\n    }\n\n    prompt = PromptBuilder.build_prompt(issue)\n\n    assert prompt =~ \"Ticket MT-697\"\n    assert prompt =~ \"created=2026-02-26T18:06:48Z\"\n    assert prompt =~ \"updated=2026-02-26T18:07:03Z\"\n  end\n\n  test \"prompt builder normalizes nested date-like values, maps, and structs in issue fields\" do\n    write_workflow_file!(Workflow.workflow_file_path(), prompt: \"Ticket {{ issue.identifier }}\")\n\n    issue = %Issue{\n      identifier: \"MT-701\",\n      title: \"Serialize nested values\",\n      description: \"Prompt builder should normalize nested terms\",\n      state: \"Todo\",\n      url: \"https://example.org/issues/MT-701\",\n      labels: [\n        ~N[2026-02-27 12:34:56],\n        ~D[2026-02-28],\n        ~T[12:34:56],\n        %{phase: \"test\"},\n        URI.parse(\"https://example.org/issues/MT-701\")\n      ]\n    }\n\n    assert PromptBuilder.build_prompt(issue) == \"Ticket MT-701\"\n  end\n\n  test \"prompt builder uses strict variable rendering\" do\n    workflow_prompt = \"Work on ticket {{ missing.ticket_id }} and follow these steps.\"\n\n    write_workflow_file!(Workflow.workflow_file_path(), prompt: workflow_prompt)\n\n    issue = %Issue{\n      identifier: \"MT-123\",\n      title: \"Investigate broken sync\",\n      description: \"Reproduce and fix\",\n      state: \"In Progress\",\n      url: \"https://example.org/issues/MT-123\",\n      labels: [\"bug\"]\n    }\n\n    assert_raise Solid.RenderError, fn ->\n      PromptBuilder.build_prompt(issue)\n    end\n  end\n\n  test \"prompt builder surfaces invalid template content with prompt context\" do\n    write_workflow_file!(Workflow.workflow_file_path(), prompt: \"{% if issue.identifier %}\")\n\n    issue = %Issue{\n      identifier: \"MT-999\",\n      title: \"Broken prompt\",\n      description: \"Invalid template syntax\",\n      state: \"Todo\",\n      url: \"https://example.org/issues/MT-999\",\n      labels: []\n    }\n\n    assert_raise RuntimeError, ~r/template_parse_error:.*template=\"/s, fn ->\n      PromptBuilder.build_prompt(issue)\n    end\n  end\n\n  test \"prompt builder uses a sensible default template when workflow prompt is blank\" do\n    write_workflow_file!(Workflow.workflow_file_path(), prompt: \"   \\n\")\n\n    issue = %Issue{\n      identifier: \"MT-777\",\n      title: \"Make fallback prompt useful\",\n      description: \"Include enough issue context to start working.\",\n      state: \"In Progress\",\n      url: \"https://example.org/issues/MT-777\",\n      labels: [\"prompt\"]\n    }\n\n    prompt = PromptBuilder.build_prompt(issue)\n\n    assert prompt =~ \"You are working on a Linear issue.\"\n    assert prompt =~ \"Identifier: MT-777\"\n    assert prompt =~ \"Title: Make fallback prompt useful\"\n    assert prompt =~ \"Body:\"\n    assert prompt =~ \"Include enough issue context to start working.\"\n    assert Config.workflow_prompt() =~ \"{{ issue.identifier }}\"\n    assert Config.workflow_prompt() =~ \"{{ issue.title }}\"\n    assert Config.workflow_prompt() =~ \"{{ issue.description }}\"\n  end\n\n  test \"prompt builder default template handles missing issue body\" do\n    write_workflow_file!(Workflow.workflow_file_path(), prompt: \"\")\n\n    issue = %Issue{\n      identifier: \"MT-778\",\n      title: \"Handle empty body\",\n      description: nil,\n      state: \"Todo\",\n      url: \"https://example.org/issues/MT-778\",\n      labels: []\n    }\n\n    prompt = PromptBuilder.build_prompt(issue)\n\n    assert prompt =~ \"Identifier: MT-778\"\n    assert prompt =~ \"Title: Handle empty body\"\n    assert prompt =~ \"No description provided.\"\n  end\n\n  test \"prompt builder reports workflow load failures separately from template parse errors\" do\n    original_workflow_path = Workflow.workflow_file_path()\n    workflow_store_pid = Process.whereis(SymphonyElixir.WorkflowStore)\n\n    on_exit(fn ->\n      Workflow.set_workflow_file_path(original_workflow_path)\n\n      if is_pid(workflow_store_pid) and is_nil(Process.whereis(SymphonyElixir.WorkflowStore)) do\n        Supervisor.restart_child(SymphonyElixir.Supervisor, SymphonyElixir.WorkflowStore)\n      end\n    end)\n\n    assert :ok = Supervisor.terminate_child(SymphonyElixir.Supervisor, SymphonyElixir.WorkflowStore)\n\n    Workflow.set_workflow_file_path(Path.join(System.tmp_dir!(), \"missing-workflow-#{System.unique_integer([:positive])}.md\"))\n\n    issue = %Issue{\n      identifier: \"MT-780\",\n      title: \"Workflow unavailable\",\n      description: \"Missing workflow file\",\n      state: \"Todo\",\n      url: \"https://example.org/issues/MT-780\",\n      labels: []\n    }\n\n    assert_raise RuntimeError, ~r/workflow_unavailable:/, fn ->\n      PromptBuilder.build_prompt(issue)\n    end\n  end\n\n  test \"in-repo WORKFLOW.md renders correctly\" do\n    workflow_path = Workflow.workflow_file_path()\n    Workflow.set_workflow_file_path(Path.expand(\"WORKFLOW.md\", File.cwd!()))\n\n    issue = %Issue{\n      identifier: \"MT-616\",\n      title: \"Use rich templates for WORKFLOW.md\",\n      description: \"Render with rich template variables\",\n      state: \"In Progress\",\n      url: \"https://example.org/issues/MT-616/use-rich-templates-for-workflowmd\",\n      labels: [\"templating\", \"workflow\"]\n    }\n\n    on_exit(fn -> Workflow.set_workflow_file_path(workflow_path) end)\n\n    prompt = PromptBuilder.build_prompt(issue, attempt: 2)\n\n    assert prompt =~ \"You are working on a Linear ticket `MT-616`\"\n    assert prompt =~ \"Issue context:\"\n    assert prompt =~ \"Identifier: MT-616\"\n    assert prompt =~ \"Title: Use rich templates for WORKFLOW.md\"\n    assert prompt =~ \"Current status: In Progress\"\n    assert prompt =~ \"https://example.org/issues/MT-616/use-rich-templates-for-workflowmd\"\n    assert prompt =~ \"This is an unattended orchestration session.\"\n    assert prompt =~ \"Only stop early for a true blocker\"\n    assert prompt =~ \"Do not include \\\"next steps for user\\\"\"\n    assert prompt =~ \"open and follow `.codex/skills/land/SKILL.md`\"\n    assert prompt =~ \"Do not call `gh pr merge` directly\"\n    assert prompt =~ \"Continuation context:\"\n    assert prompt =~ \"retry attempt #2\"\n  end\n\n  test \"prompt builder adds continuation guidance for retries\" do\n    workflow_prompt = \"{% if attempt %}Retry #\" <> \"{{ attempt }}\" <> \"{% endif %}\"\n    write_workflow_file!(Workflow.workflow_file_path(), prompt: workflow_prompt)\n\n    issue = %Issue{\n      identifier: \"MT-201\",\n      title: \"Continue autonomous ticket\",\n      description: \"Retry flow\",\n      state: \"In Progress\",\n      url: \"https://example.org/issues/MT-201\",\n      labels: []\n    }\n\n    prompt = PromptBuilder.build_prompt(issue, attempt: 2)\n\n    assert prompt == \"Retry #2\"\n  end\n\n  test \"agent runner keeps workspace after successful codex run\" do\n    test_root =\n      Path.join(\n        System.tmp_dir!(),\n        \"symphony-elixir-agent-runner-retain-workspace-#{System.unique_integer([:positive])}\"\n      )\n\n    try do\n      template_repo = Path.join(test_root, \"source\")\n      workspace_root = Path.join(test_root, \"workspaces\")\n      codex_binary = Path.join(test_root, \"fake-codex\")\n\n      File.mkdir_p!(template_repo)\n      File.mkdir_p!(workspace_root)\n      File.write!(Path.join(template_repo, \"README.md\"), \"# test\")\n      System.cmd(\"git\", [\"-C\", template_repo, \"init\", \"-b\", \"main\"])\n      System.cmd(\"git\", [\"-C\", template_repo, \"config\", \"user.name\", \"Test User\"])\n      System.cmd(\"git\", [\"-C\", template_repo, \"config\", \"user.email\", \"test@example.com\"])\n      System.cmd(\"git\", [\"-C\", template_repo, \"add\", \"README.md\"])\n      System.cmd(\"git\", [\"-C\", template_repo, \"commit\", \"-m\", \"initial\"])\n\n      File.write!(codex_binary, \"\"\"\n      #!/bin/sh\n      count=0\n      while IFS= read -r line; do\n        count=$((count + 1))\n        case \"$count\" in\n          1)\n            printf '%s\\\\n' '{\\\"id\\\":1,\\\"result\\\":{}}'\n            ;;\n          2)\n            ;;\n          3)\n            printf '%s\\\\n' '{\\\"id\\\":2,\\\"result\\\":{\\\"thread\\\":{\\\"id\\\":\\\"thread-1\\\"}}}'\n            ;;\n          4)\n            printf '%s\\\\n' '{\\\"id\\\":3,\\\"result\\\":{\\\"turn\\\":{\\\"id\\\":\\\"turn-1\\\"}}}'\n            printf '%s\\\\n' '{\\\"method\\\":\\\"turn/completed\\\"}'\n            exit 0\n            ;;\n          *)\n            ;;\n        esac\n      done\n      \"\"\")\n\n      File.chmod!(codex_binary, 0o755)\n\n      write_workflow_file!(Workflow.workflow_file_path(),\n        workspace_root: workspace_root,\n        hook_after_create: \"cp #{Path.join(template_repo, \"README.md\")} README.md\",\n        codex_command: \"#{codex_binary} app-server\"\n      )\n\n      issue = %Issue{\n        identifier: \"S-99\",\n        title: \"Smoke test\",\n        description: \"Run and keep workspace\",\n        state: \"In Progress\",\n        url: \"https://example.org/issues/S-99\",\n        labels: [\"backend\"]\n      }\n\n      before = MapSet.new(File.ls!(workspace_root))\n      assert :ok = AgentRunner.run(issue)\n      entries_after = MapSet.new(File.ls!(workspace_root))\n\n      created =\n        MapSet.difference(entries_after, before) |> Enum.filter(&(&1 == \"S-99\"))\n\n      created = MapSet.new(created)\n\n      assert MapSet.size(created) == 1\n      workspace_name = created |> Enum.to_list() |> List.first()\n      assert workspace_name == \"S-99\"\n\n      workspace = Path.join(workspace_root, workspace_name)\n      assert File.exists?(workspace)\n      assert File.exists?(Path.join(workspace, \"README.md\"))\n    after\n      File.rm_rf(test_root)\n    end\n  end\n\n  test \"agent runner forwards timestamped codex updates to recipient\" do\n    test_root =\n      Path.join(\n        System.tmp_dir!(),\n        \"symphony-elixir-agent-runner-updates-#{System.unique_integer([:positive])}\"\n      )\n\n    try do\n      template_repo = Path.join(test_root, \"source\")\n      workspace_root = Path.join(test_root, \"workspaces\")\n      codex_binary = Path.join(test_root, \"fake-codex\")\n\n      File.mkdir_p!(template_repo)\n      File.write!(Path.join(template_repo, \"README.md\"), \"# test\")\n      System.cmd(\"git\", [\"-C\", template_repo, \"init\", \"-b\", \"main\"])\n      System.cmd(\"git\", [\"-C\", template_repo, \"config\", \"user.name\", \"Test User\"])\n      System.cmd(\"git\", [\"-C\", template_repo, \"config\", \"user.email\", \"test@example.com\"])\n      System.cmd(\"git\", [\"-C\", template_repo, \"add\", \"README.md\"])\n      System.cmd(\"git\", [\"-C\", template_repo, \"commit\", \"-m\", \"initial\"])\n\n      File.write!(\n        codex_binary,\n        \"\"\"\n        #!/bin/sh\n        count=0\n        while IFS= read -r line; do\n          count=$((count + 1))\n          case \"$count\" in\n            1)\n              printf '%s\\\\n' '{\\\"id\\\":1,\\\"result\\\":{}}'\n              ;;\n            2)\n              printf '%s\\\\n' '{\\\"id\\\":2,\\\"result\\\":{\\\"thread\\\":{\\\"id\\\":\\\"thread-live\\\"}}}'\n              ;;\n            3)\n              printf '%s\\\\n' '{\\\"id\\\":3,\\\"result\\\":{\\\"turn\\\":{\\\"id\\\":\\\"turn-live\\\"}}}'\n              ;;\n            4)\n              printf '%s\\\\n' '{\\\"method\\\":\\\"turn/completed\\\"}'\n              ;;\n            *)\n              ;;\n          esac\n        done\n        \"\"\"\n      )\n\n      File.chmod!(codex_binary, 0o755)\n\n      write_workflow_file!(Workflow.workflow_file_path(),\n        workspace_root: workspace_root,\n        hook_after_create: \"cp #{Path.join(template_repo, \"README.md\")} README.md\",\n        codex_command: \"#{codex_binary} app-server\"\n      )\n\n      issue = %Issue{\n        id: \"issue-live-updates\",\n        identifier: \"MT-99\",\n        title: \"Smoke test\",\n        description: \"Capture codex updates\",\n        state: \"In Progress\",\n        url: \"https://example.org/issues/MT-99\",\n        labels: [\"backend\"]\n      }\n\n      test_pid = self()\n\n      assert :ok =\n               AgentRunner.run(\n                 issue,\n                 test_pid,\n                 issue_state_fetcher: fn [_issue_id] -> {:ok, [%{issue | state: \"Done\"}]} end\n               )\n\n      assert_receive {:codex_worker_update, \"issue-live-updates\",\n                      %{\n                        event: :session_started,\n                        timestamp: %DateTime{},\n                        session_id: session_id\n                      }},\n                     500\n\n      assert session_id == \"thread-live-turn-live\"\n    after\n      File.rm_rf(test_root)\n    end\n  end\n\n  test \"agent runner surfaces ssh startup failures instead of silently hopping hosts\" do\n    test_root =\n      Path.join(\n        System.tmp_dir!(),\n        \"symphony-elixir-agent-runner-single-host-#{System.unique_integer([:positive])}\"\n      )\n\n    previous_path = System.get_env(\"PATH\")\n    previous_trace = System.get_env(\"SYMP_TEST_SSH_TRACE\")\n\n    on_exit(fn ->\n      restore_env(\"PATH\", previous_path)\n      restore_env(\"SYMP_TEST_SSH_TRACE\", previous_trace)\n    end)\n\n    try do\n      trace_file = Path.join(test_root, \"ssh.trace\")\n      fake_ssh = Path.join(test_root, \"ssh\")\n\n      File.mkdir_p!(test_root)\n      System.put_env(\"SYMP_TEST_SSH_TRACE\", trace_file)\n      System.put_env(\"PATH\", test_root <> \":\" <> (previous_path || \"\"))\n\n      File.write!(fake_ssh, \"\"\"\n      #!/bin/sh\n      trace_file=\"${SYMP_TEST_SSH_TRACE:-/tmp/symphony-fake-ssh.trace}\"\n      printf 'ARGV:%s\\\\n' \"$*\" >> \"$trace_file\"\n\n      case \"$*\" in\n        *worker-a*\"__SYMPHONY_WORKSPACE__\"*)\n          printf '%s\\\\n' 'worker-a prepare failed' >&2\n          exit 75\n          ;;\n        *worker-b*\"__SYMPHONY_WORKSPACE__\"*)\n          printf '%s\\\\t%s\\\\t%s\\\\n' '__SYMPHONY_WORKSPACE__' '1' '/remote/home/.symphony-remote-workspaces/MT-SSH-FAILOVER'\n          exit 0\n          ;;\n        *)\n          exit 0\n          ;;\n      esac\n      \"\"\")\n\n      File.chmod!(fake_ssh, 0o755)\n\n      write_workflow_file!(Workflow.workflow_file_path(),\n        workspace_root: \"~/.symphony-remote-workspaces\",\n        worker_ssh_hosts: [\"worker-a\", \"worker-b\"]\n      )\n\n      issue = %Issue{\n        id: \"issue-ssh-failover\",\n        identifier: \"MT-SSH-FAILOVER\",\n        title: \"Do not fail over within a single worker run\",\n        description: \"Surface the startup failure to the orchestrator\",\n        state: \"In Progress\"\n      }\n\n      assert_raise RuntimeError, ~r/workspace_prepare_failed/, fn ->\n        AgentRunner.run(issue, nil, worker_host: \"worker-a\")\n      end\n\n      trace = File.read!(trace_file)\n      assert trace =~ \"worker-a bash -lc\"\n      refute trace =~ \"worker-b bash -lc\"\n    after\n      File.rm_rf(test_root)\n    end\n  end\n\n  test \"agent runner continues with a follow-up turn while the issue remains active\" do\n    test_root =\n      Path.join(\n        System.tmp_dir!(),\n        \"symphony-elixir-agent-runner-continuation-#{System.unique_integer([:positive])}\"\n      )\n\n    try do\n      template_repo = Path.join(test_root, \"source\")\n      workspace_root = Path.join(test_root, \"workspaces\")\n      codex_binary = Path.join(test_root, \"fake-codex\")\n      trace_file = Path.join(test_root, \"codex.trace\")\n\n      File.mkdir_p!(template_repo)\n      File.write!(Path.join(template_repo, \"README.md\"), \"# test\")\n      System.cmd(\"git\", [\"-C\", template_repo, \"init\", \"-b\", \"main\"])\n      System.cmd(\"git\", [\"-C\", template_repo, \"config\", \"user.name\", \"Test User\"])\n      System.cmd(\"git\", [\"-C\", template_repo, \"config\", \"user.email\", \"test@example.com\"])\n      System.cmd(\"git\", [\"-C\", template_repo, \"add\", \"README.md\"])\n      System.cmd(\"git\", [\"-C\", template_repo, \"commit\", \"-m\", \"initial\"])\n\n      File.write!(codex_binary, \"\"\"\n      #!/bin/sh\n      trace_file=\"${SYMP_TEST_CODEx_TRACE:-/tmp/codex.trace}\"\n      run_id=\"$(date +%s%N)-$$\"\n      printf 'RUN:%s\\\\n' \"$run_id\" >> \"$trace_file\"\n      count=0\n\n      while IFS= read -r line; do\n        count=$((count + 1))\n        printf 'JSON:%s\\\\n' \"$line\" >> \"$trace_file\"\n        case \"$count\" in\n          1)\n            printf '%s\\\\n' '{\"id\":1,\"result\":{}}'\n            ;;\n          2)\n            ;;\n          3)\n            printf '%s\\\\n' '{\"id\":2,\"result\":{\"thread\":{\"id\":\"thread-cont\"}}}'\n            ;;\n          4)\n            printf '%s\\\\n' '{\"id\":3,\"result\":{\"turn\":{\"id\":\"turn-cont-1\"}}}'\n            printf '%s\\\\n' '{\"method\":\"turn/completed\"}'\n            ;;\n          5)\n            printf '%s\\\\n' '{\"id\":3,\"result\":{\"turn\":{\"id\":\"turn-cont-2\"}}}'\n            printf '%s\\\\n' '{\"method\":\"turn/completed\"}'\n            ;;\n        esac\n      done\n      \"\"\")\n\n      File.chmod!(codex_binary, 0o755)\n      System.put_env(\"SYMP_TEST_CODEx_TRACE\", trace_file)\n\n      on_exit(fn -> System.delete_env(\"SYMP_TEST_CODEx_TRACE\") end)\n\n      write_workflow_file!(Workflow.workflow_file_path(),\n        workspace_root: workspace_root,\n        hook_after_create: \"cp #{Path.join(template_repo, \"README.md\")} README.md\",\n        codex_command: \"#{codex_binary} app-server\",\n        max_turns: 3\n      )\n\n      parent = self()\n\n      state_fetcher = fn [_issue_id] ->\n        attempt = Process.get(:agent_turn_fetch_count, 0) + 1\n        Process.put(:agent_turn_fetch_count, attempt)\n        send(parent, {:issue_state_fetch, attempt})\n\n        state =\n          if attempt == 1 do\n            \"In Progress\"\n          else\n            \"Done\"\n          end\n\n        {:ok,\n         [\n           %Issue{\n             id: \"issue-continue\",\n             identifier: \"MT-247\",\n             title: \"Continue until done\",\n             description: \"Still active after first turn\",\n             state: state\n           }\n         ]}\n      end\n\n      issue = %Issue{\n        id: \"issue-continue\",\n        identifier: \"MT-247\",\n        title: \"Continue until done\",\n        description: \"Still active after first turn\",\n        state: \"In Progress\",\n        url: \"https://example.org/issues/MT-247\",\n        labels: []\n      }\n\n      assert :ok = AgentRunner.run(issue, nil, issue_state_fetcher: state_fetcher)\n      assert_receive {:issue_state_fetch, 1}\n      assert_receive {:issue_state_fetch, 2}\n\n      lines = File.read!(trace_file) |> String.split(\"\\n\", trim: true)\n\n      assert length(Enum.filter(lines, &String.starts_with?(&1, \"RUN:\"))) == 1\n      assert length(Enum.filter(lines, &String.contains?(&1, \"\\\"method\\\":\\\"thread/start\\\"\"))) == 1\n\n      turn_texts =\n        lines\n        |> Enum.filter(&String.starts_with?(&1, \"JSON:\"))\n        |> Enum.map(&String.trim_leading(&1, \"JSON:\"))\n        |> Enum.map(&Jason.decode!/1)\n        |> Enum.filter(&(&1[\"method\"] == \"turn/start\"))\n        |> Enum.map(fn payload ->\n          get_in(payload, [\"params\", \"input\"])\n          |> Enum.map_join(\"\\n\", &Map.get(&1, \"text\", \"\"))\n        end)\n\n      assert length(turn_texts) == 2\n      assert Enum.at(turn_texts, 0) =~ \"You are an agent for this repository.\"\n      refute Enum.at(turn_texts, 1) =~ \"You are an agent for this repository.\"\n      assert Enum.at(turn_texts, 1) =~ \"Continuation guidance:\"\n      assert Enum.at(turn_texts, 1) =~ \"continuation turn #2 of 3\"\n    after\n      System.delete_env(\"SYMP_TEST_CODEx_TRACE\")\n      File.rm_rf(test_root)\n    end\n  end\n\n  test \"agent runner stops continuing once agent.max_turns is reached\" do\n    test_root =\n      Path.join(\n        System.tmp_dir!(),\n        \"symphony-elixir-agent-runner-max-turns-#{System.unique_integer([:positive])}\"\n      )\n\n    try do\n      template_repo = Path.join(test_root, \"source\")\n      workspace_root = Path.join(test_root, \"workspaces\")\n      codex_binary = Path.join(test_root, \"fake-codex\")\n      trace_file = Path.join(test_root, \"codex.trace\")\n\n      File.mkdir_p!(template_repo)\n      File.write!(Path.join(template_repo, \"README.md\"), \"# test\")\n      System.cmd(\"git\", [\"-C\", template_repo, \"init\", \"-b\", \"main\"])\n      System.cmd(\"git\", [\"-C\", template_repo, \"config\", \"user.name\", \"Test User\"])\n      System.cmd(\"git\", [\"-C\", template_repo, \"config\", \"user.email\", \"test@example.com\"])\n      System.cmd(\"git\", [\"-C\", template_repo, \"add\", \"README.md\"])\n      System.cmd(\"git\", [\"-C\", template_repo, \"commit\", \"-m\", \"initial\"])\n\n      File.write!(codex_binary, \"\"\"\n      #!/bin/sh\n      trace_file=\"${SYMP_TEST_CODEx_TRACE:-/tmp/codex.trace}\"\n      printf 'RUN\\\\n' >> \"$trace_file\"\n      count=0\n\n      while IFS= read -r line; do\n        count=$((count + 1))\n        printf 'JSON:%s\\\\n' \"$line\" >> \"$trace_file\"\n        case \"$count\" in\n          1)\n            printf '%s\\\\n' '{\"id\":1,\"result\":{}}'\n            ;;\n          2)\n            ;;\n          3)\n            printf '%s\\\\n' '{\"id\":2,\"result\":{\"thread\":{\"id\":\"thread-max\"}}}'\n            ;;\n          4)\n            printf '%s\\\\n' '{\"id\":3,\"result\":{\"turn\":{\"id\":\"turn-max-1\"}}}'\n            printf '%s\\\\n' '{\"method\":\"turn/completed\"}'\n            ;;\n          5)\n            printf '%s\\\\n' '{\"id\":3,\"result\":{\"turn\":{\"id\":\"turn-max-2\"}}}'\n            printf '%s\\\\n' '{\"method\":\"turn/completed\"}'\n            ;;\n        esac\n      done\n      \"\"\")\n\n      File.chmod!(codex_binary, 0o755)\n      System.put_env(\"SYMP_TEST_CODEx_TRACE\", trace_file)\n\n      on_exit(fn -> System.delete_env(\"SYMP_TEST_CODEx_TRACE\") end)\n\n      write_workflow_file!(Workflow.workflow_file_path(),\n        workspace_root: workspace_root,\n        hook_after_create: \"cp #{Path.join(template_repo, \"README.md\")} README.md\",\n        codex_command: \"#{codex_binary} app-server\",\n        max_turns: 2\n      )\n\n      state_fetcher = fn [_issue_id] ->\n        {:ok,\n         [\n           %Issue{\n             id: \"issue-max-turns\",\n             identifier: \"MT-248\",\n             title: \"Stop at max turns\",\n             description: \"Still active\",\n             state: \"In Progress\"\n           }\n         ]}\n      end\n\n      issue = %Issue{\n        id: \"issue-max-turns\",\n        identifier: \"MT-248\",\n        title: \"Stop at max turns\",\n        description: \"Still active\",\n        state: \"In Progress\",\n        url: \"https://example.org/issues/MT-248\",\n        labels: []\n      }\n\n      assert :ok = AgentRunner.run(issue, nil, issue_state_fetcher: state_fetcher)\n\n      trace = File.read!(trace_file)\n      assert length(String.split(trace, \"RUN\", trim: true)) == 1\n      assert length(Regex.scan(~r/\"method\":\"turn\\/start\"/, trace)) == 2\n    after\n      System.delete_env(\"SYMP_TEST_CODEx_TRACE\")\n      File.rm_rf(test_root)\n    end\n  end\n\n  test \"app server starts with workspace cwd and expected startup command\" do\n    test_root =\n      Path.join(\n        System.tmp_dir!(),\n        \"symphony-elixir-app-server-args-#{System.unique_integer([:positive])}\"\n      )\n\n    try do\n      workspace_root = Path.join(test_root, \"workspaces\")\n      workspace = Path.join(workspace_root, \"MT-77\")\n      codex_binary = Path.join(test_root, \"fake-codex\")\n      trace_file = Path.join(test_root, \"codex-args.trace\")\n      previous_trace = System.get_env(\"SYMP_TEST_CODex_TRACE\")\n\n      on_exit(fn ->\n        if is_binary(previous_trace) do\n          System.put_env(\"SYMP_TEST_CODex_TRACE\", previous_trace)\n        else\n          System.delete_env(\"SYMP_TEST_CODex_TRACE\")\n        end\n      end)\n\n      System.put_env(\"SYMP_TEST_CODex_TRACE\", trace_file)\n      File.mkdir_p!(workspace)\n\n      File.write!(codex_binary, \"\"\"\n      #!/bin/sh\n      trace_file=\"${SYMP_TEST_CODex_TRACE:-/tmp/codex-args.trace}\"\n      count=0\n      printf 'ARGV:%s\\\\n' \\\"$*\\\" >> \\\"$trace_file\\\"\n      printf 'CWD:%s\\\\n' \\\"$PWD\\\" >> \\\"$trace_file\\\"\n\n      while IFS= read -r line; do\n        count=$((count + 1))\n        printf 'JSON:%s\\\\n' \\\"$line\\\" >> \\\"$trace_file\\\"\n        case \\\"$count\\\" in\n          1)\n            printf '%s\\\\n' '{\\\"id\\\":1,\\\"result\\\":{}}'\n            ;;\n          2)\n            printf '%s\\\\n' '{\\\"id\\\":2,\\\"result\\\":{\\\"thread\\\":{\\\"id\\\":\\\"thread-77\\\"}}}'\n            ;;\n          3)\n            printf '%s\\\\n' '{\\\"id\\\":3,\\\"result\\\":{\\\"turn\\\":{\\\"id\\\":\\\"turn-77\\\"}}}'\n            ;;\n          4)\n            printf '%s\\\\n' '{\\\"method\\\":\\\"turn/completed\\\"}'\n            exit 0\n            ;;\n          *)\n            exit 0\n            ;;\n        esac\n      done\n      \"\"\")\n\n      File.chmod!(codex_binary, 0o755)\n\n      write_workflow_file!(Workflow.workflow_file_path(),\n        workspace_root: workspace_root,\n        codex_command: \"#{codex_binary} app-server\"\n      )\n\n      issue = %Issue{\n        id: \"issue-args\",\n        identifier: \"MT-77\",\n        title: \"Validate codex args\",\n        description: \"Check startup args and cwd\",\n        state: \"In Progress\",\n        url: \"https://example.org/issues/MT-77\",\n        labels: [\"backend\"]\n      }\n\n      assert {:ok, _result} = AppServer.run(workspace, \"Fix workspace start args\", issue)\n      assert {:ok, canonical_workspace} = SymphonyElixir.PathSafety.canonicalize(workspace)\n\n      trace = File.read!(trace_file)\n      lines = String.split(trace, \"\\n\", trim: true)\n\n      assert argv_line = Enum.find(lines, fn line -> String.starts_with?(line, \"ARGV:\") end)\n      assert String.contains?(argv_line, \"app-server\")\n      refute Enum.any?(lines, &String.contains?(&1, \"--yolo\"))\n      assert cwd_line = Enum.find(lines, fn line -> String.starts_with?(line, \"CWD:\") end)\n      assert String.ends_with?(cwd_line, Path.basename(workspace))\n\n      assert Enum.any?(lines, fn line ->\n               if String.starts_with?(line, \"JSON:\") do\n                 line\n                 |> String.trim_leading(\"JSON:\")\n                 |> Jason.decode!()\n                 |> then(fn payload ->\n                   expected_approval_policy = %{\n                     \"reject\" => %{\n                       \"sandbox_approval\" => true,\n                       \"rules\" => true,\n                       \"mcp_elicitations\" => true\n                     }\n                   }\n\n                   payload[\"method\"] == \"thread/start\" &&\n                     get_in(payload, [\"params\", \"approvalPolicy\"]) == expected_approval_policy &&\n                     get_in(payload, [\"params\", \"sandbox\"]) == \"workspace-write\" &&\n                     get_in(payload, [\"params\", \"cwd\"]) == canonical_workspace\n                 end)\n               else\n                 false\n               end\n             end)\n\n      expected_turn_sandbox_policy = %{\n        \"type\" => \"workspaceWrite\",\n        \"writableRoots\" => [canonical_workspace],\n        \"readOnlyAccess\" => %{\"type\" => \"fullAccess\"},\n        \"networkAccess\" => false,\n        \"excludeTmpdirEnvVar\" => false,\n        \"excludeSlashTmp\" => false\n      }\n\n      assert Enum.any?(lines, fn line ->\n               if String.starts_with?(line, \"JSON:\") do\n                 line\n                 |> String.trim_leading(\"JSON:\")\n                 |> Jason.decode!()\n                 |> then(fn payload ->\n                   expected_approval_policy = %{\n                     \"reject\" => %{\n                       \"sandbox_approval\" => true,\n                       \"rules\" => true,\n                       \"mcp_elicitations\" => true\n                     }\n                   }\n\n                   payload[\"method\"] == \"turn/start\" &&\n                     get_in(payload, [\"params\", \"cwd\"]) == canonical_workspace &&\n                     get_in(payload, [\"params\", \"approvalPolicy\"]) == expected_approval_policy &&\n                     get_in(payload, [\"params\", \"sandboxPolicy\"]) == expected_turn_sandbox_policy\n                 end)\n               else\n                 false\n               end\n             end)\n    after\n      File.rm_rf(test_root)\n    end\n  end\n\n  test \"app server startup command supports codex args override from workflow config\" do\n    test_root =\n      Path.join(\n        System.tmp_dir!(),\n        \"symphony-elixir-app-server-custom-args-#{System.unique_integer([:positive])}\"\n      )\n\n    try do\n      workspace_root = Path.join(test_root, \"workspaces\")\n      workspace = Path.join(workspace_root, \"MT-88\")\n      codex_binary = Path.join(test_root, \"fake-codex\")\n      trace_file = Path.join(test_root, \"codex-custom-args.trace\")\n      previous_trace = System.get_env(\"SYMP_TEST_CODex_TRACE\")\n\n      on_exit(fn ->\n        if is_binary(previous_trace) do\n          System.put_env(\"SYMP_TEST_CODex_TRACE\", previous_trace)\n        else\n          System.delete_env(\"SYMP_TEST_CODex_TRACE\")\n        end\n      end)\n\n      System.put_env(\"SYMP_TEST_CODex_TRACE\", trace_file)\n      File.mkdir_p!(workspace)\n\n      File.write!(codex_binary, \"\"\"\n      #!/bin/sh\n      trace_file=\"${SYMP_TEST_CODex_TRACE:-/tmp/codex-custom-args.trace}\"\n      count=0\n      printf 'ARGV:%s\\\\n' \\\"$*\\\" >> \\\"$trace_file\\\"\n\n      while IFS= read -r line; do\n        count=$((count + 1))\n        case \\\"$count\\\" in\n          1)\n            printf '%s\\\\n' '{\\\"id\\\":1,\\\"result\\\":{}}'\n            ;;\n          2)\n            printf '%s\\\\n' '{\\\"id\\\":2,\\\"result\\\":{\\\"thread\\\":{\\\"id\\\":\\\"thread-88\\\"}}}'\n            ;;\n          3)\n            printf '%s\\\\n' '{\\\"id\\\":3,\\\"result\\\":{\\\"turn\\\":{\\\"id\\\":\\\"turn-88\\\"}}}'\n            ;;\n          4)\n            printf '%s\\\\n' '{\\\"method\\\":\\\"turn/completed\\\"}'\n            exit 0\n            ;;\n          *)\n            exit 0\n            ;;\n        esac\n      done\n      \"\"\")\n\n      File.chmod!(codex_binary, 0o755)\n\n      write_workflow_file!(Workflow.workflow_file_path(),\n        workspace_root: workspace_root,\n        codex_command: \"#{codex_binary} --model gpt-5.3-codex app-server\"\n      )\n\n      issue = %Issue{\n        id: \"issue-custom-args\",\n        identifier: \"MT-88\",\n        title: \"Validate custom codex args\",\n        description: \"Check startup args override\",\n        state: \"In Progress\",\n        url: \"https://example.org/issues/MT-88\",\n        labels: [\"backend\"]\n      }\n\n      assert {:ok, _result} = AppServer.run(workspace, \"Fix workspace start args\", issue)\n\n      trace = File.read!(trace_file)\n      lines = String.split(trace, \"\\n\", trim: true)\n\n      assert argv_line = Enum.find(lines, fn line -> String.starts_with?(line, \"ARGV:\") end)\n      assert String.contains?(argv_line, \"--model gpt-5.3-codex app-server\")\n      refute String.contains?(argv_line, \"--ask-for-approval never\")\n      refute String.contains?(argv_line, \"--sandbox danger-full-access\")\n    after\n      File.rm_rf(test_root)\n    end\n  end\n\n  test \"app server startup payload uses configurable approval and sandbox settings from workflow config\" do\n    test_root =\n      Path.join(\n        System.tmp_dir!(),\n        \"symphony-elixir-app-server-policy-overrides-#{System.unique_integer([:positive])}\"\n      )\n\n    try do\n      workspace_root = Path.join(test_root, \"workspaces\")\n      workspace = Path.join(workspace_root, \"MT-99\")\n      codex_binary = Path.join(test_root, \"fake-codex\")\n      trace_file = Path.join(test_root, \"codex-policy-overrides.trace\")\n      previous_trace = System.get_env(\"SYMP_TEST_CODex_TRACE\")\n\n      on_exit(fn ->\n        if is_binary(previous_trace) do\n          System.put_env(\"SYMP_TEST_CODex_TRACE\", previous_trace)\n        else\n          System.delete_env(\"SYMP_TEST_CODex_TRACE\")\n        end\n      end)\n\n      System.put_env(\"SYMP_TEST_CODex_TRACE\", trace_file)\n      File.mkdir_p!(workspace)\n\n      File.write!(codex_binary, \"\"\"\n      #!/bin/sh\n      trace_file=\"${SYMP_TEST_CODex_TRACE:-/tmp/codex-policy-overrides.trace}\"\n      count=0\n\n      while IFS= read -r line; do\n        count=$((count + 1))\n        printf 'JSON:%s\\\\n' \"$line\" >> \"$trace_file\"\n\n        case \"$count\" in\n          1)\n            printf '%s\\\\n' '{\"id\":1,\"result\":{}}'\n            ;;\n          2)\n            printf '%s\\\\n' '{\"id\":2,\"result\":{\"thread\":{\"id\":\"thread-99\"}}}'\n            ;;\n          3)\n            printf '%s\\\\n' '{\"id\":3,\"result\":{\"turn\":{\"id\":\"turn-99\"}}}'\n            ;;\n          4)\n            printf '%s\\\\n' '{\"method\":\"turn/completed\"}'\n            exit 0\n            ;;\n          *)\n            exit 0\n            ;;\n        esac\n      done\n      \"\"\")\n\n      File.chmod!(codex_binary, 0o755)\n\n      workspace_cache = Path.join(Path.expand(workspace), \".cache\")\n      File.mkdir_p!(workspace_cache)\n\n      write_workflow_file!(Workflow.workflow_file_path(),\n        workspace_root: workspace_root,\n        codex_command: \"#{codex_binary} app-server\",\n        codex_approval_policy: \"on-request\",\n        codex_thread_sandbox: \"workspace-write\",\n        codex_turn_sandbox_policy: %{\n          type: \"workspaceWrite\",\n          writableRoots: [Path.expand(workspace), workspace_cache]\n        }\n      )\n\n      issue = %Issue{\n        id: \"issue-policy-overrides\",\n        identifier: \"MT-99\",\n        title: \"Validate codex policy overrides\",\n        description: \"Check startup policy payload overrides\",\n        state: \"In Progress\",\n        url: \"https://example.org/issues/MT-99\",\n        labels: [\"backend\"]\n      }\n\n      assert {:ok, _result} = AppServer.run(workspace, \"Fix workspace start args\", issue)\n\n      lines = File.read!(trace_file) |> String.split(\"\\n\", trim: true)\n\n      assert Enum.any?(lines, fn line ->\n               if String.starts_with?(line, \"JSON:\") do\n                 line\n                 |> String.trim_leading(\"JSON:\")\n                 |> Jason.decode!()\n                 |> then(fn payload ->\n                   payload[\"method\"] == \"thread/start\" &&\n                     get_in(payload, [\"params\", \"approvalPolicy\"]) == \"on-request\" &&\n                     get_in(payload, [\"params\", \"sandbox\"]) == \"workspace-write\"\n                 end)\n               else\n                 false\n               end\n             end)\n\n      expected_turn_policy = %{\n        \"type\" => \"workspaceWrite\",\n        \"writableRoots\" => [Path.expand(workspace), workspace_cache]\n      }\n\n      assert Enum.any?(lines, fn line ->\n               if String.starts_with?(line, \"JSON:\") do\n                 line\n                 |> String.trim_leading(\"JSON:\")\n                 |> Jason.decode!()\n                 |> then(fn payload ->\n                   payload[\"method\"] == \"turn/start\" &&\n                     get_in(payload, [\"params\", \"approvalPolicy\"]) == \"on-request\" &&\n                     get_in(payload, [\"params\", \"sandboxPolicy\"]) == expected_turn_policy\n                 end)\n               else\n                 false\n               end\n             end)\n    after\n      File.rm_rf(test_root)\n    end\n  end\nend\n"
  },
  {
    "path": "elixir/test/symphony_elixir/dynamic_tool_test.exs",
    "content": "defmodule SymphonyElixir.Codex.DynamicToolTest do\n  use SymphonyElixir.TestSupport\n\n  alias SymphonyElixir.Codex.DynamicTool\n\n  test \"tool_specs advertises the linear_graphql input contract\" do\n    assert [\n             %{\n               \"description\" => description,\n               \"inputSchema\" => %{\n                 \"properties\" => %{\n                   \"query\" => _,\n                   \"variables\" => _\n                 },\n                 \"required\" => [\"query\"],\n                 \"type\" => \"object\"\n               },\n               \"name\" => \"linear_graphql\"\n             }\n           ] = DynamicTool.tool_specs()\n\n    assert description =~ \"Linear\"\n  end\n\n  test \"unsupported tools return a failure payload with the supported tool list\" do\n    response = DynamicTool.execute(\"not_a_real_tool\", %{})\n\n    assert response[\"success\"] == false\n\n    assert Jason.decode!(response[\"output\"]) == %{\n             \"error\" => %{\n               \"message\" => ~s(Unsupported dynamic tool: \"not_a_real_tool\".),\n               \"supportedTools\" => [\"linear_graphql\"]\n             }\n           }\n\n    assert response[\"contentItems\"] == [\n             %{\n               \"type\" => \"inputText\",\n               \"text\" => response[\"output\"]\n             }\n           ]\n  end\n\n  test \"linear_graphql returns successful GraphQL responses as tool text\" do\n    test_pid = self()\n\n    response =\n      DynamicTool.execute(\n        \"linear_graphql\",\n        %{\n          \"query\" => \"query Viewer { viewer { id } }\",\n          \"variables\" => %{\"includeTeams\" => false}\n        },\n        linear_client: fn query, variables, opts ->\n          send(test_pid, {:linear_client_called, query, variables, opts})\n          {:ok, %{\"data\" => %{\"viewer\" => %{\"id\" => \"usr_123\"}}}}\n        end\n      )\n\n    assert_received {:linear_client_called, \"query Viewer { viewer { id } }\", %{\"includeTeams\" => false}, []}\n\n    assert response[\"success\"] == true\n    assert Jason.decode!(response[\"output\"]) == %{\"data\" => %{\"viewer\" => %{\"id\" => \"usr_123\"}}}\n    assert response[\"contentItems\"] == [%{\"type\" => \"inputText\", \"text\" => response[\"output\"]}]\n  end\n\n  test \"linear_graphql accepts a raw GraphQL query string\" do\n    test_pid = self()\n\n    response =\n      DynamicTool.execute(\n        \"linear_graphql\",\n        \"  query Viewer { viewer { id } }  \",\n        linear_client: fn query, variables, opts ->\n          send(test_pid, {:linear_client_called, query, variables, opts})\n          {:ok, %{\"data\" => %{\"viewer\" => %{\"id\" => \"usr_456\"}}}}\n        end\n      )\n\n    assert_received {:linear_client_called, \"query Viewer { viewer { id } }\", %{}, []}\n    assert response[\"success\"] == true\n  end\n\n  test \"linear_graphql ignores legacy operationName arguments\" do\n    test_pid = self()\n\n    response =\n      DynamicTool.execute(\n        \"linear_graphql\",\n        %{\"query\" => \"query Viewer { viewer { id } }\", \"operationName\" => \"Viewer\"},\n        linear_client: fn query, variables, opts ->\n          send(test_pid, {:linear_client_called, query, variables, opts})\n          {:ok, %{\"data\" => %{\"viewer\" => %{\"id\" => \"usr_789\"}}}}\n        end\n      )\n\n    assert_received {:linear_client_called, \"query Viewer { viewer { id } }\", %{}, []}\n    assert response[\"success\"] == true\n  end\n\n  test \"linear_graphql passes multi-operation documents through unchanged\" do\n    test_pid = self()\n\n    query = \"\"\"\n    query Viewer { viewer { id } }\n    query Teams { teams { nodes { id } } }\n    \"\"\"\n\n    response =\n      DynamicTool.execute(\n        \"linear_graphql\",\n        %{\"query\" => query},\n        linear_client: fn forwarded_query, variables, opts ->\n          send(test_pid, {:linear_client_called, forwarded_query, variables, opts})\n          {:ok, %{\"errors\" => [%{\"message\" => \"Must provide operation name if query contains multiple operations.\"}]}}\n        end\n      )\n\n    assert_received {:linear_client_called, forwarded_query, %{}, []}\n    assert forwarded_query == String.trim(query)\n    assert response[\"success\"] == false\n  end\n\n  test \"linear_graphql rejects blank raw query strings even when using the default client\" do\n    response = DynamicTool.execute(\"linear_graphql\", \"   \")\n\n    assert response[\"success\"] == false\n\n    assert Jason.decode!(response[\"output\"]) == %{\n             \"error\" => %{\n               \"message\" => \"`linear_graphql` requires a non-empty `query` string.\"\n             }\n           }\n  end\n\n  test \"linear_graphql marks GraphQL error responses as failures while preserving the body\" do\n    response =\n      DynamicTool.execute(\n        \"linear_graphql\",\n        %{\"query\" => \"mutation BadMutation { nope }\"},\n        linear_client: fn _query, _variables, _opts ->\n          {:ok, %{\"errors\" => [%{\"message\" => \"Unknown field `nope`\"}], \"data\" => nil}}\n        end\n      )\n\n    assert response[\"success\"] == false\n\n    assert Jason.decode!(response[\"output\"]) == %{\n             \"data\" => nil,\n             \"errors\" => [%{\"message\" => \"Unknown field `nope`\"}]\n           }\n  end\n\n  test \"linear_graphql marks atom-key GraphQL error responses as failures\" do\n    response =\n      DynamicTool.execute(\n        \"linear_graphql\",\n        %{\"query\" => \"query Viewer { viewer { id } }\"},\n        linear_client: fn _query, _variables, _opts ->\n          {:ok, %{errors: [%{message: \"boom\"}], data: nil}}\n        end\n      )\n\n    assert response[\"success\"] == false\n  end\n\n  test \"linear_graphql validates required arguments before calling Linear\" do\n    response =\n      DynamicTool.execute(\n        \"linear_graphql\",\n        %{\"variables\" => %{\"commentId\" => \"comment-1\"}},\n        linear_client: fn _query, _variables, _opts ->\n          flunk(\"linear client should not be called when arguments are invalid\")\n        end\n      )\n\n    assert response[\"success\"] == false\n\n    assert Jason.decode!(response[\"output\"]) == %{\n             \"error\" => %{\n               \"message\" => \"`linear_graphql` requires a non-empty `query` string.\"\n             }\n           }\n\n    blank_query =\n      DynamicTool.execute(\n        \"linear_graphql\",\n        %{\"query\" => \"   \"},\n        linear_client: fn _query, _variables, _opts ->\n          flunk(\"linear client should not be called when the query is blank\")\n        end\n      )\n\n    assert blank_query[\"success\"] == false\n  end\n\n  test \"linear_graphql rejects invalid argument types\" do\n    response =\n      DynamicTool.execute(\n        \"linear_graphql\",\n        [:not, :valid],\n        linear_client: fn _query, _variables, _opts ->\n          flunk(\"linear client should not be called when arguments are invalid\")\n        end\n      )\n\n    assert response[\"success\"] == false\n\n    assert Jason.decode!(response[\"output\"]) == %{\n             \"error\" => %{\n               \"message\" => \"`linear_graphql` expects either a GraphQL query string or an object with `query` and optional `variables`.\"\n             }\n           }\n  end\n\n  test \"linear_graphql rejects invalid variables\" do\n    response =\n      DynamicTool.execute(\n        \"linear_graphql\",\n        %{\"query\" => \"query Viewer { viewer { id } }\", \"variables\" => [\"bad\"]},\n        linear_client: fn _query, _variables, _opts ->\n          flunk(\"linear client should not be called when variables are invalid\")\n        end\n      )\n\n    assert response[\"success\"] == false\n\n    assert Jason.decode!(response[\"output\"]) == %{\n             \"error\" => %{\n               \"message\" => \"`linear_graphql.variables` must be a JSON object when provided.\"\n             }\n           }\n  end\n\n  test \"linear_graphql formats transport and auth failures\" do\n    missing_token =\n      DynamicTool.execute(\n        \"linear_graphql\",\n        %{\"query\" => \"query Viewer { viewer { id } }\"},\n        linear_client: fn _query, _variables, _opts -> {:error, :missing_linear_api_token} end\n      )\n\n    assert missing_token[\"success\"] == false\n\n    assert Jason.decode!(missing_token[\"output\"]) == %{\n             \"error\" => %{\n               \"message\" => \"Symphony is missing Linear auth. Set `linear.api_key` in `WORKFLOW.md` or export `LINEAR_API_KEY`.\"\n             }\n           }\n\n    status_error =\n      DynamicTool.execute(\n        \"linear_graphql\",\n        %{\"query\" => \"query Viewer { viewer { id } }\"},\n        linear_client: fn _query, _variables, _opts -> {:error, {:linear_api_status, 503}} end\n      )\n\n    assert Jason.decode!(status_error[\"output\"]) == %{\n             \"error\" => %{\n               \"message\" => \"Linear GraphQL request failed with HTTP 503.\",\n               \"status\" => 503\n             }\n           }\n\n    request_error =\n      DynamicTool.execute(\n        \"linear_graphql\",\n        %{\"query\" => \"query Viewer { viewer { id } }\"},\n        linear_client: fn _query, _variables, _opts -> {:error, {:linear_api_request, :timeout}} end\n      )\n\n    assert Jason.decode!(request_error[\"output\"]) == %{\n             \"error\" => %{\n               \"message\" => \"Linear GraphQL request failed before receiving a successful response.\",\n               \"reason\" => \":timeout\"\n             }\n           }\n  end\n\n  test \"linear_graphql formats unexpected failures from the client\" do\n    response =\n      DynamicTool.execute(\n        \"linear_graphql\",\n        %{\"query\" => \"query Viewer { viewer { id } }\"},\n        linear_client: fn _query, _variables, _opts -> {:error, :boom} end\n      )\n\n    assert response[\"success\"] == false\n\n    assert Jason.decode!(response[\"output\"]) == %{\n             \"error\" => %{\n               \"message\" => \"Linear GraphQL tool execution failed.\",\n               \"reason\" => \":boom\"\n             }\n           }\n  end\n\n  test \"linear_graphql falls back to inspect for non-JSON payloads\" do\n    response =\n      DynamicTool.execute(\n        \"linear_graphql\",\n        %{\"query\" => \"query Viewer { viewer { id } }\"},\n        linear_client: fn _query, _variables, _opts -> {:ok, :ok} end\n      )\n\n    assert response[\"success\"] == true\n    assert response[\"output\"] == \":ok\"\n  end\nend\n"
  },
  {
    "path": "elixir/test/symphony_elixir/extensions_test.exs",
    "content": "defmodule SymphonyElixir.ExtensionsTest do\n  use SymphonyElixir.TestSupport\n\n  import Phoenix.ConnTest\n  import Phoenix.LiveViewTest\n\n  alias SymphonyElixir.Linear.Adapter\n  alias SymphonyElixir.Tracker.Memory\n\n  @endpoint SymphonyElixirWeb.Endpoint\n\n  defmodule FakeLinearClient do\n    def fetch_candidate_issues do\n      send(self(), :fetch_candidate_issues_called)\n      {:ok, [:candidate]}\n    end\n\n    def fetch_issues_by_states(states) do\n      send(self(), {:fetch_issues_by_states_called, states})\n      {:ok, states}\n    end\n\n    def fetch_issue_states_by_ids(issue_ids) do\n      send(self(), {:fetch_issue_states_by_ids_called, issue_ids})\n      {:ok, issue_ids}\n    end\n\n    def graphql(query, variables) do\n      send(self(), {:graphql_called, query, variables})\n\n      case Process.get({__MODULE__, :graphql_results}) do\n        [result | rest] ->\n          Process.put({__MODULE__, :graphql_results}, rest)\n          result\n\n        _ ->\n          Process.get({__MODULE__, :graphql_result})\n      end\n    end\n  end\n\n  defmodule SlowOrchestrator do\n    use GenServer\n\n    def start_link(opts) do\n      GenServer.start_link(__MODULE__, :ok, opts)\n    end\n\n    def init(:ok), do: {:ok, :ok}\n\n    def handle_call(:snapshot, _from, state) do\n      Process.sleep(25)\n      {:reply, %{}, state}\n    end\n\n    def handle_call(:request_refresh, _from, state) do\n      {:reply, :unavailable, state}\n    end\n  end\n\n  defmodule StaticOrchestrator do\n    use GenServer\n\n    def start_link(opts) do\n      name = Keyword.fetch!(opts, :name)\n      GenServer.start_link(__MODULE__, opts, name: name)\n    end\n\n    def init(opts), do: {:ok, opts}\n\n    def handle_call(:snapshot, _from, state) do\n      {:reply, Keyword.fetch!(state, :snapshot), state}\n    end\n\n    def handle_call(:request_refresh, _from, state) do\n      {:reply, Keyword.get(state, :refresh, :unavailable), state}\n    end\n  end\n\n  setup do\n    linear_client_module = Application.get_env(:symphony_elixir, :linear_client_module)\n\n    on_exit(fn ->\n      if is_nil(linear_client_module) do\n        Application.delete_env(:symphony_elixir, :linear_client_module)\n      else\n        Application.put_env(:symphony_elixir, :linear_client_module, linear_client_module)\n      end\n    end)\n\n    :ok\n  end\n\n  setup do\n    endpoint_config = Application.get_env(:symphony_elixir, SymphonyElixirWeb.Endpoint, [])\n\n    on_exit(fn ->\n      Application.put_env(:symphony_elixir, SymphonyElixirWeb.Endpoint, endpoint_config)\n    end)\n\n    :ok\n  end\n\n  test \"workflow store reloads changes, keeps last good workflow, and falls back when stopped\" do\n    ensure_workflow_store_running()\n    assert {:ok, %{prompt: \"You are an agent for this repository.\"}} = Workflow.current()\n\n    write_workflow_file!(Workflow.workflow_file_path(), prompt: \"Second prompt\")\n    send(WorkflowStore, :poll)\n\n    assert_eventually(fn ->\n      match?({:ok, %{prompt: \"Second prompt\"}}, Workflow.current())\n    end)\n\n    File.write!(Workflow.workflow_file_path(), \"---\\ntracker: [\\n---\\nBroken prompt\\n\")\n    assert {:error, _reason} = WorkflowStore.force_reload()\n    assert {:ok, %{prompt: \"Second prompt\"}} = Workflow.current()\n\n    third_workflow = Path.join(Path.dirname(Workflow.workflow_file_path()), \"THIRD_WORKFLOW.md\")\n    write_workflow_file!(third_workflow, prompt: \"Third prompt\")\n    Workflow.set_workflow_file_path(third_workflow)\n    assert {:ok, %{prompt: \"Third prompt\"}} = Workflow.current()\n\n    assert :ok = Supervisor.terminate_child(SymphonyElixir.Supervisor, WorkflowStore)\n    assert {:ok, %{prompt: \"Third prompt\"}} = WorkflowStore.current()\n    assert :ok = WorkflowStore.force_reload()\n    assert {:ok, _pid} = Supervisor.restart_child(SymphonyElixir.Supervisor, WorkflowStore)\n  end\n\n  test \"workflow store init stops on missing workflow file\" do\n    missing_path = Path.join(Path.dirname(Workflow.workflow_file_path()), \"MISSING_WORKFLOW.md\")\n    Workflow.set_workflow_file_path(missing_path)\n\n    assert {:stop, {:missing_workflow_file, ^missing_path, :enoent}} = WorkflowStore.init([])\n  end\n\n  test \"workflow store start_link and poll callback cover missing-file error paths\" do\n    ensure_workflow_store_running()\n    existing_path = Workflow.workflow_file_path()\n    manual_path = Path.join(Path.dirname(existing_path), \"MANUAL_WORKFLOW.md\")\n    missing_path = Path.join(Path.dirname(existing_path), \"MANUAL_MISSING_WORKFLOW.md\")\n\n    assert :ok = Supervisor.terminate_child(SymphonyElixir.Supervisor, WorkflowStore)\n\n    Workflow.set_workflow_file_path(missing_path)\n\n    assert {:error, {:missing_workflow_file, ^missing_path, :enoent}} =\n             WorkflowStore.force_reload()\n\n    write_workflow_file!(manual_path, prompt: \"Manual workflow prompt\")\n    Workflow.set_workflow_file_path(manual_path)\n\n    assert {:ok, manual_pid} = WorkflowStore.start_link()\n    assert Process.alive?(manual_pid)\n\n    state = :sys.get_state(manual_pid)\n    File.write!(manual_path, \"---\\ntracker: [\\n---\\nBroken prompt\\n\")\n    assert {:noreply, returned_state} = WorkflowStore.handle_info(:poll, state)\n    assert returned_state.workflow.prompt == \"Manual workflow prompt\"\n    refute returned_state.stamp == nil\n    assert_receive :poll, 1_100\n\n    Workflow.set_workflow_file_path(missing_path)\n    assert {:noreply, path_error_state} = WorkflowStore.handle_info(:poll, returned_state)\n    assert path_error_state.workflow.prompt == \"Manual workflow prompt\"\n    assert_receive :poll, 1_100\n\n    Workflow.set_workflow_file_path(manual_path)\n    File.rm!(manual_path)\n    assert {:noreply, removed_state} = WorkflowStore.handle_info(:poll, path_error_state)\n    assert removed_state.workflow.prompt == \"Manual workflow prompt\"\n    assert_receive :poll, 1_100\n\n    Process.exit(manual_pid, :normal)\n    restart_result = Supervisor.restart_child(SymphonyElixir.Supervisor, WorkflowStore)\n\n    assert match?({:ok, _pid}, restart_result) or\n             match?({:error, {:already_started, _pid}}, restart_result)\n\n    Workflow.set_workflow_file_path(existing_path)\n    WorkflowStore.force_reload()\n  end\n\n  test \"tracker delegates to memory and linear adapters\" do\n    issue = %Issue{id: \"issue-1\", identifier: \"MT-1\", state: \"In Progress\"}\n    Application.put_env(:symphony_elixir, :memory_tracker_issues, [issue, %{id: \"ignored\"}])\n    Application.put_env(:symphony_elixir, :memory_tracker_recipient, self())\n    write_workflow_file!(Workflow.workflow_file_path(), tracker_kind: \"memory\")\n\n    assert Config.settings!().tracker.kind == \"memory\"\n    assert SymphonyElixir.Tracker.adapter() == Memory\n    assert {:ok, [^issue]} = SymphonyElixir.Tracker.fetch_candidate_issues()\n    assert {:ok, [^issue]} = SymphonyElixir.Tracker.fetch_issues_by_states([\" in progress \", 42])\n    assert {:ok, [^issue]} = SymphonyElixir.Tracker.fetch_issue_states_by_ids([\"issue-1\"])\n    assert :ok = SymphonyElixir.Tracker.create_comment(\"issue-1\", \"comment\")\n    assert :ok = SymphonyElixir.Tracker.update_issue_state(\"issue-1\", \"Done\")\n    assert_receive {:memory_tracker_comment, \"issue-1\", \"comment\"}\n    assert_receive {:memory_tracker_state_update, \"issue-1\", \"Done\"}\n\n    Application.delete_env(:symphony_elixir, :memory_tracker_recipient)\n    assert :ok = Memory.create_comment(\"issue-1\", \"quiet\")\n    assert :ok = Memory.update_issue_state(\"issue-1\", \"Quiet\")\n\n    write_workflow_file!(Workflow.workflow_file_path(), tracker_kind: \"linear\")\n    assert SymphonyElixir.Tracker.adapter() == Adapter\n  end\n\n  test \"linear adapter delegates reads and validates mutation responses\" do\n    Application.put_env(:symphony_elixir, :linear_client_module, FakeLinearClient)\n\n    assert {:ok, [:candidate]} = Adapter.fetch_candidate_issues()\n    assert_receive :fetch_candidate_issues_called\n\n    assert {:ok, [\"Todo\"]} = Adapter.fetch_issues_by_states([\"Todo\"])\n    assert_receive {:fetch_issues_by_states_called, [\"Todo\"]}\n\n    assert {:ok, [\"issue-1\"]} = Adapter.fetch_issue_states_by_ids([\"issue-1\"])\n    assert_receive {:fetch_issue_states_by_ids_called, [\"issue-1\"]}\n\n    Process.put(\n      {FakeLinearClient, :graphql_result},\n      {:ok, %{\"data\" => %{\"commentCreate\" => %{\"success\" => true}}}}\n    )\n\n    assert :ok = Adapter.create_comment(\"issue-1\", \"hello\")\n    assert_receive {:graphql_called, create_comment_query, %{body: \"hello\", issueId: \"issue-1\"}}\n    assert create_comment_query =~ \"commentCreate\"\n\n    Process.put(\n      {FakeLinearClient, :graphql_result},\n      {:ok, %{\"data\" => %{\"commentCreate\" => %{\"success\" => false}}}}\n    )\n\n    assert {:error, :comment_create_failed} =\n             Adapter.create_comment(\"issue-1\", \"broken\")\n\n    Process.put({FakeLinearClient, :graphql_result}, {:error, :boom})\n\n    assert {:error, :boom} = Adapter.create_comment(\"issue-1\", \"boom\")\n\n    Process.put({FakeLinearClient, :graphql_result}, {:ok, %{\"data\" => %{}}})\n    assert {:error, :comment_create_failed} = Adapter.create_comment(\"issue-1\", \"weird\")\n\n    Process.put({FakeLinearClient, :graphql_result}, :unexpected)\n    assert {:error, :comment_create_failed} = Adapter.create_comment(\"issue-1\", \"odd\")\n\n    Process.put(\n      {FakeLinearClient, :graphql_results},\n      [\n        {:ok,\n         %{\n           \"data\" => %{\n             \"issue\" => %{\"team\" => %{\"states\" => %{\"nodes\" => [%{\"id\" => \"state-1\"}]}}}\n           }\n         }},\n        {:ok, %{\"data\" => %{\"issueUpdate\" => %{\"success\" => true}}}}\n      ]\n    )\n\n    assert :ok = Adapter.update_issue_state(\"issue-1\", \"Done\")\n    assert_receive {:graphql_called, state_lookup_query, %{issueId: \"issue-1\", stateName: \"Done\"}}\n    assert state_lookup_query =~ \"states\"\n\n    assert_receive {:graphql_called, update_issue_query, %{issueId: \"issue-1\", stateId: \"state-1\"}}\n\n    assert update_issue_query =~ \"issueUpdate\"\n\n    Process.put(\n      {FakeLinearClient, :graphql_results},\n      [\n        {:ok,\n         %{\n           \"data\" => %{\n             \"issue\" => %{\"team\" => %{\"states\" => %{\"nodes\" => [%{\"id\" => \"state-1\"}]}}}\n           }\n         }},\n        {:ok, %{\"data\" => %{\"issueUpdate\" => %{\"success\" => false}}}}\n      ]\n    )\n\n    assert {:error, :issue_update_failed} =\n             Adapter.update_issue_state(\"issue-1\", \"Broken\")\n\n    Process.put({FakeLinearClient, :graphql_results}, [{:error, :boom}])\n\n    assert {:error, :boom} = Adapter.update_issue_state(\"issue-1\", \"Boom\")\n\n    Process.put({FakeLinearClient, :graphql_results}, [{:ok, %{\"data\" => %{}}}])\n    assert {:error, :state_not_found} = Adapter.update_issue_state(\"issue-1\", \"Missing\")\n\n    Process.put(\n      {FakeLinearClient, :graphql_results},\n      [\n        {:ok,\n         %{\n           \"data\" => %{\n             \"issue\" => %{\"team\" => %{\"states\" => %{\"nodes\" => [%{\"id\" => \"state-1\"}]}}}\n           }\n         }},\n        {:ok, %{\"data\" => %{}}}\n      ]\n    )\n\n    assert {:error, :issue_update_failed} = Adapter.update_issue_state(\"issue-1\", \"Weird\")\n\n    Process.put(\n      {FakeLinearClient, :graphql_results},\n      [\n        {:ok,\n         %{\n           \"data\" => %{\n             \"issue\" => %{\"team\" => %{\"states\" => %{\"nodes\" => [%{\"id\" => \"state-1\"}]}}}\n           }\n         }},\n        :unexpected\n      ]\n    )\n\n    assert {:error, :issue_update_failed} = Adapter.update_issue_state(\"issue-1\", \"Odd\")\n  end\n\n  test \"phoenix observability api preserves state, issue, and refresh responses\" do\n    snapshot = static_snapshot()\n    orchestrator_name = Module.concat(__MODULE__, :ObservabilityApiOrchestrator)\n\n    {:ok, _pid} =\n      StaticOrchestrator.start_link(\n        name: orchestrator_name,\n        snapshot: snapshot,\n        refresh: %{\n          queued: true,\n          coalesced: false,\n          requested_at: DateTime.utc_now(),\n          operations: [\"poll\", \"reconcile\"]\n        }\n      )\n\n    start_test_endpoint(orchestrator: orchestrator_name, snapshot_timeout_ms: 50)\n\n    conn = get(build_conn(), \"/api/v1/state\")\n    state_payload = json_response(conn, 200)\n\n    assert state_payload == %{\n             \"generated_at\" => state_payload[\"generated_at\"],\n             \"counts\" => %{\"running\" => 1, \"retrying\" => 1},\n             \"running\" => [\n               %{\n                 \"issue_id\" => \"issue-http\",\n                 \"issue_identifier\" => \"MT-HTTP\",\n                 \"state\" => \"In Progress\",\n                 \"worker_host\" => nil,\n                 \"workspace_path\" => nil,\n                 \"session_id\" => \"thread-http\",\n                 \"turn_count\" => 7,\n                 \"last_event\" => \"notification\",\n                 \"last_message\" => \"rendered\",\n                 \"started_at\" => state_payload[\"running\"] |> List.first() |> Map.fetch!(\"started_at\"),\n                 \"last_event_at\" => nil,\n                 \"tokens\" => %{\"input_tokens\" => 4, \"output_tokens\" => 8, \"total_tokens\" => 12}\n               }\n             ],\n             \"retrying\" => [\n               %{\n                 \"issue_id\" => \"issue-retry\",\n                 \"issue_identifier\" => \"MT-RETRY\",\n                 \"attempt\" => 2,\n                 \"due_at\" => state_payload[\"retrying\"] |> List.first() |> Map.fetch!(\"due_at\"),\n                 \"error\" => \"boom\",\n                 \"worker_host\" => nil,\n                 \"workspace_path\" => nil\n               }\n             ],\n             \"codex_totals\" => %{\n               \"input_tokens\" => 4,\n               \"output_tokens\" => 8,\n               \"total_tokens\" => 12,\n               \"seconds_running\" => 42.5\n             },\n             \"rate_limits\" => %{\"primary\" => %{\"remaining\" => 11}}\n           }\n\n    conn = get(build_conn(), \"/api/v1/MT-HTTP\")\n    issue_payload = json_response(conn, 200)\n\n    assert issue_payload == %{\n             \"issue_identifier\" => \"MT-HTTP\",\n             \"issue_id\" => \"issue-http\",\n             \"status\" => \"running\",\n             \"workspace\" => %{\n               \"path\" => Path.join(Config.settings!().workspace.root, \"MT-HTTP\"),\n               \"host\" => nil\n             },\n             \"attempts\" => %{\"restart_count\" => 0, \"current_retry_attempt\" => 0},\n             \"running\" => %{\n               \"worker_host\" => nil,\n               \"workspace_path\" => nil,\n               \"session_id\" => \"thread-http\",\n               \"turn_count\" => 7,\n               \"state\" => \"In Progress\",\n               \"started_at\" => issue_payload[\"running\"][\"started_at\"],\n               \"last_event\" => \"notification\",\n               \"last_message\" => \"rendered\",\n               \"last_event_at\" => nil,\n               \"tokens\" => %{\"input_tokens\" => 4, \"output_tokens\" => 8, \"total_tokens\" => 12}\n             },\n             \"retry\" => nil,\n             \"logs\" => %{\"codex_session_logs\" => []},\n             \"recent_events\" => [],\n             \"last_error\" => nil,\n             \"tracked\" => %{}\n           }\n\n    conn = get(build_conn(), \"/api/v1/MT-RETRY\")\n\n    assert %{\"status\" => \"retrying\", \"retry\" => %{\"attempt\" => 2, \"error\" => \"boom\"}} =\n             json_response(conn, 200)\n\n    conn = get(build_conn(), \"/api/v1/MT-MISSING\")\n\n    assert json_response(conn, 404) == %{\n             \"error\" => %{\"code\" => \"issue_not_found\", \"message\" => \"Issue not found\"}\n           }\n\n    conn = post(build_conn(), \"/api/v1/refresh\", %{})\n\n    assert %{\"queued\" => true, \"coalesced\" => false, \"operations\" => [\"poll\", \"reconcile\"]} =\n             json_response(conn, 202)\n  end\n\n  test \"phoenix observability api preserves 405, 404, and unavailable behavior\" do\n    unavailable_orchestrator = Module.concat(__MODULE__, :UnavailableOrchestrator)\n    start_test_endpoint(orchestrator: unavailable_orchestrator, snapshot_timeout_ms: 5)\n\n    assert json_response(post(build_conn(), \"/api/v1/state\", %{}), 405) ==\n             %{\"error\" => %{\"code\" => \"method_not_allowed\", \"message\" => \"Method not allowed\"}}\n\n    assert json_response(get(build_conn(), \"/api/v1/refresh\"), 405) ==\n             %{\"error\" => %{\"code\" => \"method_not_allowed\", \"message\" => \"Method not allowed\"}}\n\n    assert json_response(post(build_conn(), \"/\", %{}), 405) ==\n             %{\"error\" => %{\"code\" => \"method_not_allowed\", \"message\" => \"Method not allowed\"}}\n\n    assert json_response(post(build_conn(), \"/api/v1/MT-1\", %{}), 405) ==\n             %{\"error\" => %{\"code\" => \"method_not_allowed\", \"message\" => \"Method not allowed\"}}\n\n    assert json_response(get(build_conn(), \"/unknown\"), 404) ==\n             %{\"error\" => %{\"code\" => \"not_found\", \"message\" => \"Route not found\"}}\n\n    state_payload = json_response(get(build_conn(), \"/api/v1/state\"), 200)\n\n    assert state_payload ==\n             %{\n               \"generated_at\" => state_payload[\"generated_at\"],\n               \"error\" => %{\"code\" => \"snapshot_unavailable\", \"message\" => \"Snapshot unavailable\"}\n             }\n\n    assert json_response(post(build_conn(), \"/api/v1/refresh\", %{}), 503) ==\n             %{\n               \"error\" => %{\n                 \"code\" => \"orchestrator_unavailable\",\n                 \"message\" => \"Orchestrator is unavailable\"\n               }\n             }\n  end\n\n  test \"phoenix observability api preserves snapshot timeout behavior\" do\n    timeout_orchestrator = Module.concat(__MODULE__, :TimeoutOrchestrator)\n    {:ok, _pid} = SlowOrchestrator.start_link(name: timeout_orchestrator)\n    start_test_endpoint(orchestrator: timeout_orchestrator, snapshot_timeout_ms: 1)\n\n    timeout_payload = json_response(get(build_conn(), \"/api/v1/state\"), 200)\n\n    assert timeout_payload ==\n             %{\n               \"generated_at\" => timeout_payload[\"generated_at\"],\n               \"error\" => %{\"code\" => \"snapshot_timeout\", \"message\" => \"Snapshot timed out\"}\n             }\n  end\n\n  test \"dashboard bootstraps liveview from embedded static assets\" do\n    orchestrator_name = Module.concat(__MODULE__, :AssetOrchestrator)\n\n    {:ok, _pid} =\n      StaticOrchestrator.start_link(\n        name: orchestrator_name,\n        snapshot: static_snapshot(),\n        refresh: %{\n          queued: true,\n          coalesced: false,\n          requested_at: DateTime.utc_now(),\n          operations: [\"poll\"]\n        }\n      )\n\n    start_test_endpoint(orchestrator: orchestrator_name, snapshot_timeout_ms: 50)\n\n    html = html_response(get(build_conn(), \"/\"), 200)\n    assert html =~ \"/dashboard.css\"\n    assert html =~ \"/vendor/phoenix_html/phoenix_html.js\"\n    assert html =~ \"/vendor/phoenix/phoenix.js\"\n    assert html =~ \"/vendor/phoenix_live_view/phoenix_live_view.js\"\n    refute html =~ \"/assets/app.js\"\n    refute html =~ \"<style>\"\n\n    dashboard_css = response(get(build_conn(), \"/dashboard.css\"), 200)\n    assert dashboard_css =~ \":root {\"\n    assert dashboard_css =~ \".status-badge-live\"\n    assert dashboard_css =~ \"[data-phx-main].phx-connected .status-badge-live\"\n    assert dashboard_css =~ \"[data-phx-main].phx-connected .status-badge-offline\"\n\n    phoenix_html_js = response(get(build_conn(), \"/vendor/phoenix_html/phoenix_html.js\"), 200)\n    assert phoenix_html_js =~ \"phoenix.link.click\"\n\n    phoenix_js = response(get(build_conn(), \"/vendor/phoenix/phoenix.js\"), 200)\n    assert phoenix_js =~ \"var Phoenix = (() => {\"\n\n    live_view_js =\n      response(get(build_conn(), \"/vendor/phoenix_live_view/phoenix_live_view.js\"), 200)\n\n    assert live_view_js =~ \"var LiveView = (() => {\"\n  end\n\n  test \"dashboard liveview renders and refreshes over pubsub\" do\n    orchestrator_name = Module.concat(__MODULE__, :DashboardOrchestrator)\n    snapshot = static_snapshot()\n\n    {:ok, orchestrator_pid} =\n      StaticOrchestrator.start_link(\n        name: orchestrator_name,\n        snapshot: snapshot,\n        refresh: %{\n          queued: true,\n          coalesced: true,\n          requested_at: DateTime.utc_now(),\n          operations: [\"poll\"]\n        }\n      )\n\n    start_test_endpoint(orchestrator: orchestrator_name, snapshot_timeout_ms: 50)\n\n    {:ok, view, html} = live(build_conn(), \"/\")\n    assert html =~ \"Operations Dashboard\"\n    assert html =~ \"MT-HTTP\"\n    assert html =~ \"MT-RETRY\"\n    assert html =~ \"rendered\"\n    assert html =~ \"Runtime\"\n    assert html =~ \"Live\"\n    assert html =~ \"Offline\"\n    assert html =~ \"Copy ID\"\n    assert html =~ \"Codex update\"\n    refute html =~ \"data-runtime-clock=\"\n    refute html =~ \"setInterval(refreshRuntimeClocks\"\n    refute html =~ \"Refresh now\"\n    refute html =~ \"Transport\"\n    assert html =~ \"status-badge-live\"\n    assert html =~ \"status-badge-offline\"\n\n    updated_snapshot =\n      put_in(snapshot.running, [\n        %{\n          issue_id: \"issue-http\",\n          identifier: \"MT-HTTP\",\n          state: \"In Progress\",\n          session_id: \"thread-http\",\n          turn_count: 8,\n          last_codex_event: :notification,\n          last_codex_message: %{\n            event: :notification,\n            message: %{\n              payload: %{\n                \"method\" => \"codex/event/agent_message_content_delta\",\n                \"params\" => %{\n                  \"msg\" => %{\n                    \"content\" => \"structured update\"\n                  }\n                }\n              }\n            }\n          },\n          last_codex_timestamp: DateTime.utc_now(),\n          codex_input_tokens: 10,\n          codex_output_tokens: 12,\n          codex_total_tokens: 22,\n          started_at: DateTime.utc_now()\n        }\n      ])\n\n    :sys.replace_state(orchestrator_pid, fn state ->\n      Keyword.put(state, :snapshot, updated_snapshot)\n    end)\n\n    StatusDashboard.notify_update()\n\n    assert_eventually(fn ->\n      render(view) =~ \"agent message content streaming: structured update\"\n    end)\n  end\n\n  test \"dashboard liveview renders an unavailable state without crashing\" do\n    start_test_endpoint(\n      orchestrator: Module.concat(__MODULE__, :MissingDashboardOrchestrator),\n      snapshot_timeout_ms: 5\n    )\n\n    {:ok, _view, html} = live(build_conn(), \"/\")\n    assert html =~ \"Snapshot unavailable\"\n    assert html =~ \"snapshot_unavailable\"\n  end\n\n  test \"http server serves embedded assets, accepts form posts, and rejects invalid hosts\" do\n    spec = HttpServer.child_spec(port: 0)\n    assert spec.id == HttpServer\n    assert spec.start == {HttpServer, :start_link, [[port: 0]]}\n\n    assert :ignore = HttpServer.start_link(port: nil)\n    assert HttpServer.bound_port() == nil\n\n    snapshot = static_snapshot()\n    orchestrator_name = Module.concat(__MODULE__, :BoundPortOrchestrator)\n\n    refresh = %{\n      queued: true,\n      coalesced: false,\n      requested_at: DateTime.utc_now(),\n      operations: [\"poll\"]\n    }\n\n    server_opts = [\n      host: \"127.0.0.1\",\n      port: 0,\n      orchestrator: orchestrator_name,\n      snapshot_timeout_ms: 50\n    ]\n\n    start_supervised!({StaticOrchestrator, name: orchestrator_name, snapshot: snapshot, refresh: refresh})\n\n    start_supervised!({HttpServer, server_opts})\n\n    port = wait_for_bound_port()\n    assert port == HttpServer.bound_port()\n\n    response = Req.get!(\"http://127.0.0.1:#{port}/api/v1/state\")\n    assert response.status == 200\n    assert response.body[\"counts\"] == %{\"running\" => 1, \"retrying\" => 1}\n\n    dashboard_css = Req.get!(\"http://127.0.0.1:#{port}/dashboard.css\")\n    assert dashboard_css.status == 200\n    assert dashboard_css.body =~ \":root {\"\n\n    phoenix_js = Req.get!(\"http://127.0.0.1:#{port}/vendor/phoenix/phoenix.js\")\n    assert phoenix_js.status == 200\n    assert phoenix_js.body =~ \"var Phoenix = (() => {\"\n\n    refresh_response =\n      Req.post!(\"http://127.0.0.1:#{port}/api/v1/refresh\",\n        headers: [{\"content-type\", \"application/x-www-form-urlencoded\"}],\n        body: \"\"\n      )\n\n    assert refresh_response.status == 202\n    assert refresh_response.body[\"queued\"] == true\n\n    method_not_allowed_response =\n      Req.post!(\"http://127.0.0.1:#{port}/api/v1/state\",\n        headers: [{\"content-type\", \"application/x-www-form-urlencoded\"}],\n        body: \"\"\n      )\n\n    assert method_not_allowed_response.status == 405\n    assert method_not_allowed_response.body[\"error\"][\"code\"] == \"method_not_allowed\"\n\n    assert {:error, _reason} = HttpServer.start_link(host: \"bad host\", port: 0)\n  end\n\n  defp start_test_endpoint(overrides) do\n    endpoint_config =\n      :symphony_elixir\n      |> Application.get_env(SymphonyElixirWeb.Endpoint, [])\n      |> Keyword.merge(server: false, secret_key_base: String.duplicate(\"s\", 64))\n      |> Keyword.merge(overrides)\n\n    Application.put_env(:symphony_elixir, SymphonyElixirWeb.Endpoint, endpoint_config)\n    start_supervised!({SymphonyElixirWeb.Endpoint, []})\n  end\n\n  defp static_snapshot do\n    %{\n      running: [\n        %{\n          issue_id: \"issue-http\",\n          identifier: \"MT-HTTP\",\n          state: \"In Progress\",\n          session_id: \"thread-http\",\n          turn_count: 7,\n          codex_app_server_pid: nil,\n          last_codex_message: \"rendered\",\n          last_codex_timestamp: nil,\n          last_codex_event: :notification,\n          codex_input_tokens: 4,\n          codex_output_tokens: 8,\n          codex_total_tokens: 12,\n          started_at: DateTime.utc_now()\n        }\n      ],\n      retrying: [\n        %{\n          issue_id: \"issue-retry\",\n          identifier: \"MT-RETRY\",\n          attempt: 2,\n          due_in_ms: 2_000,\n          error: \"boom\"\n        }\n      ],\n      codex_totals: %{input_tokens: 4, output_tokens: 8, total_tokens: 12, seconds_running: 42.5},\n      rate_limits: %{\"primary\" => %{\"remaining\" => 11}}\n    }\n  end\n\n  defp wait_for_bound_port do\n    assert_eventually(fn ->\n      is_integer(HttpServer.bound_port())\n    end)\n\n    HttpServer.bound_port()\n  end\n\n  defp assert_eventually(fun, attempts \\\\ 20)\n\n  defp assert_eventually(fun, attempts) when attempts > 0 do\n    if fun.() do\n      true\n    else\n      Process.sleep(25)\n      assert_eventually(fun, attempts - 1)\n    end\n  end\n\n  defp assert_eventually(_fun, 0), do: flunk(\"condition not met in time\")\n\n  defp ensure_workflow_store_running do\n    if Process.whereis(WorkflowStore) do\n      :ok\n    else\n      case Supervisor.restart_child(SymphonyElixir.Supervisor, WorkflowStore) do\n        {:ok, _pid} -> :ok\n        {:error, {:already_started, _pid}} -> :ok\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "elixir/test/symphony_elixir/live_e2e_test.exs",
    "content": "defmodule SymphonyElixir.LiveE2ETest do\n  use SymphonyElixir.TestSupport\n\n  require Logger\n  alias SymphonyElixir.SSH\n\n  @moduletag :live_e2e\n  @moduletag timeout: 300_000\n\n  @default_team_key \"SYME2E\"\n  @default_docker_auth_json Path.join(System.user_home!(), \".codex/auth.json\")\n  @docker_worker_count 2\n  @docker_support_dir Path.expand(\"../support/live_e2e_docker\", __DIR__)\n  @docker_compose_file Path.join(@docker_support_dir, \"docker-compose.yml\")\n  @result_file \"LIVE_E2E_RESULT.txt\"\n  @live_e2e_skip_reason if(System.get_env(\"SYMPHONY_RUN_LIVE_E2E\") != \"1\",\n                          do: \"set SYMPHONY_RUN_LIVE_E2E=1 to enable the real Linear/Codex end-to-end test\"\n                        )\n\n  @team_query \"\"\"\n  query SymphonyLiveE2ETeam($key: String!) {\n    teams(filter: {key: {eq: $key}}, first: 1) {\n      nodes {\n        id\n        key\n        name\n        states(first: 50) {\n          nodes {\n            id\n            name\n            type\n          }\n        }\n      }\n    }\n  }\n  \"\"\"\n\n  @create_project_mutation \"\"\"\n  mutation SymphonyLiveE2ECreateProject($name: String!, $teamIds: [String!]!) {\n    projectCreate(input: {name: $name, teamIds: $teamIds}) {\n      success\n      project {\n        id\n        name\n        slugId\n        url\n      }\n    }\n  }\n  \"\"\"\n\n  @create_issue_mutation \"\"\"\n  mutation SymphonyLiveE2ECreateIssue(\n    $teamId: String!\n    $projectId: String!\n    $title: String!\n    $description: String!\n    $stateId: String\n  ) {\n    issueCreate(\n      input: {\n        teamId: $teamId\n        projectId: $projectId\n        title: $title\n        description: $description\n        stateId: $stateId\n      }\n    ) {\n      success\n      issue {\n        id\n        identifier\n        title\n        description\n        url\n        state {\n          name\n        }\n      }\n    }\n  }\n  \"\"\"\n\n  @project_statuses_query \"\"\"\n  query SymphonyLiveE2EProjectStatuses {\n    projectStatuses(first: 50) {\n      nodes {\n        id\n        name\n        type\n      }\n    }\n  }\n  \"\"\"\n\n  @issue_details_query \"\"\"\n  query SymphonyLiveE2EIssueDetails($id: String!) {\n    issue(id: $id) {\n      id\n      identifier\n      state {\n        name\n        type\n      }\n      comments(first: 20) {\n        nodes {\n          body\n        }\n      }\n    }\n  }\n  \"\"\"\n\n  @complete_project_mutation \"\"\"\n  mutation SymphonyLiveE2ECompleteProject($id: String!, $statusId: String!, $completedAt: DateTime!) {\n    projectUpdate(id: $id, input: {statusId: $statusId, completedAt: $completedAt}) {\n      success\n    }\n  }\n  \"\"\"\n\n  @tag skip: @live_e2e_skip_reason\n  test \"creates a real Linear project and issue with a local worker\" do\n    run_live_issue_flow!(:local)\n  end\n\n  @tag skip: @live_e2e_skip_reason\n  test \"creates a real Linear project and issue with an ssh worker\" do\n    run_live_issue_flow!(:ssh)\n  end\n\n  defp fetch_team!(team_key) do\n    @team_query\n    |> graphql_data!(%{key: team_key})\n    |> get_in([\"teams\", \"nodes\"])\n    |> case do\n      [team | _] ->\n        team\n\n      _ ->\n        flunk(\"expected Linear team #{inspect(team_key)} to exist\")\n    end\n  end\n\n  defp active_state!(%{\"states\" => %{\"nodes\" => states}}) when is_list(states) do\n    Enum.find(states, &(&1[\"type\"] == \"started\")) ||\n      Enum.find(states, &(&1[\"type\"] == \"unstarted\")) ||\n      Enum.find(states, &(&1[\"type\"] not in [\"completed\", \"canceled\"])) ||\n      flunk(\"expected team to expose at least one non-terminal workflow state\")\n  end\n\n  defp terminal_state_names(%{\"states\" => %{\"nodes\" => states}}) when is_list(states) do\n    states\n    |> Enum.filter(&(&1[\"type\"] in [\"completed\", \"canceled\"]))\n    |> Enum.map(& &1[\"name\"])\n    |> case do\n      [] -> [\"Done\", \"Canceled\", \"Cancelled\"]\n      names -> names\n    end\n  end\n\n  defp active_state_names(%{\"states\" => %{\"nodes\" => states}}) when is_list(states) do\n    states\n    |> Enum.reject(&(&1[\"type\"] in [\"completed\", \"canceled\"]))\n    |> Enum.map(& &1[\"name\"])\n    |> case do\n      [] -> [\"Todo\", \"In Progress\", \"In Review\"]\n      names -> names\n    end\n  end\n\n  defp completed_project_status! do\n    @project_statuses_query\n    |> graphql_data!(%{})\n    |> get_in([\"projectStatuses\", \"nodes\"])\n    |> case do\n      statuses when is_list(statuses) ->\n        Enum.find(statuses, &(&1[\"type\"] == \"completed\")) ||\n          flunk(\"expected workspace to expose a completed project status\")\n\n      payload ->\n        flunk(\"expected project statuses list, got: #{inspect(payload)}\")\n    end\n  end\n\n  defp create_project!(team_id, name) do\n    @create_project_mutation\n    |> graphql_data!(%{teamIds: [team_id], name: name})\n    |> fetch_successful_entity!(\"projectCreate\", \"project\")\n  end\n\n  defp create_issue!(team_id, project_id, state_id, title) do\n    issue =\n      @create_issue_mutation\n      |> graphql_data!(%{\n        teamId: team_id,\n        projectId: project_id,\n        title: title,\n        description: title,\n        stateId: state_id\n      })\n      |> fetch_successful_entity!(\"issueCreate\", \"issue\")\n\n    %Issue{\n      id: issue[\"id\"],\n      identifier: issue[\"identifier\"],\n      title: issue[\"title\"],\n      description: issue[\"description\"],\n      state: get_in(issue, [\"state\", \"name\"]),\n      url: issue[\"url\"],\n      labels: [],\n      blocked_by: []\n    }\n  end\n\n  defp complete_project(project_id, completed_status_id)\n       when is_binary(project_id) and is_binary(completed_status_id) do\n    update_entity(\n      @complete_project_mutation,\n      %{\n        id: project_id,\n        statusId: completed_status_id,\n        completedAt: DateTime.utc_now() |> DateTime.truncate(:second) |> DateTime.to_iso8601()\n      },\n      \"projectUpdate\",\n      \"project\"\n    )\n  end\n\n  defp fetch_issue_details!(issue_id) when is_binary(issue_id) do\n    @issue_details_query\n    |> graphql_data!(%{id: issue_id})\n    |> get_in([\"issue\"])\n    |> case do\n      %{} = issue -> issue\n      payload -> flunk(\"expected issue details payload, got: #{inspect(payload)}\")\n    end\n  end\n\n  defp issue_completed?(%{\"state\" => %{\"type\" => type}}), do: type in [\"completed\", \"canceled\"]\n  defp issue_completed?(_issue), do: false\n\n  defp issue_has_comment?(%{\"comments\" => %{\"nodes\" => comments}}, expected_body) when is_list(comments) do\n    Enum.any?(comments, &(&1[\"body\"] == expected_body))\n  end\n\n  defp issue_has_comment?(_issue, _expected_body), do: false\n\n  defp update_entity(mutation, variables, mutation_name, entity_name) do\n    case Client.graphql(mutation, variables) do\n      {:ok, %{\"data\" => %{^mutation_name => %{\"success\" => true}}}} ->\n        :ok\n\n      {:ok, %{\"errors\" => errors}} ->\n        Logger.warning(\"Live e2e finalization failed for #{entity_name}: #{inspect(errors)}\")\n        :ok\n\n      {:ok, payload} ->\n        Logger.warning(\"Live e2e finalization failed for #{entity_name}: #{inspect(payload)}\")\n        :ok\n\n      {:error, reason} ->\n        Logger.warning(\"Live e2e finalization failed for #{entity_name}: #{inspect(reason)}\")\n        :ok\n    end\n  end\n\n  defp graphql_data!(query, variables) when is_binary(query) and is_map(variables) do\n    case Client.graphql(query, variables) do\n      {:ok, %{\"data\" => data, \"errors\" => errors}} when is_map(data) and is_list(errors) ->\n        flunk(\"Linear GraphQL returned partial errors: #{inspect(errors)}\")\n\n      {:ok, %{\"errors\" => errors}} when is_list(errors) ->\n        flunk(\"Linear GraphQL failed: #{inspect(errors)}\")\n\n      {:ok, %{\"data\" => data}} when is_map(data) ->\n        data\n\n      {:ok, payload} ->\n        flunk(\"Linear GraphQL returned unexpected payload: #{inspect(payload)}\")\n\n      {:error, reason} ->\n        flunk(\"Linear GraphQL request failed: #{inspect(reason)}\")\n    end\n  end\n\n  defp fetch_successful_entity!(data, mutation_name, entity_name)\n       when is_map(data) and is_binary(mutation_name) and is_binary(entity_name) do\n    case data do\n      %{^mutation_name => %{\"success\" => true, ^entity_name => %{} = entity}} ->\n        entity\n\n      _ ->\n        flunk(\"expected successful #{mutation_name} response, got: #{inspect(data)}\")\n    end\n  end\n\n  defp live_prompt(project_slug) do\n    \"\"\"\n    You are running a real Symphony end-to-end test.\n\n    The current working directory is the workspace root.\n\n    Step 1:\n    Create a file named #{@result_file} in the current working directory by running exactly:\n\n    ```sh\n    cat > #{@result_file} <<'EOF'\n    identifier={{ issue.identifier }}\n    project_slug=#{project_slug}\n    EOF\n    ```\n\n    Then verify it by running:\n\n    ```sh\n    cat #{@result_file}\n    ```\n\n    The file content must be exactly:\n    identifier={{ issue.identifier }}\n    project_slug=#{project_slug}\n\n    Step 2:\n    You must use the `linear_graphql` tool to query the current issue by `{{ issue.id }}` and read:\n    - existing comments\n    - team workflow states\n\n    A turn that only creates the file is incomplete. Do not stop after Step 1.\n\n    If the exact comment body below is not already present, post exactly one comment on the current issue with this exact body:\n    #{expected_comment(\"{{ issue.identifier }}\", project_slug)}\n\n    Use these exact GraphQL operations:\n\n    ```graphql\n    query IssueContext($id: String!) {\n      issue(id: $id) {\n        comments(first: 20) {\n          nodes {\n            body\n          }\n        }\n        team {\n          states(first: 50) {\n            nodes {\n              id\n              name\n              type\n            }\n          }\n        }\n      }\n    }\n    ```\n\n    ```graphql\n    mutation AddComment($issueId: String!, $body: String!) {\n      commentCreate(input: {issueId: $issueId, body: $body}) {\n        success\n      }\n    }\n    ```\n\n    Step 3:\n    Use the same issue-context query result to choose a workflow state whose `type` is `completed`.\n    Then move the current issue to that state with this exact mutation:\n\n    ```graphql\n    mutation CompleteIssue($id: String!, $stateId: String!) {\n      issueUpdate(id: $id, input: {stateId: $stateId}) {\n        success\n      }\n    }\n    ```\n\n    Step 4:\n    Verify all outcomes with one final `linear_graphql` query against `{{ issue.id }}`:\n    - the exact comment body is present\n    - the issue state type is `completed`\n\n    Do not ask for approval.\n    Stop only after all three conditions are true:\n    1. the file exists with the exact contents above\n    2. the Linear comment exists with the exact body above\n    3. the Linear issue is in a completed terminal state\n    \"\"\"\n  end\n\n  defp expected_result(issue_identifier, project_slug) do\n    \"identifier=#{issue_identifier}\\nproject_slug=#{project_slug}\\n\"\n  end\n\n  defp expected_comment(issue_identifier, project_slug) do\n    \"Symphony live e2e comment\\nidentifier=#{issue_identifier}\\nproject_slug=#{project_slug}\"\n  end\n\n  defp receive_runtime_info!(issue_id) do\n    receive do\n      {:worker_runtime_info, ^issue_id, %{workspace_path: workspace_path} = runtime_info}\n      when is_binary(workspace_path) ->\n        runtime_info\n\n      {:codex_worker_update, ^issue_id, _message} ->\n        receive_runtime_info!(issue_id)\n    after\n      5_000 ->\n        flunk(\"timed out waiting for worker runtime info for #{inspect(issue_id)}\")\n    end\n  end\n\n  defp read_worker_result!(%{worker_host: nil, workspace_path: workspace_path}, result_file)\n       when is_binary(workspace_path) and is_binary(result_file) do\n    File.read!(Path.join(workspace_path, result_file))\n  end\n\n  defp read_worker_result!(%{worker_host: worker_host, workspace_path: workspace_path}, result_file)\n       when is_binary(worker_host) and is_binary(workspace_path) and is_binary(result_file) do\n    remote_result_path = Path.join(workspace_path, result_file)\n\n    case SSH.run(worker_host, \"cat #{shell_escape(remote_result_path)}\", stderr_to_stdout: true) do\n      {:ok, {output, 0}} ->\n        output\n\n      {:ok, {output, status}} ->\n        flunk(\"failed to read remote result from #{worker_host}:#{remote_result_path} (status #{status}): #{inspect(output)}\")\n\n      {:error, reason} ->\n        flunk(\"failed to read remote result from #{worker_host}:#{remote_result_path}: #{inspect(reason)}\")\n    end\n  end\n\n  defp shell_escape(value) when is_binary(value) do\n    \"'\" <> String.replace(value, \"'\", \"'\\\"'\\\"'\") <> \"'\"\n  end\n\n  defp run_live_issue_flow!(backend) when backend in [:local, :ssh] do\n    run_id = \"symphony-live-e2e-#{backend}-#{System.unique_integer([:positive])}\"\n    test_root = Path.join(System.tmp_dir!(), run_id)\n    workflow_root = Path.join(test_root, \"workflow\")\n    workflow_file = Path.join(workflow_root, \"WORKFLOW.md\")\n    worker_setup = live_worker_setup!(backend, run_id, test_root)\n    team_key = System.get_env(\"SYMPHONY_LIVE_LINEAR_TEAM_KEY\") || @default_team_key\n    original_workflow_path = Workflow.workflow_file_path()\n    orchestrator_pid = Process.whereis(SymphonyElixir.Orchestrator)\n\n    File.mkdir_p!(workflow_root)\n\n    try do\n      if is_pid(orchestrator_pid) do\n        assert :ok = Supervisor.terminate_child(SymphonyElixir.Supervisor, SymphonyElixir.Orchestrator)\n      end\n\n      Workflow.set_workflow_file_path(workflow_file)\n\n      write_workflow_file!(workflow_file,\n        tracker_api_token: \"$LINEAR_API_KEY\",\n        tracker_project_slug: \"bootstrap\",\n        workspace_root: worker_setup.workspace_root,\n        worker_ssh_hosts: worker_setup.ssh_worker_hosts,\n        codex_command: worker_setup.codex_command,\n        codex_approval_policy: \"never\",\n        observability_enabled: false\n      )\n\n      team = fetch_team!(team_key)\n      active_state = active_state!(team)\n      completed_project_status = completed_project_status!()\n      terminal_states = terminal_state_names(team)\n\n      project =\n        create_project!(\n          team[\"id\"],\n          \"Symphony Live E2E #{backend} #{System.unique_integer([:positive])}\"\n        )\n\n      issue =\n        create_issue!(\n          team[\"id\"],\n          project[\"id\"],\n          active_state[\"id\"],\n          \"Symphony live e2e #{backend} issue for #{project[\"name\"]}\"\n        )\n\n      write_workflow_file!(workflow_file,\n        tracker_api_token: \"$LINEAR_API_KEY\",\n        tracker_project_slug: project[\"slugId\"],\n        tracker_active_states: active_state_names(team),\n        tracker_terminal_states: terminal_states,\n        workspace_root: worker_setup.workspace_root,\n        worker_ssh_hosts: worker_setup.ssh_worker_hosts,\n        codex_command: worker_setup.codex_command,\n        codex_approval_policy: \"never\",\n        codex_turn_timeout_ms: 600_000,\n        codex_stall_timeout_ms: 600_000,\n        observability_enabled: false,\n        prompt: live_prompt(project[\"slugId\"])\n      )\n\n      assert :ok = AgentRunner.run(issue, self(), max_turns: 3)\n\n      runtime_info = receive_runtime_info!(issue.id)\n\n      assert read_worker_result!(runtime_info, @result_file) ==\n               expected_result(issue.identifier, project[\"slugId\"])\n\n      issue_snapshot = fetch_issue_details!(issue.id)\n      assert issue_completed?(issue_snapshot)\n      assert issue_has_comment?(issue_snapshot, expected_comment(issue.identifier, project[\"slugId\"]))\n\n      assert :ok = complete_project(project[\"id\"], completed_project_status[\"id\"])\n    after\n      restart_orchestrator_if_needed()\n      cleanup_live_worker_setup(worker_setup)\n      Workflow.set_workflow_file_path(original_workflow_path)\n      File.rm_rf(test_root)\n    end\n  end\n\n  defp live_worker_setup!(:local, _run_id, test_root) when is_binary(test_root) do\n    %{\n      cleanup: fn -> :ok end,\n      codex_command: \"codex app-server\",\n      ssh_worker_hosts: [],\n      workspace_root: Path.join(test_root, \"workspaces\")\n    }\n  end\n\n  defp live_worker_setup!(:ssh, run_id, test_root) when is_binary(run_id) and is_binary(test_root) do\n    case live_ssh_worker_hosts() do\n      [] ->\n        live_docker_worker_setup!(run_id, test_root)\n\n      _hosts ->\n        live_ssh_worker_setup!(run_id)\n    end\n  end\n\n  defp cleanup_live_worker_setup(%{cleanup: cleanup}) when is_function(cleanup, 0) do\n    cleanup.()\n  end\n\n  defp cleanup_live_worker_setup(_worker_setup), do: :ok\n\n  defp restart_orchestrator_if_needed do\n    if is_nil(Process.whereis(SymphonyElixir.Orchestrator)) do\n      case Supervisor.restart_child(SymphonyElixir.Supervisor, SymphonyElixir.Orchestrator) do\n        {:ok, _pid} -> :ok\n        {:error, {:already_started, _pid}} -> :ok\n      end\n    end\n  end\n\n  defp live_ssh_worker_setup!(run_id) when is_binary(run_id) do\n    ssh_worker_hosts = live_ssh_worker_hosts()\n    remote_test_root = Path.join(shared_remote_home!(ssh_worker_hosts), \".#{run_id}\")\n    remote_workspace_root = \"~/.#{run_id}/workspaces\"\n\n    %{\n      cleanup: fn -> cleanup_remote_test_root(remote_test_root, ssh_worker_hosts) end,\n      codex_command: \"codex app-server\",\n      ssh_worker_hosts: ssh_worker_hosts,\n      workspace_root: remote_workspace_root\n    }\n  end\n\n  defp live_docker_worker_setup!(run_id, test_root) when is_binary(run_id) and is_binary(test_root) do\n    ssh_root = Path.join(test_root, \"live-docker-ssh\")\n    key_path = Path.join(ssh_root, \"id_ed25519\")\n    config_path = Path.join(ssh_root, \"config\")\n    auth_json_path = @default_docker_auth_json\n    worker_ports = reserve_tcp_ports(@docker_worker_count)\n    worker_hosts = Enum.map(worker_ports, &\"localhost:#{&1}\")\n    project_name = docker_project_name(run_id)\n    previous_ssh_config = System.get_env(\"SYMPHONY_SSH_CONFIG\")\n\n    base_cleanup = fn ->\n      restore_env(\"SYMPHONY_SSH_CONFIG\", previous_ssh_config)\n      docker_compose_down(project_name, docker_compose_env(worker_ports, auth_json_path, key_path <> \".pub\"))\n    end\n\n    result =\n      try do\n        File.mkdir_p!(ssh_root)\n        generate_ssh_keypair!(key_path)\n        write_docker_ssh_config!(config_path, key_path)\n        System.put_env(\"SYMPHONY_SSH_CONFIG\", config_path)\n\n        docker_compose_up!(project_name, docker_compose_env(worker_ports, auth_json_path, key_path <> \".pub\"))\n        wait_for_ssh_hosts!(worker_hosts)\n        remote_test_root = Path.join(shared_remote_home!(worker_hosts), \".#{run_id}\")\n        remote_workspace_root = \"~/.#{run_id}/workspaces\"\n\n        %{\n          cleanup: fn ->\n            cleanup_remote_test_root(remote_test_root, worker_hosts)\n            base_cleanup.()\n          end,\n          codex_command: \"codex app-server\",\n          ssh_worker_hosts: worker_hosts,\n          workspace_root: remote_workspace_root\n        }\n      rescue\n        error ->\n          {:error, error, __STACKTRACE__}\n      catch\n        kind, reason ->\n          {:caught, kind, reason, __STACKTRACE__}\n      end\n\n    case result do\n      %{ssh_worker_hosts: _hosts} = worker_setup ->\n        worker_setup\n\n      {:error, error, stacktrace} ->\n        base_cleanup.()\n        reraise(error, stacktrace)\n\n      {:caught, kind, reason, stacktrace} ->\n        base_cleanup.()\n        :erlang.raise(kind, reason, stacktrace)\n    end\n  end\n\n  defp live_ssh_worker_hosts do\n    System.get_env(\"SYMPHONY_LIVE_SSH_WORKER_HOSTS\", \"\")\n    |> String.split(\",\", trim: true)\n    |> Enum.map(&String.trim/1)\n    |> Enum.reject(&(&1 == \"\"))\n  end\n\n  defp cleanup_remote_test_root(test_root, ssh_worker_hosts)\n       when is_binary(test_root) and is_list(ssh_worker_hosts) do\n    Enum.each(ssh_worker_hosts, fn worker_host ->\n      _ = SSH.run(worker_host, \"rm -rf #{shell_escape(test_root)}\", stderr_to_stdout: true)\n    end)\n  end\n\n  defp shared_remote_home!([first_host | rest] = worker_hosts) when is_binary(first_host) and rest != [] do\n    homes =\n      worker_hosts\n      |> Enum.map(fn worker_host -> {worker_host, remote_home!(worker_host)} end)\n\n    [{_host, home} | _remaining] = homes\n\n    if Enum.all?(homes, fn {_host, other_home} -> other_home == home end) do\n      home\n    else\n      flunk(\"expected all live SSH workers to share one home directory, got: #{inspect(homes)}\")\n    end\n  end\n\n  defp shared_remote_home!([worker_host]) when is_binary(worker_host), do: remote_home!(worker_host)\n  defp shared_remote_home!(_worker_hosts), do: flunk(\"expected at least one live SSH worker host\")\n\n  defp remote_home!(worker_host) when is_binary(worker_host) do\n    case SSH.run(worker_host, \"printf '%s\\\\n' \\\"$HOME\\\"\", stderr_to_stdout: true) do\n      {:ok, {output, 0}} ->\n        output\n        |> String.trim()\n        |> case do\n          \"\" -> flunk(\"expected non-empty remote home for #{worker_host}\")\n          home -> home\n        end\n\n      {:ok, {output, status}} ->\n        flunk(\"failed to resolve remote home for #{worker_host} (status #{status}): #{inspect(output)}\")\n\n      {:error, reason} ->\n        flunk(\"failed to resolve remote home for #{worker_host}: #{inspect(reason)}\")\n    end\n  end\n\n  defp reserve_tcp_ports(count) when is_integer(count) and count > 0 do\n    reserve_tcp_ports(count, MapSet.new(), [])\n  end\n\n  defp reserve_tcp_ports(0, _seen, ports), do: Enum.reverse(ports)\n\n  defp reserve_tcp_ports(remaining, seen, ports) do\n    port = reserve_tcp_port!()\n\n    if MapSet.member?(seen, port) do\n      reserve_tcp_ports(remaining, seen, ports)\n    else\n      reserve_tcp_ports(remaining - 1, MapSet.put(seen, port), [port | ports])\n    end\n  end\n\n  defp reserve_tcp_port! do\n    {:ok, socket} = :gen_tcp.listen(0, [:binary, {:active, false}, {:reuseaddr, true}])\n    {:ok, port} = :inet.port(socket)\n    :ok = :gen_tcp.close(socket)\n    port\n  end\n\n  defp generate_ssh_keypair!(key_path) when is_binary(key_path) do\n    case System.find_executable(\"ssh-keygen\") do\n      nil ->\n        flunk(\"docker worker mode requires `ssh-keygen` on PATH\")\n\n      executable ->\n        key_dir = Path.dirname(key_path)\n        File.mkdir_p!(key_dir)\n        File.rm_rf(key_path)\n        File.rm_rf(key_path <> \".pub\")\n\n        case System.cmd(executable, [\"-q\", \"-t\", \"ed25519\", \"-N\", \"\", \"-f\", key_path], stderr_to_stdout: true) do\n          {_output, 0} -> :ok\n          {output, status} -> flunk(\"failed to generate live docker ssh key (status #{status}): #{inspect(output)}\")\n        end\n    end\n  end\n\n  defp write_docker_ssh_config!(config_path, key_path)\n       when is_binary(config_path) and is_binary(key_path) do\n    config_contents = \"\"\"\n    Host localhost 127.0.0.1\n      User root\n      IdentityFile #{key_path}\n      IdentitiesOnly yes\n      StrictHostKeyChecking no\n      UserKnownHostsFile /dev/null\n      LogLevel ERROR\n    \"\"\"\n\n    File.mkdir_p!(Path.dirname(config_path))\n    File.write!(config_path, config_contents)\n  end\n\n  defp docker_project_name(run_id) when is_binary(run_id) do\n    run_id\n    |> String.downcase()\n    |> String.replace(~r/[^a-z0-9_-]/, \"-\")\n  end\n\n  defp docker_compose_env(worker_ports, auth_json_path, authorized_key_path)\n       when is_list(worker_ports) and is_binary(auth_json_path) and is_binary(authorized_key_path) do\n    [\n      {\"SYMPHONY_LIVE_DOCKER_AUTH_JSON\", auth_json_path},\n      {\"SYMPHONY_LIVE_DOCKER_AUTHORIZED_KEY\", authorized_key_path},\n      {\"SYMPHONY_LIVE_DOCKER_WORKER_1_PORT\", Integer.to_string(Enum.at(worker_ports, 0))},\n      {\"SYMPHONY_LIVE_DOCKER_WORKER_2_PORT\", Integer.to_string(Enum.at(worker_ports, 1))}\n    ]\n  end\n\n  defp docker_compose_up!(project_name, env) when is_binary(project_name) and is_list(env) do\n    args = [\"compose\", \"-f\", @docker_compose_file, \"-p\", project_name, \"up\", \"-d\", \"--build\"]\n\n    case System.cmd(\"docker\", args, cd: @docker_support_dir, env: env, stderr_to_stdout: true) do\n      {_output, 0} ->\n        :ok\n\n      {output, status} ->\n        flunk(\"failed to start live docker workers (status #{status}): #{inspect(output)}\")\n    end\n  end\n\n  defp docker_compose_down(project_name, env) when is_binary(project_name) and is_list(env) do\n    _ =\n      System.cmd(\n        \"docker\",\n        [\"compose\", \"-f\", @docker_compose_file, \"-p\", project_name, \"down\", \"-v\", \"--remove-orphans\"],\n        cd: @docker_support_dir,\n        env: env,\n        stderr_to_stdout: true\n      )\n\n    :ok\n  end\n\n  defp wait_for_ssh_hosts!(worker_hosts) when is_list(worker_hosts) do\n    deadline = System.monotonic_time(:millisecond) + 60_000\n\n    Enum.each(worker_hosts, fn worker_host ->\n      wait_for_ssh_host!(worker_host, deadline)\n    end)\n  end\n\n  defp wait_for_ssh_host!(worker_host, deadline_ms) when is_binary(worker_host) do\n    case SSH.run(worker_host, \"printf ready\", stderr_to_stdout: true) do\n      {:ok, {\"ready\", 0}} ->\n        :ok\n\n      {:ok, {_output, _status}} ->\n        retry_or_flunk_ssh_host(worker_host, deadline_ms)\n\n      {:error, _reason} ->\n        retry_or_flunk_ssh_host(worker_host, deadline_ms)\n    end\n  end\n\n  defp retry_or_flunk_ssh_host(worker_host, deadline_ms) do\n    if System.monotonic_time(:millisecond) < deadline_ms do\n      Process.sleep(1_000)\n      wait_for_ssh_host!(worker_host, deadline_ms)\n    else\n      flunk(\"timed out waiting for SSH worker #{worker_host} to accept connections\")\n    end\n  end\nend\n"
  },
  {
    "path": "elixir/test/symphony_elixir/log_file_test.exs",
    "content": "defmodule SymphonyElixir.LogFileTest do\n  use ExUnit.Case, async: true\n\n  alias SymphonyElixir.LogFile\n\n  test \"default_log_file/0 uses the current working directory\" do\n    assert LogFile.default_log_file() == Path.join(File.cwd!(), \"log/symphony.log\")\n  end\n\n  test \"default_log_file/1 builds the log path under a custom root\" do\n    assert LogFile.default_log_file(\"/tmp/symphony-logs\") == \"/tmp/symphony-logs/log/symphony.log\"\n  end\nend\n"
  },
  {
    "path": "elixir/test/symphony_elixir/observability_pubsub_test.exs",
    "content": "defmodule SymphonyElixir.ObservabilityPubSubTest do\n  use SymphonyElixir.TestSupport\n\n  alias SymphonyElixirWeb.ObservabilityPubSub\n\n  test \"subscribe and broadcast_update deliver dashboard updates\" do\n    assert :ok = ObservabilityPubSub.subscribe()\n    assert :ok = ObservabilityPubSub.broadcast_update()\n    assert_receive :observability_updated\n  end\n\n  test \"broadcast_update is a no-op when pubsub is unavailable\" do\n    pubsub_child_id = Phoenix.PubSub.Supervisor\n\n    on_exit(fn ->\n      if Process.whereis(SymphonyElixir.PubSub) == nil do\n        assert {:ok, _pid} =\n                 Supervisor.restart_child(SymphonyElixir.Supervisor, pubsub_child_id)\n      end\n    end)\n\n    assert is_pid(Process.whereis(SymphonyElixir.PubSub))\n    assert :ok = Supervisor.terminate_child(SymphonyElixir.Supervisor, pubsub_child_id)\n    refute Process.whereis(SymphonyElixir.PubSub)\n\n    assert :ok = ObservabilityPubSub.broadcast_update()\n  end\nend\n"
  },
  {
    "path": "elixir/test/symphony_elixir/orchestrator_status_test.exs",
    "content": "defmodule SymphonyElixir.OrchestratorStatusTest do\n  use SymphonyElixir.TestSupport\n\n  test \"snapshot returns :timeout when snapshot server is unresponsive\" do\n    server_name = Module.concat(__MODULE__, :UnresponsiveSnapshotServer)\n    parent = self()\n\n    pid =\n      spawn(fn ->\n        Process.register(self(), server_name)\n        send(parent, :snapshot_server_ready)\n\n        receive do\n          :stop -> :ok\n        end\n      end)\n\n    assert_receive :snapshot_server_ready, 1_000\n    assert Orchestrator.snapshot(server_name, 10) == :timeout\n\n    send(pid, :stop)\n  end\n\n  test \"orchestrator snapshot reflects last codex update and session id\" do\n    issue_id = \"issue-snapshot\"\n\n    issue = %Issue{\n      id: issue_id,\n      identifier: \"MT-188\",\n      title: \"Snapshot test\",\n      description: \"Capture codex state\",\n      state: \"In Progress\",\n      url: \"https://example.org/issues/MT-188\"\n    }\n\n    orchestrator_name = Module.concat(__MODULE__, :SnapshotOrchestrator)\n    {:ok, pid} = Orchestrator.start_link(name: orchestrator_name)\n\n    on_exit(fn ->\n      if Process.alive?(pid) do\n        Process.exit(pid, :normal)\n      end\n    end)\n\n    initial_state = :sys.get_state(pid)\n    started_at = DateTime.utc_now()\n\n    running_entry = %{\n      pid: self(),\n      ref: make_ref(),\n      identifier: issue.identifier,\n      issue: issue,\n      session_id: nil,\n      turn_count: 0,\n      last_codex_message: nil,\n      last_codex_timestamp: nil,\n      last_codex_event: nil,\n      started_at: started_at\n    }\n\n    state_with_issue =\n      initial_state\n      |> Map.put(:running, %{issue_id => running_entry})\n      |> Map.put(:claimed, MapSet.put(initial_state.claimed, issue_id))\n\n    :sys.replace_state(pid, fn _ -> state_with_issue end)\n\n    now = DateTime.utc_now()\n\n    send(\n      pid,\n      {:codex_worker_update, issue_id,\n       %{\n         event: :session_started,\n         session_id: \"thread-live-turn-live\",\n         timestamp: now\n       }}\n    )\n\n    send(\n      pid,\n      {:codex_worker_update, issue_id,\n       %{\n         event: :notification,\n         payload: %{method: \"some-event\"},\n         timestamp: now\n       }}\n    )\n\n    snapshot = GenServer.call(pid, :snapshot)\n    assert %{running: [snapshot_entry]} = snapshot\n    assert snapshot_entry.issue_id == issue_id\n    assert snapshot_entry.session_id == \"thread-live-turn-live\"\n    assert snapshot_entry.turn_count == 1\n    assert snapshot_entry.last_codex_timestamp == now\n\n    assert snapshot_entry.last_codex_message == %{\n             event: :notification,\n             message: %{method: \"some-event\"},\n             timestamp: now\n           }\n  end\n\n  test \"orchestrator snapshot tracks codex thread totals and app-server pid\" do\n    issue_id = \"issue-usage-snapshot\"\n\n    issue = %Issue{\n      id: issue_id,\n      identifier: \"MT-201\",\n      title: \"Usage snapshot test\",\n      description: \"Collect usage stats\",\n      state: \"In Progress\",\n      url: \"https://example.org/issues/MT-201\"\n    }\n\n    orchestrator_name = Module.concat(__MODULE__, :UsageOrchestrator)\n    {:ok, pid} = Orchestrator.start_link(name: orchestrator_name)\n\n    on_exit(fn ->\n      if Process.alive?(pid) do\n        Process.exit(pid, :normal)\n      end\n    end)\n\n    initial_state = :sys.get_state(pid)\n    process_ref = make_ref()\n    started_at = DateTime.utc_now()\n\n    running_entry = %{\n      pid: self(),\n      ref: process_ref,\n      identifier: issue.identifier,\n      issue: issue,\n      session_id: nil,\n      turn_count: 0,\n      last_codex_message: nil,\n      last_codex_timestamp: nil,\n      last_codex_event: nil,\n      codex_input_tokens: 0,\n      codex_output_tokens: 0,\n      codex_total_tokens: 0,\n      codex_last_reported_input_tokens: 0,\n      codex_last_reported_output_tokens: 0,\n      codex_last_reported_total_tokens: 0,\n      started_at: started_at\n    }\n\n    :sys.replace_state(pid, fn _ ->\n      initial_state\n      |> Map.put(:running, %{issue_id => running_entry})\n      |> Map.put(:claimed, MapSet.put(initial_state.claimed, issue_id))\n    end)\n\n    now = DateTime.utc_now()\n\n    send(\n      pid,\n      {:codex_worker_update, issue_id,\n       %{\n         event: :session_started,\n         session_id: \"thread-usage-turn-usage\",\n         timestamp: now\n       }}\n    )\n\n    send(\n      pid,\n      {:codex_worker_update, issue_id,\n       %{\n         event: :notification,\n         payload: %{\n           \"method\" => \"thread/tokenUsage/updated\",\n           \"params\" => %{\n             \"tokenUsage\" => %{\n               \"total\" => %{\"inputTokens\" => 12, \"outputTokens\" => 4, \"totalTokens\" => 16}\n             }\n           }\n         },\n         timestamp: now,\n         codex_app_server_pid: \"4242\"\n       }}\n    )\n\n    snapshot = GenServer.call(pid, :snapshot)\n    assert %{running: [snapshot_entry]} = snapshot\n    assert snapshot_entry.codex_app_server_pid == \"4242\"\n    assert snapshot_entry.codex_input_tokens == 12\n    assert snapshot_entry.codex_output_tokens == 4\n    assert snapshot_entry.codex_total_tokens == 16\n    assert snapshot_entry.turn_count == 1\n    assert is_integer(snapshot_entry.runtime_seconds)\n\n    send(pid, {:DOWN, process_ref, :process, self(), :normal})\n    completed_state = :sys.get_state(pid)\n\n    assert completed_state.codex_totals.input_tokens == 12\n    assert completed_state.codex_totals.output_tokens == 4\n    assert completed_state.codex_totals.total_tokens == 16\n    assert is_integer(completed_state.codex_totals.seconds_running)\n  end\n\n  test \"orchestrator snapshot tracks turn completed usage when present\" do\n    issue_id = \"issue-turn-completed-usage\"\n\n    issue = %Issue{\n      id: issue_id,\n      identifier: \"MT-202\",\n      title: \"Turn completed usage test\",\n      description: \"Track final turn usage\",\n      state: \"In Progress\",\n      url: \"https://example.org/issues/MT-202\"\n    }\n\n    orchestrator_name = Module.concat(__MODULE__, :TurnCompletedUsageOrchestrator)\n    {:ok, pid} = Orchestrator.start_link(name: orchestrator_name)\n\n    on_exit(fn ->\n      if Process.alive?(pid) do\n        Process.exit(pid, :normal)\n      end\n    end)\n\n    initial_state = :sys.get_state(pid)\n    process_ref = make_ref()\n    started_at = DateTime.utc_now()\n\n    running_entry = %{\n      pid: self(),\n      ref: process_ref,\n      identifier: issue.identifier,\n      issue: issue,\n      session_id: nil,\n      last_codex_message: nil,\n      last_codex_timestamp: nil,\n      last_codex_event: nil,\n      codex_input_tokens: 0,\n      codex_output_tokens: 0,\n      codex_total_tokens: 0,\n      codex_last_reported_input_tokens: 0,\n      codex_last_reported_output_tokens: 0,\n      codex_last_reported_total_tokens: 0,\n      started_at: started_at\n    }\n\n    :sys.replace_state(pid, fn _ ->\n      initial_state\n      |> Map.put(:running, %{issue_id => running_entry})\n      |> Map.put(:claimed, MapSet.put(initial_state.claimed, issue_id))\n    end)\n\n    send(\n      pid,\n      {:codex_worker_update, issue_id,\n       %{\n         event: :turn_completed,\n         payload: %{\n           method: \"turn/completed\",\n           usage: %{\"input_tokens\" => \"12\", \"output_tokens\" => 4, \"total_tokens\" => 16}\n         },\n         timestamp: DateTime.utc_now()\n       }}\n    )\n\n    snapshot = GenServer.call(pid, :snapshot)\n    assert %{running: [snapshot_entry]} = snapshot\n    assert snapshot_entry.codex_input_tokens == 12\n    assert snapshot_entry.codex_output_tokens == 4\n    assert snapshot_entry.codex_total_tokens == 16\n\n    send(pid, {:DOWN, process_ref, :process, self(), :normal})\n    completed_state = :sys.get_state(pid)\n    assert completed_state.codex_totals.input_tokens == 12\n    assert completed_state.codex_totals.output_tokens == 4\n    assert completed_state.codex_totals.total_tokens == 16\n  end\n\n  test \"orchestrator snapshot tracks codex token-count cumulative usage payloads\" do\n    issue_id = \"issue-token-count-snapshot\"\n\n    issue = %Issue{\n      id: issue_id,\n      identifier: \"MT-220\",\n      title: \"Token count snapshot test\",\n      description: \"Validate token-count style payloads\",\n      state: \"In Progress\",\n      url: \"https://example.org/issues/MT-220\"\n    }\n\n    orchestrator_name = Module.concat(__MODULE__, :TokenCountOrchestrator)\n    {:ok, pid} = Orchestrator.start_link(name: orchestrator_name)\n\n    on_exit(fn ->\n      if Process.alive?(pid) do\n        Process.exit(pid, :normal)\n      end\n    end)\n\n    initial_state = :sys.get_state(pid)\n    process_ref = make_ref()\n    started_at = DateTime.utc_now()\n\n    running_entry = %{\n      pid: self(),\n      ref: process_ref,\n      identifier: issue.identifier,\n      issue: issue,\n      session_id: nil,\n      last_codex_message: nil,\n      last_codex_timestamp: nil,\n      last_codex_event: nil,\n      codex_input_tokens: 0,\n      codex_output_tokens: 0,\n      codex_total_tokens: 0,\n      codex_last_reported_input_tokens: 0,\n      codex_last_reported_output_tokens: 0,\n      codex_last_reported_total_tokens: 0,\n      started_at: started_at\n    }\n\n    :sys.replace_state(pid, fn _ ->\n      initial_state\n      |> Map.put(:running, %{issue_id => running_entry})\n      |> Map.put(:claimed, MapSet.put(initial_state.claimed, issue_id))\n    end)\n\n    now = DateTime.utc_now()\n\n    send(\n      pid,\n      {:codex_worker_update, issue_id,\n       %{\n         event: :notification,\n         payload: %{\n           \"method\" => \"codex/event/token_count\",\n           \"params\" => %{\n             \"msg\" => %{\n               \"type\" => \"token_count\",\n               \"info\" => %{\n                 \"total_token_usage\" => %{\n                   \"input_tokens\" => \"2\",\n                   \"output_tokens\" => 2,\n                   \"total_tokens\" => 4\n                 }\n               }\n             }\n           }\n         },\n         timestamp: now\n       }}\n    )\n\n    send(\n      pid,\n      {:codex_worker_update, issue_id,\n       %{\n         event: :notification,\n         payload: %{\n           \"method\" => \"codex/event/token_count\",\n           \"params\" => %{\n             \"msg\" => %{\n               \"type\" => \"token_count\",\n               \"info\" => %{\n                 \"total_token_usage\" => %{\n                   \"prompt_tokens\" => 10,\n                   \"completion_tokens\" => 5,\n                   \"total_tokens\" => 15\n                 }\n               }\n             }\n           }\n         },\n         timestamp: DateTime.utc_now()\n       }}\n    )\n\n    snapshot = GenServer.call(pid, :snapshot)\n    assert %{running: [snapshot_entry]} = snapshot\n    assert snapshot_entry.codex_input_tokens == 10\n    assert snapshot_entry.codex_output_tokens == 5\n    assert snapshot_entry.codex_total_tokens == 15\n\n    send(pid, {:DOWN, process_ref, :process, self(), :normal})\n    completed_state = :sys.get_state(pid)\n\n    assert completed_state.codex_totals.input_tokens == 10\n    assert completed_state.codex_totals.output_tokens == 5\n    assert completed_state.codex_totals.total_tokens == 15\n  end\n\n  test \"orchestrator snapshot tracks codex rate-limit payloads\" do\n    issue_id = \"issue-rate-limit-snapshot\"\n\n    issue = %Issue{\n      id: issue_id,\n      identifier: \"MT-221\",\n      title: \"Rate limit snapshot test\",\n      description: \"Capture codex rate limit state\",\n      state: \"In Progress\",\n      url: \"https://example.org/issues/MT-221\"\n    }\n\n    orchestrator_name = Module.concat(__MODULE__, :RateLimitOrchestrator)\n    {:ok, pid} = Orchestrator.start_link(name: orchestrator_name)\n\n    on_exit(fn ->\n      if Process.alive?(pid) do\n        Process.exit(pid, :normal)\n      end\n    end)\n\n    initial_state = :sys.get_state(pid)\n    process_ref = make_ref()\n    started_at = DateTime.utc_now()\n\n    running_entry = %{\n      pid: self(),\n      ref: process_ref,\n      identifier: issue.identifier,\n      issue: issue,\n      session_id: nil,\n      last_codex_message: nil,\n      last_codex_timestamp: nil,\n      last_codex_event: nil,\n      codex_input_tokens: 0,\n      codex_output_tokens: 0,\n      codex_total_tokens: 0,\n      codex_last_reported_input_tokens: 0,\n      codex_last_reported_output_tokens: 0,\n      codex_last_reported_total_tokens: 0,\n      started_at: started_at\n    }\n\n    :sys.replace_state(pid, fn _ ->\n      initial_state\n      |> Map.put(:running, %{issue_id => running_entry})\n      |> Map.put(:claimed, MapSet.put(initial_state.claimed, issue_id))\n    end)\n\n    rate_limits = %{\n      \"limit_id\" => \"codex\",\n      \"primary\" => %{\"remaining\" => 90, \"limit\" => 100},\n      \"secondary\" => nil,\n      \"credits\" => %{\"has_credits\" => false, \"unlimited\" => false, \"balance\" => nil}\n    }\n\n    send(\n      pid,\n      {:codex_worker_update, issue_id,\n       %{\n         event: :notification,\n         payload: %{\n           \"method\" => \"codex/event/token_count\",\n           \"params\" => %{\n             \"msg\" => %{\n               \"type\" => \"event_msg\",\n               \"payload\" => %{\n                 \"type\" => \"token_count\",\n                 \"rate_limits\" => rate_limits\n               }\n             }\n           }\n         },\n         timestamp: DateTime.utc_now()\n       }}\n    )\n\n    snapshot = GenServer.call(pid, :snapshot)\n    assert snapshot.rate_limits == rate_limits\n  end\n\n  test \"orchestrator token accounting prefers total_token_usage over last_token_usage in token_count payloads\" do\n    issue_id = \"issue-token-precedence\"\n\n    issue = %Issue{\n      id: issue_id,\n      identifier: \"MT-222\",\n      title: \"Token precedence\",\n      description: \"Prefer per-event deltas\",\n      state: \"In Progress\",\n      url: \"https://example.org/issues/MT-222\"\n    }\n\n    orchestrator_name = Module.concat(__MODULE__, :TokenPrecedenceOrchestrator)\n    {:ok, pid} = Orchestrator.start_link(name: orchestrator_name)\n\n    on_exit(fn ->\n      if Process.alive?(pid) do\n        Process.exit(pid, :normal)\n      end\n    end)\n\n    initial_state = :sys.get_state(pid)\n    process_ref = make_ref()\n    started_at = DateTime.utc_now()\n\n    running_entry = %{\n      pid: self(),\n      ref: process_ref,\n      identifier: issue.identifier,\n      issue: issue,\n      session_id: nil,\n      last_codex_message: nil,\n      last_codex_timestamp: nil,\n      last_codex_event: nil,\n      codex_input_tokens: 0,\n      codex_output_tokens: 0,\n      codex_total_tokens: 0,\n      codex_last_reported_input_tokens: 0,\n      codex_last_reported_output_tokens: 0,\n      codex_last_reported_total_tokens: 0,\n      started_at: started_at\n    }\n\n    :sys.replace_state(pid, fn _ ->\n      initial_state\n      |> Map.put(:running, %{issue_id => running_entry})\n      |> Map.put(:claimed, MapSet.put(initial_state.claimed, issue_id))\n    end)\n\n    send(\n      pid,\n      {:codex_worker_update, issue_id,\n       %{\n         event: :notification,\n         payload: %{\n           \"method\" => \"codex/event/token_count\",\n           \"params\" => %{\n             \"msg\" => %{\n               \"type\" => \"event_msg\",\n               \"payload\" => %{\n                 \"type\" => \"token_count\",\n                 \"info\" => %{\n                   \"last_token_usage\" => %{\n                     \"input_tokens\" => 2,\n                     \"output_tokens\" => 1,\n                     \"total_tokens\" => 3\n                   },\n                   \"total_token_usage\" => %{\n                     \"input_tokens\" => 200,\n                     \"output_tokens\" => 100,\n                     \"total_tokens\" => 300\n                   }\n                 }\n               }\n             }\n           }\n         },\n         timestamp: DateTime.utc_now()\n       }}\n    )\n\n    snapshot = GenServer.call(pid, :snapshot)\n    assert %{running: [snapshot_entry]} = snapshot\n    assert snapshot_entry.codex_input_tokens == 200\n    assert snapshot_entry.codex_output_tokens == 100\n    assert snapshot_entry.codex_total_tokens == 300\n  end\n\n  test \"orchestrator token accounting accumulates monotonic thread token usage totals\" do\n    issue_id = \"issue-thread-token-usage\"\n\n    issue = %Issue{\n      id: issue_id,\n      identifier: \"MT-223\",\n      title: \"Thread token usage\",\n      description: \"Accumulate absolute thread totals\",\n      state: \"In Progress\",\n      url: \"https://example.org/issues/MT-223\"\n    }\n\n    orchestrator_name = Module.concat(__MODULE__, :ThreadTokenUsageOrchestrator)\n    {:ok, pid} = Orchestrator.start_link(name: orchestrator_name)\n\n    on_exit(fn ->\n      if Process.alive?(pid) do\n        Process.exit(pid, :normal)\n      end\n    end)\n\n    initial_state = :sys.get_state(pid)\n    process_ref = make_ref()\n    started_at = DateTime.utc_now()\n\n    running_entry = %{\n      pid: self(),\n      ref: process_ref,\n      identifier: issue.identifier,\n      issue: issue,\n      session_id: nil,\n      last_codex_message: nil,\n      last_codex_timestamp: nil,\n      last_codex_event: nil,\n      codex_input_tokens: 0,\n      codex_output_tokens: 0,\n      codex_total_tokens: 0,\n      codex_last_reported_input_tokens: 0,\n      codex_last_reported_output_tokens: 0,\n      codex_last_reported_total_tokens: 0,\n      started_at: started_at\n    }\n\n    :sys.replace_state(pid, fn _ ->\n      initial_state\n      |> Map.put(:running, %{issue_id => running_entry})\n      |> Map.put(:claimed, MapSet.put(initial_state.claimed, issue_id))\n    end)\n\n    for usage <- [\n          %{\"input_tokens\" => 8, \"output_tokens\" => 3, \"total_tokens\" => 11},\n          %{\"input_tokens\" => 10, \"output_tokens\" => 4, \"total_tokens\" => 14}\n        ] do\n      send(\n        pid,\n        {:codex_worker_update, issue_id,\n         %{\n           event: :notification,\n           payload: %{\n             \"method\" => \"thread/tokenUsage/updated\",\n             \"params\" => %{\"tokenUsage\" => %{\"total\" => usage}}\n           },\n           timestamp: DateTime.utc_now()\n         }}\n      )\n    end\n\n    snapshot = GenServer.call(pid, :snapshot)\n    assert %{running: [snapshot_entry]} = snapshot\n    assert snapshot_entry.codex_input_tokens == 10\n    assert snapshot_entry.codex_output_tokens == 4\n    assert snapshot_entry.codex_total_tokens == 14\n  end\n\n  test \"orchestrator token accounting ignores last_token_usage without cumulative totals\" do\n    issue_id = \"issue-last-token-ignored\"\n\n    issue = %Issue{\n      id: issue_id,\n      identifier: \"MT-224\",\n      title: \"Last token ignored\",\n      description: \"Ignore delta-only token reports\",\n      state: \"In Progress\",\n      url: \"https://example.org/issues/MT-224\"\n    }\n\n    orchestrator_name = Module.concat(__MODULE__, :LastTokenIgnoredOrchestrator)\n    {:ok, pid} = Orchestrator.start_link(name: orchestrator_name)\n\n    on_exit(fn ->\n      if Process.alive?(pid) do\n        Process.exit(pid, :normal)\n      end\n    end)\n\n    initial_state = :sys.get_state(pid)\n    process_ref = make_ref()\n    started_at = DateTime.utc_now()\n\n    running_entry = %{\n      pid: self(),\n      ref: process_ref,\n      identifier: issue.identifier,\n      issue: issue,\n      session_id: nil,\n      last_codex_message: nil,\n      last_codex_timestamp: nil,\n      last_codex_event: nil,\n      codex_input_tokens: 0,\n      codex_output_tokens: 0,\n      codex_total_tokens: 0,\n      codex_last_reported_input_tokens: 0,\n      codex_last_reported_output_tokens: 0,\n      codex_last_reported_total_tokens: 0,\n      started_at: started_at\n    }\n\n    :sys.replace_state(pid, fn _ ->\n      initial_state\n      |> Map.put(:running, %{issue_id => running_entry})\n      |> Map.put(:claimed, MapSet.put(initial_state.claimed, issue_id))\n    end)\n\n    send(\n      pid,\n      {:codex_worker_update, issue_id,\n       %{\n         event: :notification,\n         payload: %{\n           \"method\" => \"codex/event/token_count\",\n           \"params\" => %{\n             \"msg\" => %{\n               \"type\" => \"event_msg\",\n               \"payload\" => %{\n                 \"type\" => \"token_count\",\n                 \"info\" => %{\n                   \"last_token_usage\" => %{\n                     \"input_tokens\" => 8,\n                     \"output_tokens\" => 3,\n                     \"total_tokens\" => 11\n                   }\n                 }\n               }\n             }\n           }\n         },\n         timestamp: DateTime.utc_now()\n       }}\n    )\n\n    snapshot = GenServer.call(pid, :snapshot)\n    assert %{running: [snapshot_entry]} = snapshot\n    assert snapshot_entry.codex_input_tokens == 0\n    assert snapshot_entry.codex_output_tokens == 0\n    assert snapshot_entry.codex_total_tokens == 0\n  end\n\n  test \"orchestrator snapshot includes retry backoff entries\" do\n    orchestrator_name = Module.concat(__MODULE__, :RetryOrchestrator)\n    {:ok, pid} = Orchestrator.start_link(name: orchestrator_name)\n\n    on_exit(fn ->\n      if Process.alive?(pid) do\n        Process.exit(pid, :normal)\n      end\n    end)\n\n    retry_entry = %{\n      attempt: 2,\n      timer_ref: nil,\n      due_at_ms: System.monotonic_time(:millisecond) + 5_000,\n      identifier: \"MT-500\",\n      error: \"agent exited: :boom\"\n    }\n\n    initial_state = :sys.get_state(pid)\n    new_state = %{initial_state | retry_attempts: %{\"mt-500\" => retry_entry}}\n    :sys.replace_state(pid, fn _ -> new_state end)\n\n    snapshot = GenServer.call(pid, :snapshot)\n    assert is_list(snapshot.retrying)\n\n    assert [\n             %{\n               issue_id: \"mt-500\",\n               attempt: 2,\n               due_in_ms: due_in_ms,\n               identifier: \"MT-500\",\n               error: \"agent exited: :boom\"\n             }\n           ] = snapshot.retrying\n\n    assert due_in_ms > 0\n  end\n\n  test \"orchestrator snapshot includes poll countdown and checking status\" do\n    orchestrator_name = Module.concat(__MODULE__, :PollingSnapshotOrchestrator)\n    {:ok, pid} = Orchestrator.start_link(name: orchestrator_name)\n\n    on_exit(fn ->\n      if Process.alive?(pid) do\n        Process.exit(pid, :normal)\n      end\n    end)\n\n    now_ms = System.monotonic_time(:millisecond)\n\n    :sys.replace_state(pid, fn state ->\n      %{\n        state\n        | poll_interval_ms: 30_000,\n          tick_timer_ref: nil,\n          tick_token: make_ref(),\n          next_poll_due_at_ms: now_ms + 4_000,\n          poll_check_in_progress: false\n      }\n    end)\n\n    snapshot = GenServer.call(pid, :snapshot)\n\n    assert %{\n             polling: %{\n               checking?: false,\n               poll_interval_ms: 30_000,\n               next_poll_in_ms: due_in_ms\n             }\n           } = snapshot\n\n    assert is_integer(due_in_ms)\n    assert due_in_ms >= 0\n    assert due_in_ms <= 4_000\n\n    :sys.replace_state(pid, fn state ->\n      %{state | poll_check_in_progress: true, next_poll_due_at_ms: nil}\n    end)\n\n    snapshot = GenServer.call(pid, :snapshot)\n    assert %{polling: %{checking?: true, next_poll_in_ms: nil}} = snapshot\n  end\n\n  test \"orchestrator triggers an immediate poll cycle shortly after startup\" do\n    write_workflow_file!(Workflow.workflow_file_path(),\n      tracker_api_token: nil,\n      poll_interval_ms: 5_000\n    )\n\n    orchestrator_name = Module.concat(__MODULE__, :ImmediateStartupOrchestrator)\n    {:ok, pid} = Orchestrator.start_link(name: orchestrator_name)\n\n    on_exit(fn ->\n      if Process.alive?(pid) do\n        Process.exit(pid, :normal)\n      end\n    end)\n\n    assert %{polling: %{checking?: true}} =\n             wait_for_snapshot(\n               pid,\n               fn\n                 %{polling: %{checking?: true}} ->\n                   true\n\n                 _ ->\n                   false\n               end,\n               500\n             )\n\n    assert %{\n             polling: %{\n               checking?: false,\n               next_poll_in_ms: next_poll_in_ms,\n               poll_interval_ms: 5_000\n             }\n           } =\n             wait_for_snapshot(\n               pid,\n               fn\n                 %{polling: %{checking?: false, next_poll_in_ms: due_in_ms}}\n                 when is_integer(due_in_ms) and due_in_ms <= 5_000 ->\n                   true\n\n                 _ ->\n                   false\n               end,\n               500\n             )\n\n    assert is_integer(next_poll_in_ms)\n    assert next_poll_in_ms >= 0\n  end\n\n  test \"orchestrator poll cycle resets next refresh countdown after a check\" do\n    write_workflow_file!(Workflow.workflow_file_path(),\n      tracker_api_token: nil,\n      poll_interval_ms: 50\n    )\n\n    orchestrator_name = Module.concat(__MODULE__, :PollCycleOrchestrator)\n    {:ok, pid} = Orchestrator.start_link(name: orchestrator_name)\n\n    on_exit(fn ->\n      if Process.alive?(pid) do\n        Process.exit(pid, :normal)\n      end\n    end)\n\n    :sys.replace_state(pid, fn state ->\n      %{\n        state\n        | poll_interval_ms: 50,\n          poll_check_in_progress: true,\n          next_poll_due_at_ms: nil\n      }\n    end)\n\n    send(pid, :run_poll_cycle)\n\n    snapshot =\n      wait_for_snapshot(pid, fn\n        %{polling: %{checking?: false, poll_interval_ms: 50, next_poll_in_ms: next_poll_in_ms}}\n        when is_integer(next_poll_in_ms) and next_poll_in_ms <= 50 ->\n          true\n\n        _ ->\n          false\n      end)\n\n    assert %{\n             polling: %{\n               checking?: false,\n               poll_interval_ms: 50,\n               next_poll_in_ms: next_poll_in_ms\n             }\n           } = snapshot\n\n    assert is_integer(next_poll_in_ms)\n    assert next_poll_in_ms >= 0\n    assert next_poll_in_ms <= 50\n  end\n\n  test \"orchestrator restarts stalled workers with retry backoff\" do\n    write_workflow_file!(Workflow.workflow_file_path(),\n      tracker_api_token: nil,\n      codex_stall_timeout_ms: 1_000\n    )\n\n    issue_id = \"issue-stall\"\n    orchestrator_name = Module.concat(__MODULE__, :StallOrchestrator)\n    {:ok, pid} = Orchestrator.start_link(name: orchestrator_name)\n\n    on_exit(fn ->\n      if Process.alive?(pid) do\n        Process.exit(pid, :normal)\n      end\n    end)\n\n    worker_pid =\n      spawn(fn ->\n        receive do\n          :done -> :ok\n        end\n      end)\n\n    stale_activity_at = DateTime.add(DateTime.utc_now(), -5, :second)\n    initial_state = :sys.get_state(pid)\n\n    running_entry = %{\n      pid: worker_pid,\n      ref: make_ref(),\n      identifier: \"MT-STALL\",\n      issue: %Issue{id: issue_id, identifier: \"MT-STALL\", state: \"In Progress\"},\n      session_id: \"thread-stall-turn-stall\",\n      last_codex_message: nil,\n      last_codex_timestamp: stale_activity_at,\n      last_codex_event: :notification,\n      started_at: stale_activity_at\n    }\n\n    :sys.replace_state(pid, fn _ ->\n      initial_state\n      |> Map.put(:running, %{issue_id => running_entry})\n      |> Map.put(:claimed, MapSet.put(initial_state.claimed, issue_id))\n    end)\n\n    send(pid, :tick)\n    Process.sleep(100)\n    state = :sys.get_state(pid)\n\n    refute Process.alive?(worker_pid)\n    refute Map.has_key?(state.running, issue_id)\n\n    assert %{\n             attempt: 1,\n             due_at_ms: due_at_ms,\n             identifier: \"MT-STALL\",\n             error: \"stalled for \" <> _\n           } = state.retry_attempts[issue_id]\n\n    assert is_integer(due_at_ms)\n    remaining_ms = due_at_ms - System.monotonic_time(:millisecond)\n    assert remaining_ms >= 9_500\n    assert remaining_ms <= 10_500\n  end\n\n  test \"status dashboard renders offline marker to terminal\" do\n    rendered =\n      ExUnit.CaptureIO.capture_io(fn ->\n        assert :ok = StatusDashboard.render_offline_status()\n      end)\n\n    assert rendered =~ \"app_status=offline\"\n    refute rendered =~ \"Timestamp:\"\n  end\n\n  test \"status dashboard renders linear project link in header\" do\n    snapshot_data =\n      {:ok,\n       %{\n         running: [],\n         retrying: [],\n         codex_totals: %{input_tokens: 0, output_tokens: 0, total_tokens: 0, seconds_running: 0},\n         rate_limits: nil\n       }}\n\n    rendered = StatusDashboard.format_snapshot_content_for_test(snapshot_data, 0.0)\n\n    assert rendered =~ \"https://linear.app/project/project/issues\"\n    refute rendered =~ \"Dashboard:\"\n  end\n\n  test \"status dashboard renders dashboard url on its own line when server port is configured\" do\n    previous_port_override = Application.get_env(:symphony_elixir, :server_port_override)\n\n    on_exit(fn ->\n      if is_nil(previous_port_override) do\n        Application.delete_env(:symphony_elixir, :server_port_override)\n      else\n        Application.put_env(:symphony_elixir, :server_port_override, previous_port_override)\n      end\n    end)\n\n    Application.put_env(:symphony_elixir, :server_port_override, 4000)\n\n    snapshot_data =\n      {:ok,\n       %{\n         running: [],\n         retrying: [],\n         codex_totals: %{input_tokens: 0, output_tokens: 0, total_tokens: 0, seconds_running: 0},\n         rate_limits: nil\n       }}\n\n    rendered = StatusDashboard.format_snapshot_content_for_test(snapshot_data, 0.0)\n\n    assert rendered =~ \"│ Project:\"\n    assert rendered =~ \"https://linear.app/project/project/issues\"\n    assert rendered =~ \"│ Dashboard:\"\n    assert rendered =~ \"http://127.0.0.1:4000/\"\n  end\n\n  test \"status dashboard prefers the bound server port and normalizes wildcard hosts\" do\n    assert StatusDashboard.dashboard_url_for_test(\"0.0.0.0\", 0, 43_123) ==\n             \"http://127.0.0.1:43123/\"\n\n    assert StatusDashboard.dashboard_url_for_test(\"::1\", 4000, nil) ==\n             \"http://[::1]:4000/\"\n  end\n\n  test \"status dashboard renders next refresh countdown and checking marker\" do\n    waiting_snapshot =\n      {:ok,\n       %{\n         running: [],\n         retrying: [],\n         codex_totals: %{input_tokens: 0, output_tokens: 0, total_tokens: 0, seconds_running: 0},\n         rate_limits: nil,\n         polling: %{checking?: false, next_poll_in_ms: 2_000, poll_interval_ms: 30_000}\n       }}\n\n    waiting_rendered = StatusDashboard.format_snapshot_content_for_test(waiting_snapshot, 0.0)\n    assert waiting_rendered =~ \"Next refresh:\"\n    assert waiting_rendered =~ \"2s\"\n\n    checking_snapshot =\n      {:ok,\n       %{\n         running: [],\n         retrying: [],\n         codex_totals: %{input_tokens: 0, output_tokens: 0, total_tokens: 0, seconds_running: 0},\n         rate_limits: nil,\n         polling: %{checking?: true, next_poll_in_ms: nil, poll_interval_ms: 30_000}\n       }}\n\n    checking_rendered = StatusDashboard.format_snapshot_content_for_test(checking_snapshot, 0.0)\n    assert checking_rendered =~ \"checking now…\"\n  end\n\n  test \"status dashboard adds a spacer line before backoff queue when no agents are active\" do\n    snapshot_data =\n      {:ok,\n       %{\n         running: [],\n         retrying: [],\n         codex_totals: %{input_tokens: 0, output_tokens: 0, total_tokens: 0, seconds_running: 0},\n         rate_limits: nil\n       }}\n\n    rendered = StatusDashboard.format_snapshot_content_for_test(snapshot_data, 0.0)\n    plain = Regex.replace(~r/\\e\\[[0-9;]*m/, rendered, \"\")\n\n    assert plain =~ ~r/No active agents\\r?\\n│\\s*\\r?\\n├─ Backoff queue/\n  end\n\n  test \"status dashboard adds a spacer line before backoff queue when agents are active\" do\n    snapshot_data =\n      {:ok,\n       %{\n         running: [\n           %{\n             identifier: \"MT-777\",\n             state: \"running\",\n             session_id: \"thread-1234567890\",\n             codex_app_server_pid: \"4242\",\n             codex_total_tokens: 3_200,\n             runtime_seconds: 75,\n             turn_count: 7,\n             last_codex_event: \"turn_completed\",\n             last_codex_message: %{\n               event: :notification,\n               message: %{\n                 \"method\" => \"turn/completed\",\n                 \"params\" => %{\"turn\" => %{\"status\" => \"completed\"}}\n               }\n             }\n           }\n         ],\n         retrying: [],\n         codex_totals: %{\n           input_tokens: 90,\n           output_tokens: 12,\n           total_tokens: 102,\n           seconds_running: 75\n         },\n         rate_limits: nil\n       }}\n\n    rendered = StatusDashboard.format_snapshot_content_for_test(snapshot_data, 0.0)\n    plain = Regex.replace(~r/\\e\\[[0-9;]*m/, rendered, \"\")\n\n    assert plain =~ ~r/MT-777.*\\r?\\n│\\s*\\r?\\n├─ Backoff queue/s\n  end\n\n  test \"status dashboard renders an unstyled closing corner when the retry queue is empty\" do\n    snapshot_data =\n      {:ok,\n       %{\n         running: [],\n         retrying: [],\n         codex_totals: %{input_tokens: 0, output_tokens: 0, total_tokens: 0, seconds_running: 0},\n         rate_limits: nil\n       }}\n\n    rendered = StatusDashboard.format_snapshot_content_for_test(snapshot_data, 0.0)\n\n    assert rendered |> String.split(\"\\n\") |> List.last() == \"╰─\"\n  end\n\n  test \"status dashboard coalesces rapid updates to one render per interval\" do\n    dashboard_name = Module.concat(__MODULE__, :RenderDashboard)\n    parent = self()\n    orchestrator_pid = Process.whereis(SymphonyElixir.Orchestrator)\n\n    on_exit(fn ->\n      if is_nil(Process.whereis(SymphonyElixir.Orchestrator)) do\n        case Supervisor.restart_child(SymphonyElixir.Supervisor, SymphonyElixir.Orchestrator) do\n          {:ok, _pid} -> :ok\n          {:error, {:already_started, _pid}} -> :ok\n        end\n      end\n    end)\n\n    if is_pid(orchestrator_pid) do\n      assert :ok = Supervisor.terminate_child(SymphonyElixir.Supervisor, SymphonyElixir.Orchestrator)\n    end\n\n    {:ok, pid} =\n      StatusDashboard.start_link(\n        name: dashboard_name,\n        enabled: true,\n        refresh_ms: 60_000,\n        render_interval_ms: 16,\n        render_fun: fn content ->\n          send(parent, {:render, System.monotonic_time(:millisecond), content})\n        end\n      )\n\n    on_exit(fn ->\n      if Process.alive?(pid) do\n        Process.exit(pid, :normal)\n      end\n    end)\n\n    StatusDashboard.notify_update(dashboard_name)\n    assert_receive {:render, first_render_ms, _content}, 200\n\n    :sys.replace_state(pid, fn state ->\n      %{state | last_snapshot_fingerprint: :force_next_change, last_rendered_content: nil}\n    end)\n\n    StatusDashboard.notify_update(dashboard_name)\n    StatusDashboard.notify_update(dashboard_name)\n\n    assert_receive {:render, second_render_ms, _content}, 200\n    assert second_render_ms > first_render_ms\n    refute_receive {:render, _third_render_ms, _content}, 60\n  end\n\n  test \"status dashboard computes rolling 5-second token throughput\" do\n    assert StatusDashboard.rolling_tps([], 10_000, 0) == 0.0\n\n    assert StatusDashboard.rolling_tps([{9_000, 20}], 10_000, 40) == 20.0\n\n    # sample older than 5s is dropped from the window\n    assert StatusDashboard.rolling_tps([{4_900, 10}], 10_000, 90) == 0.0\n\n    tps =\n      StatusDashboard.rolling_tps(\n        [{9_500, 10}, {9_000, 40}, {8_000, 80}],\n        10_000,\n        95\n      )\n\n    assert tps == 7.5\n  end\n\n  test \"status dashboard throttles tps updates to once per second\" do\n    {first_second, first_tps} =\n      StatusDashboard.throttled_tps(nil, nil, 10_000, [{9_000, 20}], 40)\n\n    {same_second, same_tps} =\n      StatusDashboard.throttled_tps(first_second, first_tps, 10_500, [{9_000, 20}], 200)\n\n    assert same_second == first_second\n    assert same_tps == first_tps\n\n    {next_second, next_tps} =\n      StatusDashboard.throttled_tps(same_second, same_tps, 11_000, [{10_500, 200}], 260)\n\n    assert next_second == 11\n    refute next_tps == same_tps\n  end\n\n  test \"status dashboard formats timestamps at second precision\" do\n    dt = ~U[2026-02-15 21:36:38.987654Z]\n    assert StatusDashboard.format_timestamp_for_test(dt) == \"2026-02-15 21:36:38Z\"\n  end\n\n  test \"status dashboard renders 10-minute TPS graph snapshot for steady throughput\" do\n    now_ms = 600_000\n    current_tokens = 6_000\n\n    samples =\n      for timestamp <- 575_000..0//-25_000 do\n        {timestamp, div(timestamp, 100)}\n      end\n\n    assert StatusDashboard.tps_graph_for_test(samples, now_ms, current_tokens) ==\n             \"████████████████████████\"\n  end\n\n  test \"status dashboard renders 10-minute TPS graph snapshot for ramping throughput\" do\n    now_ms = 600_000\n\n    rates_per_bucket =\n      1..24\n      |> Enum.map(&(&1 * 2))\n\n    {current_tokens, samples} = graph_samples_from_rates(rates_per_bucket)\n\n    assert StatusDashboard.tps_graph_for_test(samples, now_ms, current_tokens) ==\n             \"▁▂▂▂▃▃▃▃▄▄▄▅▅▅▆▆▆▆▇▇▇██▅\"\n  end\n\n  test \"status dashboard keeps historical TPS bars stable within the active bucket\" do\n    now_ms = 600_000\n    current_tokens = 74_400\n    next_current_tokens = current_tokens + 120\n    samples = graph_samples_for_stability_test(now_ms)\n\n    graph_at_now = StatusDashboard.tps_graph_for_test(samples, now_ms, current_tokens)\n\n    graph_next_second =\n      StatusDashboard.tps_graph_for_test(samples, now_ms + 1_000, next_current_tokens)\n\n    historical_changes =\n      graph_at_now\n      |> String.graphemes()\n      |> Enum.zip(String.graphemes(graph_next_second))\n      |> Enum.take(23)\n      |> Enum.count(fn {left, right} -> left != right end)\n\n    assert historical_changes == 0\n  end\n\n  test \"application configures a rotating file logger handler\" do\n    assert {:ok, handler_config} = :logger.get_handler_config(:symphony_disk_log)\n    assert handler_config.module == :logger_disk_log_h\n\n    disk_config = handler_config.config\n    assert disk_config.type == :wrap\n    assert is_list(disk_config.file)\n    assert disk_config.max_no_bytes > 0\n    assert disk_config.max_no_files > 0\n  end\n\n  test \"status dashboard renders last codex message in EVENT column\" do\n    row =\n      StatusDashboard.format_running_summary_for_test(%{\n        identifier: \"MT-233\",\n        state: \"running\",\n        session_id: \"thread-1234567890\",\n        codex_app_server_pid: \"4242\",\n        codex_total_tokens: 12,\n        runtime_seconds: 15,\n        last_codex_event: :notification,\n        last_codex_message: %{\n          event: :notification,\n          message: %{\n            \"method\" => \"turn/completed\",\n            \"params\" => %{\"turn\" => %{\"status\" => \"completed\"}}\n          }\n        }\n      })\n\n    plain = Regex.replace(~r/\\e\\[[\\\\d;]*m/, row, \"\")\n\n    assert plain =~ \"turn completed (completed)\"\n    assert (String.split(plain, \"turn completed (completed)\") |> length()) - 1 == 1\n    refute plain =~ \" notification \"\n  end\n\n  test \"status dashboard strips ANSI and control bytes from last codex message\" do\n    payload =\n      \"cmd: \" <>\n        <<27>> <>\n        \"[31mRED\" <>\n        <<27>> <>\n        \"[0m\" <>\n        <<0>> <>\n        \" after\\nline\"\n\n    row =\n      StatusDashboard.format_running_summary_for_test(%{\n        identifier: \"MT-898\",\n        state: \"running\",\n        session_id: \"thread-1234567890\",\n        codex_app_server_pid: \"4242\",\n        codex_total_tokens: 12,\n        runtime_seconds: 15,\n        last_codex_event: :notification,\n        last_codex_message: payload\n      })\n\n    plain = Regex.replace(~r/\\e\\[[0-9;]*m/, row, \"\")\n\n    assert plain =~ \"cmd: RED after line\"\n    refute plain =~ <<27>>\n    refute plain =~ <<0>>\n  end\n\n  test \"status dashboard expands running row to requested terminal width\" do\n    terminal_columns = 140\n\n    row =\n      StatusDashboard.format_running_summary_for_test(\n        %{\n          identifier: \"MT-598\",\n          state: \"running\",\n          session_id: \"thread-1234567890\",\n          codex_app_server_pid: \"4242\",\n          codex_total_tokens: 123,\n          runtime_seconds: 15,\n          last_codex_event: :notification,\n          last_codex_message: %{\n            event: :notification,\n            message: %{\n              \"method\" => \"turn/completed\",\n              \"params\" => %{\"turn\" => %{\"status\" => \"completed\"}}\n            }\n          }\n        },\n        terminal_columns\n      )\n\n    plain = Regex.replace(~r/\\e\\[[\\d;]*m/, row, \"\")\n\n    assert String.length(plain) == terminal_columns\n    assert plain =~ \"turn completed (completed)\"\n  end\n\n  test \"status dashboard humanizes full codex app-server event set\" do\n    event_cases = [\n      {\"turn/started\", %{\"params\" => %{\"turn\" => %{\"id\" => \"turn-1\"}}}, \"turn started\"},\n      {\"turn/completed\", %{\"params\" => %{\"turn\" => %{\"status\" => \"completed\"}}}, \"turn completed\"},\n      {\"turn/diff/updated\", %{\"params\" => %{\"diff\" => \"line1\\nline2\"}}, \"turn diff updated\"},\n      {\"turn/plan/updated\", %{\"params\" => %{\"plan\" => [%{\"step\" => \"a\"}, %{\"step\" => \"b\"}]}}, \"plan updated\"},\n      {\"thread/tokenUsage/updated\",\n       %{\n         \"params\" => %{\n           \"usage\" => %{\"input_tokens\" => 8, \"output_tokens\" => 3, \"total_tokens\" => 11}\n         }\n       }, \"thread token usage updated\"},\n      {\"item/started\",\n       %{\n         \"params\" => %{\n           \"item\" => %{\n             \"id\" => \"item-1234567890abcdef\",\n             \"type\" => \"commandExecution\",\n             \"status\" => \"running\"\n           }\n         }\n       }, \"item started: command execution\"},\n      {\"item/completed\", %{\"params\" => %{\"item\" => %{\"type\" => \"fileChange\", \"status\" => \"completed\"}}}, \"item completed: file change\"},\n      {\"item/agentMessage/delta\", %{\"params\" => %{\"delta\" => \"hello\"}}, \"agent message streaming\"},\n      {\"item/plan/delta\", %{\"params\" => %{\"delta\" => \"step\"}}, \"plan streaming\"},\n      {\"item/reasoning/summaryTextDelta\", %{\"params\" => %{\"summaryText\" => \"thinking\"}}, \"reasoning summary streaming\"},\n      {\"item/reasoning/summaryPartAdded\", %{\"params\" => %{\"summaryText\" => \"section\"}}, \"reasoning summary section added\"},\n      {\"item/reasoning/textDelta\", %{\"params\" => %{\"textDelta\" => \"reason\"}}, \"reasoning text streaming\"},\n      {\"item/commandExecution/outputDelta\", %{\"params\" => %{\"outputDelta\" => \"ok\"}}, \"command output streaming\"},\n      {\"item/fileChange/outputDelta\", %{\"params\" => %{\"outputDelta\" => \"changed\"}}, \"file change output streaming\"},\n      {\"item/commandExecution/requestApproval\", %{\"params\" => %{\"parsedCmd\" => \"git status\"}}, \"command approval requested (git status)\"},\n      {\"item/fileChange/requestApproval\", %{\"params\" => %{\"fileChangeCount\" => 2}}, \"file change approval requested (2 files)\"},\n      {\"item/tool/call\", %{\"params\" => %{\"tool\" => \"linear_graphql\"}}, \"dynamic tool call requested (linear_graphql)\"},\n      {\"item/tool/requestUserInput\", %{\"params\" => %{\"question\" => \"Continue?\"}}, \"tool requires user input: Continue?\"}\n    ]\n\n    Enum.each(event_cases, fn {method, payload, expected_fragment} ->\n      message = Map.put(payload, \"method\", method)\n\n      humanized =\n        StatusDashboard.humanize_codex_message(%{event: :notification, message: message})\n\n      assert humanized =~ expected_fragment\n    end)\n  end\n\n  test \"status dashboard humanizes dynamic tool wrapper events\" do\n    completed = %{\n      event: :tool_call_completed,\n      message: %{\n        payload: %{\"method\" => \"item/tool/call\", \"params\" => %{\"name\" => \"linear_graphql\"}}\n      }\n    }\n\n    failed = %{\n      event: :tool_call_failed,\n      message: %{\n        payload: %{\"method\" => \"item/tool/call\", \"params\" => %{\"tool\" => \"linear_graphql\"}}\n      }\n    }\n\n    unsupported = %{\n      event: :unsupported_tool_call,\n      message: %{\n        payload: %{\"method\" => \"item/tool/call\", \"params\" => %{\"tool\" => \"unknown_tool\"}}\n      }\n    }\n\n    assert StatusDashboard.humanize_codex_message(completed) =~\n             \"dynamic tool call completed (linear_graphql)\"\n\n    assert StatusDashboard.humanize_codex_message(failed) =~\n             \"dynamic tool call failed (linear_graphql)\"\n\n    assert StatusDashboard.humanize_codex_message(unsupported) =~\n             \"unsupported dynamic tool call rejected (unknown_tool)\"\n  end\n\n  test \"status dashboard unwraps nested codex payload envelopes\" do\n    wrapped = %{\n      event: :notification,\n      message: %{\n        payload: %{\n          \"method\" => \"turn/completed\",\n          \"params\" => %{\n            \"turn\" => %{\"status\" => \"completed\"},\n            \"usage\" => %{\"input_tokens\" => \"10\", \"output_tokens\" => 2, \"total_tokens\" => 12}\n          }\n        },\n        raw: \"{\\\"method\\\":\\\"turn/completed\\\"}\"\n      }\n    }\n\n    assert StatusDashboard.humanize_codex_message(wrapped) =~ \"turn completed\"\n    assert StatusDashboard.humanize_codex_message(wrapped) =~ \"in 10\"\n  end\n\n  test \"status dashboard uses shell command line as exec command status text\" do\n    message = %{\n      event: :notification,\n      message: %{\n        \"method\" => \"codex/event/exec_command_begin\",\n        \"params\" => %{\"msg\" => %{\"command\" => \"git status --short\"}}\n      }\n    }\n\n    assert StatusDashboard.humanize_codex_message(message) == \"git status --short\"\n  end\n\n  test \"status dashboard formats auto-approval updates from codex\" do\n    message = %{\n      event: :approval_auto_approved,\n      message: %{\n        payload: %{\n          \"method\" => \"item/commandExecution/requestApproval\",\n          \"params\" => %{\"parsedCmd\" => \"mix test\"}\n        },\n        decision: \"acceptForSession\"\n      }\n    }\n\n    humanized = StatusDashboard.humanize_codex_message(message)\n    assert humanized =~ \"command approval requested\"\n    assert humanized =~ \"auto-approved\"\n  end\n\n  test \"status dashboard formats auto-answered tool input updates from codex\" do\n    message = %{\n      event: :tool_input_auto_answered,\n      message: %{\n        payload: %{\n          \"method\" => \"item/tool/requestUserInput\",\n          \"params\" => %{\"question\" => \"Continue?\"}\n        },\n        answer: \"This is a non-interactive session. Operator input is unavailable.\"\n      }\n    }\n\n    humanized = StatusDashboard.humanize_codex_message(message)\n    assert humanized =~ \"tool requires user input\"\n    assert humanized =~ \"auto-answered\"\n  end\n\n  test \"status dashboard enriches wrapper reasoning and message streaming events with payload context\" do\n    reasoning_message = %{\n      event: :notification,\n      message: %{\n        \"method\" => \"codex/event/agent_reasoning\",\n        \"params\" => %{\n          \"msg\" => %{\n            \"payload\" => %{\"summaryText\" => \"compare retry paths for Linear polling\"}\n          }\n        }\n      }\n    }\n\n    message_delta = %{\n      event: :notification,\n      message: %{\n        \"method\" => \"codex/event/agent_message_delta\",\n        \"params\" => %{\n          \"msg\" => %{\n            \"payload\" => %{\"delta\" => \"writing workpad reconciliation update\"}\n          }\n        }\n      }\n    }\n\n    fallback_reasoning = %{\n      event: :notification,\n      message: %{\n        \"method\" => \"codex/event/agent_reasoning\",\n        \"params\" => %{\"msg\" => %{\"payload\" => %{}}}\n      }\n    }\n\n    assert StatusDashboard.humanize_codex_message(reasoning_message) =~\n             \"reasoning update: compare retry paths for Linear polling\"\n\n    assert StatusDashboard.humanize_codex_message(message_delta) =~\n             \"agent message streaming: writing workpad reconciliation update\"\n\n    assert StatusDashboard.humanize_codex_message(fallback_reasoning) == \"reasoning update\"\n  end\n\n  test \"application stop renders offline status\" do\n    rendered =\n      ExUnit.CaptureIO.capture_io(fn ->\n        assert :ok = SymphonyElixir.Application.stop(:normal)\n      end)\n\n    assert rendered =~ \"app_status=offline\"\n    refute rendered =~ \"Timestamp:\"\n  end\n\n  defp wait_for_snapshot(pid, predicate, timeout_ms \\\\ 200) when is_function(predicate, 1) do\n    deadline_ms = System.monotonic_time(:millisecond) + timeout_ms\n    do_wait_for_snapshot(pid, predicate, deadline_ms)\n  end\n\n  defp do_wait_for_snapshot(pid, predicate, deadline_ms) do\n    snapshot = GenServer.call(pid, :snapshot)\n\n    if predicate.(snapshot) do\n      snapshot\n    else\n      if System.monotonic_time(:millisecond) >= deadline_ms do\n        flunk(\"timed out waiting for orchestrator snapshot state: #{inspect(snapshot)}\")\n      else\n        Process.sleep(5)\n        do_wait_for_snapshot(pid, predicate, deadline_ms)\n      end\n    end\n  end\n\n  defp graph_samples_from_rates(rates_per_bucket) do\n    bucket_ms = 25_000\n\n    {timestamp, tokens, samples} =\n      Enum.reduce(rates_per_bucket, {0, 0, []}, fn rate, {timestamp, tokens, acc} ->\n        next_timestamp = timestamp + bucket_ms\n        next_tokens = tokens + trunc(rate * bucket_ms / 1000)\n        {next_timestamp, next_tokens, [{timestamp, tokens} | acc]}\n      end)\n\n    {tokens, [{timestamp, tokens} | samples]}\n  end\n\n  defp graph_samples_for_stability_test(now_ms) do\n    rates_per_bucket = Enum.map(1..24, &(&1 * 5))\n    bucket_ms = 25_000\n\n    rate_for_timestamp = fn timestamp ->\n      bucket_idx = min(div(max(timestamp, 0), bucket_ms), 23)\n      Enum.at(rates_per_bucket, bucket_idx, 0)\n    end\n\n    0..(now_ms - 1_000)//1_000\n    |> Enum.reduce({0, []}, fn timestamp, {tokens, acc} ->\n      next_tokens = tokens + rate_for_timestamp.(timestamp)\n      {next_tokens, [{timestamp, next_tokens} | acc]}\n    end)\n    |> elem(1)\n  end\nend\n"
  },
  {
    "path": "elixir/test/symphony_elixir/specs_check_test.exs",
    "content": "defmodule SymphonyElixir.SpecsCheckTest do\n  use ExUnit.Case, async: true\n\n  alias SymphonyElixir.SpecsCheck\n\n  test \"reports missing @spec for public functions\" do\n    dir = create_tmp_dir()\n\n    write_module!(dir, \"sample.ex\", \"\"\"\n    defmodule Sample do\n      def missing(arg), do: arg\n    end\n    \"\"\")\n\n    findings = SpecsCheck.missing_public_specs([dir])\n\n    assert Enum.map(findings, &SpecsCheck.finding_identifier/1) == [\"Sample.missing/1\"]\n  end\n\n  test \"accepts adjacent @spec on public function\" do\n    dir = create_tmp_dir()\n\n    write_module!(dir, \"sample.ex\", \"\"\"\n    defmodule Sample do\n      @spec ok(term()) :: term()\n      def ok(arg), do: arg\n    end\n    \"\"\")\n\n    assert SpecsCheck.missing_public_specs([dir]) == []\n  end\n\n  test \"allows defp without @spec\" do\n    dir = create_tmp_dir()\n\n    write_module!(dir, \"sample.ex\", \"\"\"\n    defmodule Sample do\n      def public do\n        helper(:ok)\n      end\n\n      defp helper(value), do: value\n    end\n    \"\"\")\n\n    findings = SpecsCheck.missing_public_specs([dir])\n\n    assert Enum.map(findings, &SpecsCheck.finding_identifier/1) == [\"Sample.public/0\"]\n  end\n\n  test \"exempts callback implementations marked with @impl\" do\n    dir = create_tmp_dir()\n\n    write_module!(dir, \"worker.ex\", \"\"\"\n    defmodule Worker do\n      @behaviour GenServer\n\n      @impl true\n      def init(state), do: {:ok, state}\n    end\n    \"\"\")\n\n    assert SpecsCheck.missing_public_specs([dir]) == []\n  end\n\n  test \"honors explicit exemptions list\" do\n    dir = create_tmp_dir()\n\n    write_module!(dir, \"sample.ex\", \"\"\"\n    defmodule Sample do\n      def legacy(arg), do: arg\n    end\n    \"\"\")\n\n    findings = SpecsCheck.missing_public_specs([dir], exemptions: [\"Sample.legacy/1\"])\n\n    assert findings == []\n  end\n\n  defp create_tmp_dir do\n    unique = :erlang.unique_integer([:positive, :monotonic])\n    dir = Path.join(System.tmp_dir!(), \"specs-check-test-#{unique}\")\n    File.rm_rf!(dir)\n    File.mkdir_p!(dir)\n    dir\n  end\n\n  defp write_module!(dir, rel_path, source) do\n    path = Path.join(dir, rel_path)\n    File.write!(path, source)\n  end\nend\n"
  },
  {
    "path": "elixir/test/symphony_elixir/ssh_test.exs",
    "content": "defmodule SymphonyElixir.SSHTest do\n  use ExUnit.Case, async: false\n\n  alias SymphonyElixir.SSH\n\n  test \"run/3 keeps bracketed IPv6 host:port targets intact\" do\n    test_root = Path.join(System.tmp_dir!(), \"symphony-ssh-ipv6-test-#{System.unique_integer([:positive])}\")\n    trace_file = Path.join(test_root, \"ssh.trace\")\n    previous_path = System.get_env(\"PATH\")\n\n    on_exit(fn ->\n      restore_env(\"PATH\", previous_path)\n      File.rm_rf(test_root)\n    end)\n\n    install_fake_ssh!(test_root, trace_file)\n\n    assert {:ok, {\"\", 0}} =\n             SSH.run(\"root@[::1]:2200\", \"printf ok\", stderr_to_stdout: true)\n\n    trace = File.read!(trace_file)\n    assert trace =~ \"-T -p 2200 root@[::1] bash -lc\"\n    assert trace =~ \"printf ok\"\n  end\n\n  test \"run/3 leaves unbracketed IPv6-style targets unchanged\" do\n    test_root = Path.join(System.tmp_dir!(), \"symphony-ssh-ipv6-raw-test-#{System.unique_integer([:positive])}\")\n    trace_file = Path.join(test_root, \"ssh.trace\")\n    previous_path = System.get_env(\"PATH\")\n\n    on_exit(fn ->\n      restore_env(\"PATH\", previous_path)\n      File.rm_rf(test_root)\n    end)\n\n    install_fake_ssh!(test_root, trace_file)\n\n    assert {:ok, {\"\", 0}} =\n             SSH.run(\"::1:2200\", \"printf ok\", stderr_to_stdout: true)\n\n    trace = File.read!(trace_file)\n    assert trace =~ \"-T ::1:2200 bash -lc\"\n    refute trace =~ \"-p 2200\"\n  end\n\n  test \"run/3 passes host:port targets through ssh -p\" do\n    test_root = Path.join(System.tmp_dir!(), \"symphony-ssh-test-#{System.unique_integer([:positive])}\")\n    trace_file = Path.join(test_root, \"ssh.trace\")\n    previous_path = System.get_env(\"PATH\")\n    previous_ssh_config = System.get_env(\"SYMPHONY_SSH_CONFIG\")\n\n    on_exit(fn ->\n      restore_env(\"PATH\", previous_path)\n      restore_env(\"SYMPHONY_SSH_CONFIG\", previous_ssh_config)\n      File.rm_rf(test_root)\n    end)\n\n    install_fake_ssh!(test_root, trace_file)\n    System.put_env(\"SYMPHONY_SSH_CONFIG\", \"/tmp/symphony-test-ssh-config\")\n\n    assert {:ok, {\"\", 0}} =\n             SSH.run(\"localhost:2222\", \"echo ready\", stderr_to_stdout: true)\n\n    trace = File.read!(trace_file)\n    assert trace =~ \"-F /tmp/symphony-test-ssh-config\"\n    assert trace =~ \"-T -p 2222 localhost bash -lc\"\n    assert trace =~ \"echo ready\"\n  end\n\n  test \"run/3 keeps the user prefix when parsing user@host:port targets\" do\n    test_root = Path.join(System.tmp_dir!(), \"symphony-ssh-user-test-#{System.unique_integer([:positive])}\")\n    trace_file = Path.join(test_root, \"ssh.trace\")\n    previous_path = System.get_env(\"PATH\")\n\n    on_exit(fn ->\n      restore_env(\"PATH\", previous_path)\n      File.rm_rf(test_root)\n    end)\n\n    install_fake_ssh!(test_root, trace_file)\n\n    assert {:ok, {\"\", 0}} =\n             SSH.run(\"root@127.0.0.1:2200\", \"printf ok\", stderr_to_stdout: true)\n\n    trace = File.read!(trace_file)\n    assert trace =~ \"-T -p 2200 root@127.0.0.1 bash -lc\"\n    assert trace =~ \"printf ok\"\n  end\n\n  test \"run/3 returns an error when ssh is unavailable\" do\n    test_root = Path.join(System.tmp_dir!(), \"symphony-ssh-missing-test-#{System.unique_integer([:positive])}\")\n    previous_path = System.get_env(\"PATH\")\n\n    on_exit(fn ->\n      restore_env(\"PATH\", previous_path)\n      File.rm_rf(test_root)\n    end)\n\n    File.mkdir_p!(test_root)\n    System.put_env(\"PATH\", test_root)\n\n    assert {:error, :ssh_not_found} = SSH.run(\"localhost\", \"printf ok\")\n  end\n\n  test \"start_port/3 supports binary output without line mode\" do\n    test_root = Path.join(System.tmp_dir!(), \"symphony-ssh-port-test-#{System.unique_integer([:positive])}\")\n    trace_file = Path.join(test_root, \"ssh.trace\")\n    previous_path = System.get_env(\"PATH\")\n    previous_ssh_config = System.get_env(\"SYMPHONY_SSH_CONFIG\")\n\n    on_exit(fn ->\n      restore_env(\"PATH\", previous_path)\n      restore_env(\"SYMPHONY_SSH_CONFIG\", previous_ssh_config)\n      File.rm_rf(test_root)\n    end)\n\n    install_fake_ssh!(test_root, trace_file, \"\"\"\n    #!/bin/sh\n    printf 'ARGV:%s\\\\n' \"$*\" >> \"#{trace_file}\"\n    printf 'ready\\\\n'\n    exit 0\n    \"\"\")\n\n    System.delete_env(\"SYMPHONY_SSH_CONFIG\")\n\n    assert {:ok, port} = SSH.start_port(\"localhost\", \"printf ok\")\n    assert is_port(port)\n    wait_for_trace!(trace_file)\n\n    trace = File.read!(trace_file)\n    assert trace =~ \"-T localhost bash -lc\"\n    refute trace =~ \" -F \"\n  end\n\n  test \"start_port/3 supports line mode\" do\n    test_root = Path.join(System.tmp_dir!(), \"symphony-ssh-line-port-test-#{System.unique_integer([:positive])}\")\n    trace_file = Path.join(test_root, \"ssh.trace\")\n    previous_path = System.get_env(\"PATH\")\n\n    on_exit(fn ->\n      restore_env(\"PATH\", previous_path)\n      File.rm_rf(test_root)\n    end)\n\n    install_fake_ssh!(test_root, trace_file, \"\"\"\n    #!/bin/sh\n    printf 'ARGV:%s\\\\n' \"$*\" >> \"#{trace_file}\"\n    printf 'ready\\\\n'\n    exit 0\n    \"\"\")\n\n    assert {:ok, port} = SSH.start_port(\"localhost:2222\", \"printf ok\", line: 256)\n    assert is_port(port)\n    wait_for_trace!(trace_file)\n\n    trace = File.read!(trace_file)\n    assert trace =~ \"-T -p 2222 localhost bash -lc\"\n  end\n\n  test \"remote_shell_command/1 escapes embedded single quotes\" do\n    assert SSH.remote_shell_command(\"printf 'hello'\") ==\n             \"bash -lc 'printf '\\\"'\\\"'hello'\\\"'\\\"''\"\n  end\n\n  defp install_fake_ssh!(test_root, trace_file, script \\\\ nil) do\n    fake_bin_dir = Path.join(test_root, \"bin\")\n    fake_ssh = Path.join(fake_bin_dir, \"ssh\")\n\n    File.mkdir_p!(fake_bin_dir)\n\n    File.write!(\n      fake_ssh,\n      script ||\n        \"\"\"\n        #!/bin/sh\n        printf 'ARGV:%s\\\\n' \"$*\" >> \"#{trace_file}\"\n        exit 0\n        \"\"\"\n    )\n\n    File.chmod!(fake_ssh, 0o755)\n    System.put_env(\"PATH\", fake_bin_dir <> \":\" <> (System.get_env(\"PATH\") || \"\"))\n  end\n\n  defp wait_for_trace!(trace_file, attempts \\\\ 20)\n  defp wait_for_trace!(trace_file, 0), do: flunk(\"timed out waiting for fake ssh trace at #{trace_file}\")\n\n  defp wait_for_trace!(trace_file, attempts) do\n    if File.exists?(trace_file) and File.read!(trace_file) != \"\" do\n      :ok\n    else\n      Process.sleep(25)\n      wait_for_trace!(trace_file, attempts - 1)\n    end\n  end\n\n  defp restore_env(key, nil), do: System.delete_env(key)\n  defp restore_env(key, value), do: System.put_env(key, value)\nend\n"
  },
  {
    "path": "elixir/test/symphony_elixir/status_dashboard_snapshot_test.exs",
    "content": "defmodule SymphonyElixir.StatusDashboardSnapshotTest do\n  use SymphonyElixir.TestSupport\n\n  alias SymphonyElixir.TestSupport.Snapshot\n\n  @terminal_columns 115\n\n  test \"snapshot fixture: idle dashboard\" do\n    snapshot_data =\n      {:ok,\n       %{\n         running: [],\n         retrying: [],\n         codex_totals: %{input_tokens: 0, output_tokens: 0, total_tokens: 0, seconds_running: 0},\n         rate_limits: nil\n       }}\n\n    Snapshot.assert_dashboard_snapshot!(\"idle\", render_snapshot(snapshot_data, 0.0))\n  end\n\n  test \"snapshot fixture: idle dashboard with observability url\" do\n    previous_port_override = Application.get_env(:symphony_elixir, :server_port_override)\n\n    on_exit(fn ->\n      if is_nil(previous_port_override) do\n        Application.delete_env(:symphony_elixir, :server_port_override)\n      else\n        Application.put_env(:symphony_elixir, :server_port_override, previous_port_override)\n      end\n    end)\n\n    Application.put_env(:symphony_elixir, :server_port_override, 4000)\n\n    snapshot_data =\n      {:ok,\n       %{\n         running: [],\n         retrying: [],\n         codex_totals: %{input_tokens: 0, output_tokens: 0, total_tokens: 0, seconds_running: 0},\n         rate_limits: nil\n       }}\n\n    Snapshot.assert_dashboard_snapshot!(\"idle_with_dashboard_url\", render_snapshot(snapshot_data, 0.0))\n  end\n\n  test \"snapshot fixture: super busy dashboard\" do\n    snapshot_data =\n      {:ok,\n       %{\n         running: [\n           running_entry(%{\n             identifier: \"MT-101\",\n             codex_total_tokens: 120_450,\n             runtime_seconds: 785,\n             turn_count: 11,\n             last_codex_event: \"turn_completed\",\n             last_codex_message: turn_completed_message(\"completed\")\n           }),\n           running_entry(%{\n             identifier: \"MT-102\",\n             session_id: \"thread-abcdef1234567890\",\n             codex_app_server_pid: \"5252\",\n             codex_total_tokens: 89_200,\n             runtime_seconds: 412,\n             turn_count: 4,\n             last_codex_event: \"codex/event/task_started\",\n             last_codex_message: exec_command_message(\"mix test --cover\")\n           })\n         ],\n         retrying: [],\n         codex_totals: %{\n           input_tokens: 250_000,\n           output_tokens: 18_500,\n           total_tokens: 268_500,\n           seconds_running: 4_321\n         },\n         rate_limits: %{\n           limit_id: \"gpt-5\",\n           primary: %{remaining: 12_345, limit: 20_000, reset_in_seconds: 30},\n           secondary: %{remaining: 45, limit: 60, reset_in_seconds: 12},\n           credits: %{has_credits: true, balance: 9_876.5}\n         }\n       }}\n\n    Snapshot.assert_dashboard_snapshot!(\"super_busy\", render_snapshot(snapshot_data, 1_842.7))\n  end\n\n  test \"snapshot fixture: backoff queue pressure\" do\n    snapshot_data =\n      {:ok,\n       %{\n         running: [\n           running_entry(%{\n             identifier: \"MT-638\",\n             state: \"retrying\",\n             codex_total_tokens: 14_200,\n             runtime_seconds: 1_225,\n             turn_count: 7,\n             last_codex_event: :notification,\n             last_codex_message: agent_message_delta(\"waiting on rate-limit backoff window\")\n           })\n         ],\n         retrying: [\n           retry_entry(%{\n             identifier: \"MT-450\",\n             attempt: 4,\n             due_in_ms: 1_250,\n             error: \"rate limit exhausted\"\n           }),\n           retry_entry(%{\n             identifier: \"MT-451\",\n             attempt: 2,\n             due_in_ms: 3_900,\n             error: \"retrying after API timeout with jitter\"\n           }),\n           retry_entry(%{\n             identifier: \"MT-452\",\n             attempt: 6,\n             due_in_ms: 8_100,\n             error: \"worker crashed\\nrestarting cleanly\"\n           }),\n           retry_entry(%{\n             identifier: \"MT-453\",\n             attempt: 1,\n             due_in_ms: 11_000,\n             error: \"fourth queued retry should also render after removing the top-three limit\"\n           })\n         ],\n         codex_totals: %{input_tokens: 18_000, output_tokens: 2_200, total_tokens: 20_200, seconds_running: 2_700},\n         rate_limits: %{\n           limit_id: \"gpt-5\",\n           primary: %{remaining: 0, limit: 20_000, reset_in_seconds: 95},\n           secondary: %{remaining: 0, limit: 60, reset_in_seconds: 45},\n           credits: %{has_credits: false}\n         }\n       }}\n\n    Snapshot.assert_dashboard_snapshot!(\"backoff_queue\", render_snapshot(snapshot_data, 15.4))\n  end\n\n  test \"backoff queue row escapes escaped newline sequences\" do\n    snapshot_data =\n      {:ok,\n       %{\n         running: [],\n         retrying: [\n           retry_entry(%{\n             identifier: \"MT-980\",\n             attempt: 1,\n             due_in_ms: 1_500,\n             error: \"error with \\\\nnewline\"\n           })\n         ],\n         codex_totals: %{input_tokens: 0, output_tokens: 0, total_tokens: 0, seconds_running: 0},\n         rate_limits: nil\n       }}\n\n    rendered = render_snapshot(snapshot_data, 0.0)\n    backoff_lines = rendered |> String.split(\"\\n\") |> Enum.filter(&String.contains?(&1, \"MT-980\"))\n\n    assert length(backoff_lines) == 1\n\n    [backoff_line] = backoff_lines\n\n    assert backoff_line =~ \"error=error with newline\"\n    refute backoff_line =~ \"\\\\n\"\n  end\n\n  test \"snapshot fixture: unlimited credits variant\" do\n    snapshot_data =\n      {:ok,\n       %{\n         running: [\n           running_entry(%{\n             identifier: \"MT-777\",\n             state: \"running\",\n             codex_total_tokens: 3_200,\n             runtime_seconds: 75,\n             turn_count: 7,\n             last_codex_event: \"codex/event/token_count\",\n             last_codex_message: token_usage_message(90, 12, 102)\n           })\n         ],\n         retrying: [],\n         codex_totals: %{input_tokens: 90, output_tokens: 12, total_tokens: 102, seconds_running: 75},\n         rate_limits: %{\n           limit_id: \"priority-tier\",\n           primary: %{remaining: 100, limit: 100, reset_in_seconds: 1},\n           secondary: %{remaining: 500, limit: 500, reset_in_seconds: 1},\n           credits: %{unlimited: true}\n         }\n       }}\n\n    Snapshot.assert_dashboard_snapshot!(\"credits_unlimited\", render_snapshot(snapshot_data, 42.0))\n  end\n\n  defp render_snapshot(snapshot_data, tps) do\n    StatusDashboard.format_snapshot_content_for_test(snapshot_data, tps, @terminal_columns)\n  end\n\n  defp running_entry(overrides) do\n    Map.merge(\n      %{\n        identifier: \"MT-000\",\n        state: \"running\",\n        session_id: \"thread-1234567890\",\n        codex_app_server_pid: \"4242\",\n        codex_total_tokens: 0,\n        runtime_seconds: 0,\n        turn_count: 1,\n        last_codex_event: :notification,\n        last_codex_message: turn_started_message()\n      },\n      overrides\n    )\n  end\n\n  defp retry_entry(overrides) do\n    Map.merge(\n      %{\n        issue_id: \"issue-1\",\n        identifier: \"MT-000\",\n        attempt: 1,\n        due_in_ms: 1_000,\n        error: \"retry scheduled\"\n      },\n      overrides\n    )\n  end\n\n  defp turn_started_message do\n    %{\n      event: :notification,\n      message: %{\n        \"method\" => \"turn/started\",\n        \"params\" => %{\"turn\" => %{\"id\" => \"turn-1\"}}\n      }\n    }\n  end\n\n  defp turn_completed_message(status) do\n    %{\n      event: :notification,\n      message: %{\n        \"method\" => \"turn/completed\",\n        \"params\" => %{\"turn\" => %{\"status\" => status}}\n      }\n    }\n  end\n\n  defp exec_command_message(command) do\n    %{\n      event: :notification,\n      message: %{\n        \"method\" => \"codex/event/exec_command_begin\",\n        \"params\" => %{\"msg\" => %{\"command\" => command}}\n      }\n    }\n  end\n\n  defp agent_message_delta(delta) do\n    %{\n      event: :notification,\n      message: %{\n        \"method\" => \"codex/event/agent_message_delta\",\n        \"params\" => %{\"msg\" => %{\"payload\" => %{\"delta\" => delta}}}\n      }\n    }\n  end\n\n  defp token_usage_message(input_tokens, output_tokens, total_tokens) do\n    %{\n      event: :notification,\n      message: %{\n        \"method\" => \"thread/tokenUsage/updated\",\n        \"params\" => %{\n          \"tokenUsage\" => %{\n            \"total\" => %{\n              \"inputTokens\" => input_tokens,\n              \"outputTokens\" => output_tokens,\n              \"totalTokens\" => total_tokens\n            }\n          }\n        }\n      }\n    }\n  end\nend\n"
  },
  {
    "path": "elixir/test/symphony_elixir/workspace_and_config_test.exs",
    "content": "defmodule SymphonyElixir.WorkspaceAndConfigTest do\n  use SymphonyElixir.TestSupport\n  alias Ecto.Changeset\n  alias SymphonyElixir.Config.Schema\n  alias SymphonyElixir.Config.Schema.{Codex, StringOrMap}\n  alias SymphonyElixir.Linear.Client\n\n  test \"workspace bootstrap can be implemented in after_create hook\" do\n    test_root =\n      Path.join(\n        System.tmp_dir!(),\n        \"symphony-elixir-workspace-hook-bootstrap-#{System.unique_integer([:positive])}\"\n      )\n\n    try do\n      template_repo = Path.join(test_root, \"source\")\n      workspace_root = Path.join(test_root, \"workspaces\")\n\n      File.mkdir_p!(template_repo)\n      File.mkdir_p!(Path.join(template_repo, \"keep\"))\n      File.write!(Path.join([template_repo, \"keep\", \"file.txt\"]), \"keep me\")\n      File.write!(Path.join(template_repo, \"README.md\"), \"hook clone\\n\")\n      System.cmd(\"git\", [\"-C\", template_repo, \"init\", \"-b\", \"main\"])\n      System.cmd(\"git\", [\"-C\", template_repo, \"config\", \"user.name\", \"Test User\"])\n      System.cmd(\"git\", [\"-C\", template_repo, \"config\", \"user.email\", \"test@example.com\"])\n      System.cmd(\"git\", [\"-C\", template_repo, \"add\", \"README.md\", \"keep/file.txt\"])\n      System.cmd(\"git\", [\"-C\", template_repo, \"commit\", \"-m\", \"initial\"])\n\n      write_workflow_file!(Workflow.workflow_file_path(),\n        workspace_root: workspace_root,\n        hook_after_create: \"git clone --depth 1 #{template_repo} .\"\n      )\n\n      assert {:ok, workspace} = Workspace.create_for_issue(\"S-1\")\n      assert File.exists?(Path.join(workspace, \".git\"))\n      assert File.read!(Path.join(workspace, \"README.md\")) == \"hook clone\\n\"\n      assert File.read!(Path.join([workspace, \"keep\", \"file.txt\"])) == \"keep me\"\n    after\n      File.rm_rf(test_root)\n    end\n  end\n\n  test \"workspace path is deterministic per issue identifier\" do\n    workspace_root =\n      Path.join(\n        System.tmp_dir!(),\n        \"symphony-elixir-workspace-deterministic-#{System.unique_integer([:positive])}\"\n      )\n\n    write_workflow_file!(Workflow.workflow_file_path(), workspace_root: workspace_root)\n\n    assert {:ok, first_workspace} = Workspace.create_for_issue(\"MT/Det\")\n    assert {:ok, second_workspace} = Workspace.create_for_issue(\"MT/Det\")\n\n    assert first_workspace == second_workspace\n    assert Path.basename(first_workspace) == \"MT_Det\"\n  end\n\n  test \"workspace reuses existing issue directory without deleting local changes\" do\n    workspace_root =\n      Path.join(\n        System.tmp_dir!(),\n        \"symphony-elixir-workspace-reuse-#{System.unique_integer([:positive])}\"\n      )\n\n    try do\n      write_workflow_file!(Workflow.workflow_file_path(),\n        workspace_root: workspace_root,\n        hook_after_create: \"echo first > README.md\"\n      )\n\n      assert {:ok, first_workspace} = Workspace.create_for_issue(\"MT-REUSE\")\n\n      File.write!(Path.join(first_workspace, \"README.md\"), \"changed\\n\")\n      File.write!(Path.join(first_workspace, \"local-progress.txt\"), \"in progress\\n\")\n      File.mkdir_p!(Path.join(first_workspace, \"deps\"))\n      File.mkdir_p!(Path.join(first_workspace, \"_build\"))\n      File.mkdir_p!(Path.join(first_workspace, \"tmp\"))\n      File.write!(Path.join([first_workspace, \"deps\", \"cache.txt\"]), \"cached deps\\n\")\n      File.write!(Path.join([first_workspace, \"_build\", \"artifact.txt\"]), \"compiled artifact\\n\")\n      File.write!(Path.join([first_workspace, \"tmp\", \"scratch.txt\"]), \"remove me\\n\")\n\n      assert {:ok, second_workspace} = Workspace.create_for_issue(\"MT-REUSE\")\n      assert second_workspace == first_workspace\n      assert File.read!(Path.join(second_workspace, \"README.md\")) == \"changed\\n\"\n      assert File.read!(Path.join(second_workspace, \"local-progress.txt\")) == \"in progress\\n\"\n      assert File.read!(Path.join([second_workspace, \"deps\", \"cache.txt\"])) == \"cached deps\\n\"\n      assert File.read!(Path.join([second_workspace, \"_build\", \"artifact.txt\"])) == \"compiled artifact\\n\"\n      assert File.read!(Path.join([second_workspace, \"tmp\", \"scratch.txt\"])) == \"remove me\\n\"\n    after\n      File.rm_rf(workspace_root)\n    end\n  end\n\n  test \"workspace replaces stale non-directory paths\" do\n    workspace_root =\n      Path.join(\n        System.tmp_dir!(),\n        \"symphony-elixir-workspace-stale-path-#{System.unique_integer([:positive])}\"\n      )\n\n    try do\n      stale_workspace = Path.join(workspace_root, \"MT-STALE\")\n      File.mkdir_p!(workspace_root)\n      File.write!(stale_workspace, \"old state\\n\")\n\n      write_workflow_file!(Workflow.workflow_file_path(), workspace_root: workspace_root)\n\n      assert {:ok, canonical_workspace} = SymphonyElixir.PathSafety.canonicalize(stale_workspace)\n      assert {:ok, workspace} = Workspace.create_for_issue(\"MT-STALE\")\n      assert workspace == canonical_workspace\n      assert File.dir?(workspace)\n    after\n      File.rm_rf(workspace_root)\n    end\n  end\n\n  test \"workspace rejects symlink escapes under the configured root\" do\n    test_root =\n      Path.join(\n        System.tmp_dir!(),\n        \"symphony-elixir-workspace-symlink-#{System.unique_integer([:positive])}\"\n      )\n\n    try do\n      workspace_root = Path.join(test_root, \"workspaces\")\n      outside_root = Path.join(test_root, \"outside\")\n      symlink_path = Path.join(workspace_root, \"MT-SYM\")\n\n      File.mkdir_p!(workspace_root)\n      File.mkdir_p!(outside_root)\n      File.ln_s!(outside_root, symlink_path)\n\n      write_workflow_file!(Workflow.workflow_file_path(), workspace_root: workspace_root)\n\n      assert {:ok, canonical_outside_root} = SymphonyElixir.PathSafety.canonicalize(outside_root)\n      assert {:ok, canonical_workspace_root} = SymphonyElixir.PathSafety.canonicalize(workspace_root)\n\n      assert {:error, {:workspace_outside_root, ^canonical_outside_root, ^canonical_workspace_root}} =\n               Workspace.create_for_issue(\"MT-SYM\")\n    after\n      File.rm_rf(test_root)\n    end\n  end\n\n  test \"workspace canonicalizes symlinked workspace roots before creating issue directories\" do\n    test_root =\n      Path.join(\n        System.tmp_dir!(),\n        \"symphony-elixir-workspace-root-symlink-#{System.unique_integer([:positive])}\"\n      )\n\n    try do\n      actual_root = Path.join(test_root, \"actual-workspaces\")\n      linked_root = Path.join(test_root, \"linked-workspaces\")\n\n      File.mkdir_p!(actual_root)\n      File.ln_s!(actual_root, linked_root)\n\n      write_workflow_file!(Workflow.workflow_file_path(), workspace_root: linked_root)\n\n      assert {:ok, canonical_workspace} =\n               SymphonyElixir.PathSafety.canonicalize(Path.join(actual_root, \"MT-LINK\"))\n\n      assert {:ok, workspace} = Workspace.create_for_issue(\"MT-LINK\")\n      assert workspace == canonical_workspace\n      assert File.dir?(workspace)\n    after\n      File.rm_rf(test_root)\n    end\n  end\n\n  test \"workspace remove rejects the workspace root itself with a distinct error\" do\n    workspace_root =\n      Path.join(\n        System.tmp_dir!(),\n        \"symphony-elixir-workspace-root-remove-#{System.unique_integer([:positive])}\"\n      )\n\n    try do\n      File.mkdir_p!(workspace_root)\n      write_workflow_file!(Workflow.workflow_file_path(), workspace_root: workspace_root)\n\n      assert {:ok, canonical_workspace_root} =\n               SymphonyElixir.PathSafety.canonicalize(workspace_root)\n\n      assert {:error, {:workspace_equals_root, ^canonical_workspace_root, ^canonical_workspace_root}, \"\"} =\n               Workspace.remove(workspace_root)\n    after\n      File.rm_rf(workspace_root)\n    end\n  end\n\n  test \"workspace surfaces after_create hook failures\" do\n    workspace_root =\n      Path.join(\n        System.tmp_dir!(),\n        \"symphony-elixir-workspace-hook-failure-#{System.unique_integer([:positive])}\"\n      )\n\n    try do\n      write_workflow_file!(Workflow.workflow_file_path(),\n        workspace_root: workspace_root,\n        hook_after_create: \"echo nope && exit 17\"\n      )\n\n      assert {:error, {:workspace_hook_failed, \"after_create\", 17, _output}} =\n               Workspace.create_for_issue(\"MT-FAIL\")\n    after\n      File.rm_rf(workspace_root)\n    end\n  end\n\n  test \"workspace surfaces after_create hook timeouts\" do\n    workspace_root =\n      Path.join(\n        System.tmp_dir!(),\n        \"symphony-elixir-workspace-hook-timeout-#{System.unique_integer([:positive])}\"\n      )\n\n    try do\n      write_workflow_file!(Workflow.workflow_file_path(),\n        workspace_root: workspace_root,\n        hook_timeout_ms: 10,\n        hook_after_create: \"sleep 1\"\n      )\n\n      assert {:error, {:workspace_hook_timeout, \"after_create\", 10}} =\n               Workspace.create_for_issue(\"MT-TIMEOUT\")\n    after\n      File.rm_rf(workspace_root)\n    end\n  end\n\n  test \"workspace creates an empty directory when no bootstrap hook is configured\" do\n    workspace_root =\n      Path.join(\n        System.tmp_dir!(),\n        \"symphony-workspace-empty-#{System.unique_integer([:positive])}\"\n      )\n\n    try do\n      write_workflow_file!(Workflow.workflow_file_path(), workspace_root: workspace_root)\n\n      workspace = Path.join(workspace_root, \"MT-608\")\n      assert {:ok, canonical_workspace} = SymphonyElixir.PathSafety.canonicalize(workspace)\n\n      assert {:ok, ^canonical_workspace} = Workspace.create_for_issue(\"MT-608\")\n      assert File.dir?(workspace)\n      assert {:ok, []} = File.ls(workspace)\n    after\n      File.rm_rf(workspace_root)\n    end\n  end\n\n  test \"workspace removes all workspaces for a closed issue identifier\" do\n    workspace_root =\n      Path.join(\n        System.tmp_dir!(),\n        \"symphony-elixir-issue-workspace-cleanup-#{System.unique_integer([:positive])}\"\n      )\n\n    try do\n      target_workspace = Path.join(workspace_root, \"S_1\")\n      untouched_workspace = Path.join(workspace_root, \"OTHER-#{System.unique_integer([:positive])}\")\n\n      File.mkdir_p!(target_workspace)\n      File.mkdir_p!(untouched_workspace)\n      File.write!(Path.join(target_workspace, \"marker.txt\"), \"stale\")\n      File.write!(Path.join(untouched_workspace, \"marker.txt\"), \"keep\")\n\n      write_workflow_file!(Workflow.workflow_file_path(), workspace_root: workspace_root)\n\n      assert :ok = Workspace.remove_issue_workspaces(\"S_1\")\n      refute File.exists?(target_workspace)\n      assert File.exists?(untouched_workspace)\n    after\n      File.rm_rf(workspace_root)\n    end\n  end\n\n  test \"workspace cleanup handles missing workspace root\" do\n    missing_root =\n      Path.join(\n        System.tmp_dir!(),\n        \"symphony-elixir-missing-workspaces-#{System.unique_integer([:positive])}\"\n      )\n\n    write_workflow_file!(Workflow.workflow_file_path(), workspace_root: missing_root)\n\n    assert :ok = Workspace.remove_issue_workspaces(\"S-2\")\n  end\n\n  test \"workspace cleanup ignores non-binary identifier\" do\n    assert :ok = Workspace.remove_issue_workspaces(nil)\n  end\n\n  test \"linear issue helpers\" do\n    issue = %Issue{\n      id: \"abc\",\n      labels: [\"frontend\", \"infra\"],\n      assigned_to_worker: false\n    }\n\n    assert Issue.label_names(issue) == [\"frontend\", \"infra\"]\n    assert issue.labels == [\"frontend\", \"infra\"]\n    refute issue.assigned_to_worker\n  end\n\n  test \"linear client normalizes blockers from inverse relations\" do\n    raw_issue = %{\n      \"id\" => \"issue-1\",\n      \"identifier\" => \"MT-1\",\n      \"title\" => \"Blocked todo\",\n      \"description\" => \"Needs dependency\",\n      \"priority\" => 2,\n      \"state\" => %{\"name\" => \"Todo\"},\n      \"branchName\" => \"mt-1\",\n      \"url\" => \"https://example.org/issues/MT-1\",\n      \"assignee\" => %{\n        \"id\" => \"user-1\"\n      },\n      \"labels\" => %{\"nodes\" => [%{\"name\" => \"Backend\"}]},\n      \"inverseRelations\" => %{\n        \"nodes\" => [\n          %{\n            \"type\" => \"blocks\",\n            \"issue\" => %{\n              \"id\" => \"issue-2\",\n              \"identifier\" => \"MT-2\",\n              \"state\" => %{\"name\" => \"In Progress\"}\n            }\n          },\n          %{\n            \"type\" => \"relatesTo\",\n            \"issue\" => %{\n              \"id\" => \"issue-3\",\n              \"identifier\" => \"MT-3\",\n              \"state\" => %{\"name\" => \"Done\"}\n            }\n          }\n        ]\n      },\n      \"createdAt\" => \"2026-01-01T00:00:00Z\",\n      \"updatedAt\" => \"2026-01-02T00:00:00Z\"\n    }\n\n    issue = Client.normalize_issue_for_test(raw_issue, \"user-1\")\n\n    assert issue.blocked_by == [%{id: \"issue-2\", identifier: \"MT-2\", state: \"In Progress\"}]\n    assert issue.labels == [\"backend\"]\n    assert issue.priority == 2\n    assert issue.state == \"Todo\"\n    assert issue.assignee_id == \"user-1\"\n    assert issue.assigned_to_worker\n  end\n\n  test \"linear client marks explicitly unassigned issues as not routed to worker\" do\n    raw_issue = %{\n      \"id\" => \"issue-99\",\n      \"identifier\" => \"MT-99\",\n      \"title\" => \"Someone else's task\",\n      \"state\" => %{\"name\" => \"Todo\"},\n      \"assignee\" => %{\n        \"id\" => \"user-2\"\n      }\n    }\n\n    issue = Client.normalize_issue_for_test(raw_issue, \"user-1\")\n\n    refute issue.assigned_to_worker\n  end\n\n  test \"linear client pagination merge helper preserves issue ordering\" do\n    issue_page_1 = [\n      %Issue{id: \"issue-1\", identifier: \"MT-1\"},\n      %Issue{id: \"issue-2\", identifier: \"MT-2\"}\n    ]\n\n    issue_page_2 = [\n      %Issue{id: \"issue-3\", identifier: \"MT-3\"}\n    ]\n\n    merged = Client.merge_issue_pages_for_test([issue_page_1, issue_page_2])\n\n    assert Enum.map(merged, & &1.identifier) == [\"MT-1\", \"MT-2\", \"MT-3\"]\n  end\n\n  test \"linear client paginates issue state fetches by id beyond one page\" do\n    issue_ids = Enum.map(1..55, &\"issue-#{&1}\")\n    first_batch_ids = Enum.take(issue_ids, 50)\n    second_batch_ids = Enum.drop(issue_ids, 50)\n\n    raw_issue = fn issue_id ->\n      suffix = String.replace_prefix(issue_id, \"issue-\", \"\")\n\n      %{\n        \"id\" => issue_id,\n        \"identifier\" => \"MT-#{suffix}\",\n        \"title\" => \"Issue #{suffix}\",\n        \"description\" => \"Description #{suffix}\",\n        \"state\" => %{\"name\" => \"In Progress\"},\n        \"labels\" => %{\"nodes\" => []},\n        \"inverseRelations\" => %{\"nodes\" => []}\n      }\n    end\n\n    graphql_fun = fn query, variables ->\n      send(self(), {:fetch_issue_states_page, query, variables})\n\n      body = %{\n        \"data\" => %{\n          \"issues\" => %{\n            \"nodes\" => Enum.map(variables.ids, raw_issue)\n          }\n        }\n      }\n\n      {:ok, body}\n    end\n\n    assert {:ok, issues} = Client.fetch_issue_states_by_ids_for_test(issue_ids, graphql_fun)\n\n    assert Enum.map(issues, & &1.id) == issue_ids\n\n    assert_receive {:fetch_issue_states_page, query, %{ids: ^first_batch_ids, first: 50, relationFirst: 50}}\n    assert query =~ \"SymphonyLinearIssuesById\"\n\n    assert_receive {:fetch_issue_states_page, ^query, %{ids: ^second_batch_ids, first: 5, relationFirst: 50}}\n  end\n\n  test \"linear client logs response bodies for non-200 graphql responses\" do\n    log =\n      ExUnit.CaptureLog.capture_log(fn ->\n        assert {:error, {:linear_api_status, 400}} =\n                 Client.graphql(\n                   \"query Viewer { viewer { id } }\",\n                   %{},\n                   request_fun: fn _payload, _headers ->\n                     {:ok,\n                      %{\n                        status: 400,\n                        body: %{\n                          \"errors\" => [\n                            %{\n                              \"message\" => \"Variable \\\"$ids\\\" got invalid value\",\n                              \"extensions\" => %{\"code\" => \"BAD_USER_INPUT\"}\n                            }\n                          ]\n                        }\n                      }}\n                   end\n                 )\n      end)\n\n    assert log =~ \"Linear GraphQL request failed status=400\"\n    assert log =~ ~s(body=%{\"errors\" => [%{\"extensions\" => %{\"code\" => \"BAD_USER_INPUT\"})\n    assert log =~ \"Variable \\\\\\\"$ids\\\\\\\" got invalid value\"\n  end\n\n  test \"orchestrator sorts dispatch by priority then oldest created_at\" do\n    issue_same_priority_older = %Issue{\n      id: \"issue-old-high\",\n      identifier: \"MT-200\",\n      title: \"Old high priority\",\n      state: \"Todo\",\n      priority: 1,\n      created_at: ~U[2026-01-01 00:00:00Z]\n    }\n\n    issue_same_priority_newer = %Issue{\n      id: \"issue-new-high\",\n      identifier: \"MT-201\",\n      title: \"New high priority\",\n      state: \"Todo\",\n      priority: 1,\n      created_at: ~U[2026-01-02 00:00:00Z]\n    }\n\n    issue_lower_priority_older = %Issue{\n      id: \"issue-old-low\",\n      identifier: \"MT-199\",\n      title: \"Old lower priority\",\n      state: \"Todo\",\n      priority: 2,\n      created_at: ~U[2025-12-01 00:00:00Z]\n    }\n\n    sorted =\n      Orchestrator.sort_issues_for_dispatch_for_test([\n        issue_lower_priority_older,\n        issue_same_priority_newer,\n        issue_same_priority_older\n      ])\n\n    assert Enum.map(sorted, & &1.identifier) == [\"MT-200\", \"MT-201\", \"MT-199\"]\n  end\n\n  test \"todo issue with non-terminal blocker is not dispatch-eligible\" do\n    state = %Orchestrator.State{\n      max_concurrent_agents: 3,\n      running: %{},\n      claimed: MapSet.new(),\n      codex_totals: %{input_tokens: 0, output_tokens: 0, total_tokens: 0, seconds_running: 0},\n      retry_attempts: %{}\n    }\n\n    issue = %Issue{\n      id: \"blocked-1\",\n      identifier: \"MT-1001\",\n      title: \"Blocked work\",\n      state: \"Todo\",\n      blocked_by: [%{id: \"blocker-1\", identifier: \"MT-1002\", state: \"In Progress\"}]\n    }\n\n    refute Orchestrator.should_dispatch_issue_for_test(issue, state)\n  end\n\n  test \"issue assigned to another worker is not dispatch-eligible\" do\n    write_workflow_file!(Workflow.workflow_file_path(), tracker_assignee: \"dev@example.com\")\n\n    state = %Orchestrator.State{\n      max_concurrent_agents: 3,\n      running: %{},\n      claimed: MapSet.new(),\n      codex_totals: %{input_tokens: 0, output_tokens: 0, total_tokens: 0, seconds_running: 0},\n      retry_attempts: %{}\n    }\n\n    issue = %Issue{\n      id: \"assigned-away-1\",\n      identifier: \"MT-1007\",\n      title: \"Owned elsewhere\",\n      state: \"Todo\",\n      assigned_to_worker: false\n    }\n\n    refute Orchestrator.should_dispatch_issue_for_test(issue, state)\n  end\n\n  test \"todo issue with terminal blockers remains dispatch-eligible\" do\n    state = %Orchestrator.State{\n      max_concurrent_agents: 3,\n      running: %{},\n      claimed: MapSet.new(),\n      codex_totals: %{input_tokens: 0, output_tokens: 0, total_tokens: 0, seconds_running: 0},\n      retry_attempts: %{}\n    }\n\n    issue = %Issue{\n      id: \"ready-1\",\n      identifier: \"MT-1003\",\n      title: \"Ready work\",\n      state: \"Todo\",\n      blocked_by: [%{id: \"blocker-2\", identifier: \"MT-1004\", state: \"Closed\"}]\n    }\n\n    assert Orchestrator.should_dispatch_issue_for_test(issue, state)\n  end\n\n  test \"dispatch revalidation skips stale todo issue once a non-terminal blocker appears\" do\n    stale_issue = %Issue{\n      id: \"blocked-2\",\n      identifier: \"MT-1005\",\n      title: \"Stale blocked work\",\n      state: \"Todo\",\n      blocked_by: []\n    }\n\n    refreshed_issue = %Issue{\n      id: \"blocked-2\",\n      identifier: \"MT-1005\",\n      title: \"Stale blocked work\",\n      state: \"Todo\",\n      blocked_by: [%{id: \"blocker-3\", identifier: \"MT-1006\", state: \"In Progress\"}]\n    }\n\n    fetcher = fn [\"blocked-2\"] -> {:ok, [refreshed_issue]} end\n\n    assert {:skip, %Issue{} = skipped_issue} =\n             Orchestrator.revalidate_issue_for_dispatch_for_test(stale_issue, fetcher)\n\n    assert skipped_issue.identifier == \"MT-1005\"\n    assert skipped_issue.blocked_by == [%{id: \"blocker-3\", identifier: \"MT-1006\", state: \"In Progress\"}]\n  end\n\n  test \"workspace remove returns error information for missing directory\" do\n    random_path =\n      Path.join(\n        System.tmp_dir!(),\n        \"symphony-elixir-missing-#{System.unique_integer([:positive])}\"\n      )\n\n    assert {:ok, []} = Workspace.remove(random_path)\n  end\n\n  test \"workspace hooks support multiline YAML scripts and run at lifecycle boundaries\" do\n    test_root =\n      Path.join(\n        System.tmp_dir!(),\n        \"symphony-elixir-workspace-hooks-#{System.unique_integer([:positive])}\"\n      )\n\n    try do\n      workspace_root = Path.join(test_root, \"workspaces\")\n      before_remove_marker = Path.join(test_root, \"before_remove.log\")\n      after_create_counter = Path.join(test_root, \"after_create.count\")\n\n      File.mkdir_p!(workspace_root)\n\n      write_workflow_file!(Workflow.workflow_file_path(),\n        workspace_root: workspace_root,\n        hook_after_create: \"echo after_create > after_create.log\\necho call >> \\\"#{after_create_counter}\\\"\",\n        hook_before_remove: \"echo before_remove > \\\"#{before_remove_marker}\\\"\"\n      )\n\n      config = Config.settings!()\n      assert config.hooks.after_create =~ \"echo after_create > after_create.log\"\n      assert config.hooks.before_remove =~ \"echo before_remove >\"\n\n      assert {:ok, workspace} = Workspace.create_for_issue(\"MT-HOOKS\")\n      assert File.read!(Path.join(workspace, \"after_create.log\")) == \"after_create\\n\"\n\n      assert {:ok, _workspace} = Workspace.create_for_issue(\"MT-HOOKS\")\n      assert length(String.split(String.trim(File.read!(after_create_counter)), \"\\n\")) == 1\n\n      assert :ok = Workspace.remove_issue_workspaces(\"MT-HOOKS\")\n      assert File.read!(before_remove_marker) == \"before_remove\\n\"\n      refute File.exists?(workspace)\n    after\n      File.rm_rf(test_root)\n    end\n  end\n\n  test \"workspace remove continues when before_remove hook fails\" do\n    test_root =\n      Path.join(\n        System.tmp_dir!(),\n        \"symphony-elixir-workspace-hooks-fail-#{System.unique_integer([:positive])}\"\n      )\n\n    try do\n      workspace_root = Path.join(test_root, \"workspaces\")\n\n      File.mkdir_p!(workspace_root)\n\n      write_workflow_file!(Workflow.workflow_file_path(),\n        workspace_root: workspace_root,\n        hook_before_remove: \"echo failure && exit 17\"\n      )\n\n      assert {:ok, workspace} = Workspace.create_for_issue(\"MT-HOOKS-FAIL\")\n      assert :ok = Workspace.remove_issue_workspaces(\"MT-HOOKS-FAIL\")\n      refute File.exists?(workspace)\n    after\n      File.rm_rf(test_root)\n    end\n  end\n\n  test \"workspace remove continues when before_remove hook fails with large output\" do\n    test_root =\n      Path.join(\n        System.tmp_dir!(),\n        \"symphony-elixir-workspace-hooks-large-fail-#{System.unique_integer([:positive])}\"\n      )\n\n    try do\n      workspace_root = Path.join(test_root, \"workspaces\")\n\n      File.mkdir_p!(workspace_root)\n\n      write_workflow_file!(Workflow.workflow_file_path(),\n        workspace_root: workspace_root,\n        hook_before_remove: \"i=0; while [ $i -lt 3000 ]; do printf a; i=$((i+1)); done; exit 17\"\n      )\n\n      assert {:ok, workspace} = Workspace.create_for_issue(\"MT-HOOKS-LARGE-FAIL\")\n      assert :ok = Workspace.remove_issue_workspaces(\"MT-HOOKS-LARGE-FAIL\")\n      refute File.exists?(workspace)\n    after\n      File.rm_rf(test_root)\n    end\n  end\n\n  test \"workspace remove continues when before_remove hook times out\" do\n    previous_timeout = Application.get_env(:symphony_elixir, :workspace_hook_timeout_ms)\n\n    on_exit(fn ->\n      if is_nil(previous_timeout) do\n        Application.delete_env(:symphony_elixir, :workspace_hook_timeout_ms)\n      else\n        Application.put_env(:symphony_elixir, :workspace_hook_timeout_ms, previous_timeout)\n      end\n    end)\n\n    Application.put_env(:symphony_elixir, :workspace_hook_timeout_ms, 10)\n\n    test_root =\n      Path.join(\n        System.tmp_dir!(),\n        \"symphony-elixir-workspace-hooks-timeout-#{System.unique_integer([:positive])}\"\n      )\n\n    try do\n      workspace_root = Path.join(test_root, \"workspaces\")\n\n      File.mkdir_p!(workspace_root)\n\n      write_workflow_file!(Workflow.workflow_file_path(),\n        workspace_root: workspace_root,\n        hook_before_remove: \"sleep 1\"\n      )\n\n      assert {:ok, workspace} = Workspace.create_for_issue(\"MT-HOOKS-TIMEOUT\")\n      assert :ok = Workspace.remove_issue_workspaces(\"MT-HOOKS-TIMEOUT\")\n      refute File.exists?(workspace)\n    after\n      File.rm_rf(test_root)\n    end\n  end\n\n  test \"config reads defaults for optional settings\" do\n    previous_linear_api_key = System.get_env(\"LINEAR_API_KEY\")\n    on_exit(fn -> restore_env(\"LINEAR_API_KEY\", previous_linear_api_key) end)\n    System.delete_env(\"LINEAR_API_KEY\")\n\n    write_workflow_file!(Workflow.workflow_file_path(),\n      workspace_root: nil,\n      max_concurrent_agents: nil,\n      codex_approval_policy: nil,\n      codex_thread_sandbox: nil,\n      codex_turn_sandbox_policy: nil,\n      codex_turn_timeout_ms: nil,\n      codex_read_timeout_ms: nil,\n      codex_stall_timeout_ms: nil,\n      tracker_api_token: nil,\n      tracker_project_slug: nil\n    )\n\n    config = Config.settings!()\n    assert config.tracker.endpoint == \"https://api.linear.app/graphql\"\n    assert config.tracker.api_key == nil\n    assert config.tracker.project_slug == nil\n    assert config.workspace.root == Path.join(System.tmp_dir!(), \"symphony_workspaces\")\n    assert config.worker.max_concurrent_agents_per_host == nil\n    assert config.agent.max_concurrent_agents == 10\n    assert config.codex.command == \"codex app-server\"\n\n    assert config.codex.approval_policy == %{\n             \"reject\" => %{\n               \"sandbox_approval\" => true,\n               \"rules\" => true,\n               \"mcp_elicitations\" => true\n             }\n           }\n\n    assert config.codex.thread_sandbox == \"workspace-write\"\n\n    assert {:ok, canonical_default_workspace_root} =\n             SymphonyElixir.PathSafety.canonicalize(Path.join(System.tmp_dir!(), \"symphony_workspaces\"))\n\n    assert Config.codex_turn_sandbox_policy() == %{\n             \"type\" => \"workspaceWrite\",\n             \"writableRoots\" => [canonical_default_workspace_root],\n             \"readOnlyAccess\" => %{\"type\" => \"fullAccess\"},\n             \"networkAccess\" => false,\n             \"excludeTmpdirEnvVar\" => false,\n             \"excludeSlashTmp\" => false\n           }\n\n    assert config.codex.turn_timeout_ms == 3_600_000\n    assert config.codex.read_timeout_ms == 5_000\n    assert config.codex.stall_timeout_ms == 300_000\n\n    write_workflow_file!(Workflow.workflow_file_path(), codex_command: \"codex app-server --model gpt-5.3-codex\")\n    assert Config.settings!().codex.command == \"codex app-server --model gpt-5.3-codex\"\n\n    explicit_root =\n      Path.join(\n        System.tmp_dir!(),\n        \"symphony-elixir-explicit-sandbox-root-#{System.unique_integer([:positive])}\"\n      )\n\n    explicit_workspace = Path.join(explicit_root, \"MT-EXPLICIT\")\n    explicit_cache = Path.join(explicit_workspace, \"cache\")\n    File.mkdir_p!(explicit_cache)\n\n    on_exit(fn -> File.rm_rf(explicit_root) end)\n\n    write_workflow_file!(Workflow.workflow_file_path(),\n      workspace_root: explicit_root,\n      codex_approval_policy: \"on-request\",\n      codex_thread_sandbox: \"workspace-write\",\n      codex_turn_sandbox_policy: %{\n        type: \"workspaceWrite\",\n        writableRoots: [explicit_workspace, explicit_cache]\n      }\n    )\n\n    config = Config.settings!()\n    assert config.codex.approval_policy == \"on-request\"\n    assert config.codex.thread_sandbox == \"workspace-write\"\n\n    assert Config.codex_turn_sandbox_policy(explicit_workspace) == %{\n             \"type\" => \"workspaceWrite\",\n             \"writableRoots\" => [explicit_workspace, explicit_cache]\n           }\n\n    write_workflow_file!(Workflow.workflow_file_path(), tracker_active_states: \",\")\n    assert {:error, {:invalid_workflow_config, message}} = Config.validate!()\n    assert message =~ \"tracker.active_states\"\n\n    write_workflow_file!(Workflow.workflow_file_path(), max_concurrent_agents: \"bad\")\n    assert {:error, {:invalid_workflow_config, message}} = Config.validate!()\n    assert message =~ \"agent.max_concurrent_agents\"\n\n    write_workflow_file!(Workflow.workflow_file_path(), worker_max_concurrent_agents_per_host: 0)\n    assert {:error, {:invalid_workflow_config, message}} = Config.validate!()\n    assert message =~ \"worker.max_concurrent_agents_per_host\"\n\n    write_workflow_file!(Workflow.workflow_file_path(), codex_turn_timeout_ms: \"bad\")\n    assert {:error, {:invalid_workflow_config, message}} = Config.validate!()\n    assert message =~ \"codex.turn_timeout_ms\"\n\n    write_workflow_file!(Workflow.workflow_file_path(), codex_read_timeout_ms: \"bad\")\n    assert {:error, {:invalid_workflow_config, message}} = Config.validate!()\n    assert message =~ \"codex.read_timeout_ms\"\n\n    write_workflow_file!(Workflow.workflow_file_path(), codex_stall_timeout_ms: \"bad\")\n    assert {:error, {:invalid_workflow_config, message}} = Config.validate!()\n    assert message =~ \"codex.stall_timeout_ms\"\n\n    write_workflow_file!(Workflow.workflow_file_path(),\n      tracker_active_states: %{todo: true},\n      tracker_terminal_states: %{done: true},\n      poll_interval_ms: %{bad: true},\n      workspace_root: 123,\n      max_retry_backoff_ms: 0,\n      max_concurrent_agents_by_state: %{\"Todo\" => \"1\", \"Review\" => 0, \"Done\" => \"bad\"},\n      hook_timeout_ms: 0,\n      observability_enabled: \"maybe\",\n      observability_refresh_ms: %{bad: true},\n      observability_render_interval_ms: %{bad: true},\n      server_port: -1,\n      server_host: 123\n    )\n\n    assert {:error, {:invalid_workflow_config, _message}} = Config.validate!()\n\n    write_workflow_file!(Workflow.workflow_file_path(), codex_approval_policy: \"\")\n    assert :ok = Config.validate!()\n    assert Config.settings!().codex.approval_policy == \"\"\n\n    write_workflow_file!(Workflow.workflow_file_path(), codex_thread_sandbox: \"\")\n    assert :ok = Config.validate!()\n    assert Config.settings!().codex.thread_sandbox == \"\"\n\n    write_workflow_file!(Workflow.workflow_file_path(), codex_turn_sandbox_policy: \"bad\")\n    assert {:error, {:invalid_workflow_config, message}} = Config.validate!()\n    assert message =~ \"codex.turn_sandbox_policy\"\n\n    write_workflow_file!(Workflow.workflow_file_path(),\n      codex_approval_policy: \"future-policy\",\n      codex_thread_sandbox: \"future-sandbox\",\n      codex_turn_sandbox_policy: %{\n        type: \"futureSandbox\",\n        nested: %{flag: true}\n      }\n    )\n\n    config = Config.settings!()\n    assert config.codex.approval_policy == \"future-policy\"\n    assert config.codex.thread_sandbox == \"future-sandbox\"\n\n    assert :ok = Config.validate!()\n\n    assert Config.codex_turn_sandbox_policy() == %{\n             \"type\" => \"futureSandbox\",\n             \"nested\" => %{\"flag\" => true}\n           }\n\n    write_workflow_file!(Workflow.workflow_file_path(), codex_command: \"codex app-server\")\n    assert Config.settings!().codex.command == \"codex app-server\"\n  end\n\n  test \"config resolves $VAR references for env-backed secret and path values\" do\n    workspace_env_var = \"SYMP_WORKSPACE_ROOT_#{System.unique_integer([:positive])}\"\n    api_key_env_var = \"SYMP_LINEAR_API_KEY_#{System.unique_integer([:positive])}\"\n    workspace_root = Path.join(\"/tmp\", \"symphony-workspace-root\")\n    api_key = \"resolved-secret\"\n    codex_bin = Path.join([\"~\", \"bin\", \"codex\"])\n\n    previous_workspace_root = System.get_env(workspace_env_var)\n    previous_api_key = System.get_env(api_key_env_var)\n\n    System.put_env(workspace_env_var, workspace_root)\n    System.put_env(api_key_env_var, api_key)\n\n    on_exit(fn ->\n      restore_env(workspace_env_var, previous_workspace_root)\n      restore_env(api_key_env_var, previous_api_key)\n    end)\n\n    write_workflow_file!(Workflow.workflow_file_path(),\n      tracker_api_token: \"$#{api_key_env_var}\",\n      workspace_root: \"$#{workspace_env_var}\",\n      codex_command: \"#{codex_bin} app-server\"\n    )\n\n    config = Config.settings!()\n    assert config.tracker.api_key == api_key\n    assert config.workspace.root == Path.expand(workspace_root)\n    assert config.codex.command == \"#{codex_bin} app-server\"\n  end\n\n  test \"config no longer resolves legacy env: references\" do\n    workspace_env_var = \"SYMP_WORKSPACE_ROOT_#{System.unique_integer([:positive])}\"\n    api_key_env_var = \"SYMP_LINEAR_API_KEY_#{System.unique_integer([:positive])}\"\n    workspace_root = Path.join(\"/tmp\", \"symphony-workspace-root\")\n    api_key = \"resolved-secret\"\n\n    previous_workspace_root = System.get_env(workspace_env_var)\n    previous_api_key = System.get_env(api_key_env_var)\n\n    System.put_env(workspace_env_var, workspace_root)\n    System.put_env(api_key_env_var, api_key)\n\n    on_exit(fn ->\n      restore_env(workspace_env_var, previous_workspace_root)\n      restore_env(api_key_env_var, previous_api_key)\n    end)\n\n    write_workflow_file!(Workflow.workflow_file_path(),\n      tracker_api_token: \"env:#{api_key_env_var}\",\n      workspace_root: \"env:#{workspace_env_var}\"\n    )\n\n    config = Config.settings!()\n    assert config.tracker.api_key == \"env:#{api_key_env_var}\"\n    assert config.workspace.root == \"env:#{workspace_env_var}\"\n  end\n\n  test \"config supports per-state max concurrent agent overrides\" do\n    workflow = \"\"\"\n    ---\n    agent:\n      max_concurrent_agents: 10\n      max_concurrent_agents_by_state:\n        todo: 1\n        \"In Progress\": 4\n        \"In Review\": 2\n    ---\n    \"\"\"\n\n    File.write!(Workflow.workflow_file_path(), workflow)\n\n    assert Config.settings!().agent.max_concurrent_agents == 10\n    assert Config.max_concurrent_agents_for_state(\"Todo\") == 1\n    assert Config.max_concurrent_agents_for_state(\"In Progress\") == 4\n    assert Config.max_concurrent_agents_for_state(\"In Review\") == 2\n    assert Config.max_concurrent_agents_for_state(\"Closed\") == 10\n    assert Config.max_concurrent_agents_for_state(:not_a_string) == 10\n\n    write_workflow_file!(Workflow.workflow_file_path(), worker_max_concurrent_agents_per_host: 2)\n    assert :ok = Config.validate!()\n    assert Config.settings!().worker.max_concurrent_agents_per_host == 2\n  end\n\n  test \"schema helpers cover custom type and state limit validation\" do\n    assert StringOrMap.type() == :map\n    assert StringOrMap.embed_as(:json) == :self\n    assert StringOrMap.equal?(%{\"a\" => 1}, %{\"a\" => 1})\n    refute StringOrMap.equal?(%{\"a\" => 1}, %{\"a\" => 2})\n\n    assert {:ok, \"value\"} = StringOrMap.cast(\"value\")\n    assert {:ok, %{\"a\" => 1}} = StringOrMap.cast(%{\"a\" => 1})\n    assert :error = StringOrMap.cast(123)\n\n    assert {:ok, \"value\"} = StringOrMap.load(\"value\")\n    assert :error = StringOrMap.load(123)\n\n    assert {:ok, %{\"a\" => 1}} = StringOrMap.dump(%{\"a\" => 1})\n    assert :error = StringOrMap.dump(123)\n\n    assert Schema.normalize_state_limits(nil) == %{}\n\n    assert Schema.normalize_state_limits(%{\"In Progress\" => 2, todo: 1}) == %{\n             \"todo\" => 1,\n             \"in progress\" => 2\n           }\n\n    changeset =\n      {%{}, %{limits: :map}}\n      |> Changeset.cast(%{limits: %{\"\" => 1, \"todo\" => 0}}, [:limits])\n      |> Schema.validate_state_limits(:limits)\n\n    assert changeset.errors == [\n             limits: {\"state names must not be blank\", []},\n             limits: {\"limits must be positive integers\", []}\n           ]\n  end\n\n  test \"schema parse normalizes policy keys and env-backed fallbacks\" do\n    missing_workspace_env = \"SYMP_MISSING_WORKSPACE_#{System.unique_integer([:positive])}\"\n    empty_secret_env = \"SYMP_EMPTY_SECRET_#{System.unique_integer([:positive])}\"\n    missing_secret_env = \"SYMP_MISSING_SECRET_#{System.unique_integer([:positive])}\"\n\n    previous_missing_workspace_env = System.get_env(missing_workspace_env)\n    previous_empty_secret_env = System.get_env(empty_secret_env)\n    previous_missing_secret_env = System.get_env(missing_secret_env)\n    previous_linear_api_key = System.get_env(\"LINEAR_API_KEY\")\n\n    System.delete_env(missing_workspace_env)\n    System.put_env(empty_secret_env, \"\")\n    System.delete_env(missing_secret_env)\n    System.put_env(\"LINEAR_API_KEY\", \"fallback-linear-token\")\n\n    on_exit(fn ->\n      restore_env(missing_workspace_env, previous_missing_workspace_env)\n      restore_env(empty_secret_env, previous_empty_secret_env)\n      restore_env(missing_secret_env, previous_missing_secret_env)\n      restore_env(\"LINEAR_API_KEY\", previous_linear_api_key)\n    end)\n\n    assert {:ok, settings} =\n             Schema.parse(%{\n               tracker: %{api_key: \"$#{empty_secret_env}\"},\n               workspace: %{root: \"$#{missing_workspace_env}\"},\n               codex: %{approval_policy: %{reject: %{sandbox_approval: true}}}\n             })\n\n    assert settings.tracker.api_key == nil\n    assert settings.workspace.root == Path.join(System.tmp_dir!(), \"symphony_workspaces\")\n\n    assert settings.codex.approval_policy == %{\n             \"reject\" => %{\"sandbox_approval\" => true}\n           }\n\n    assert {:ok, settings} =\n             Schema.parse(%{\n               tracker: %{api_key: \"$#{missing_secret_env}\"},\n               workspace: %{root: \"\"}\n             })\n\n    assert settings.tracker.api_key == \"fallback-linear-token\"\n    assert settings.workspace.root == Path.join(System.tmp_dir!(), \"symphony_workspaces\")\n  end\n\n  test \"schema resolves sandbox policies from explicit and default workspaces\" do\n    explicit_policy = %{\"type\" => \"workspaceWrite\", \"writableRoots\" => [\"/tmp/explicit\"]}\n\n    assert Schema.resolve_turn_sandbox_policy(%Schema{\n             codex: %Codex{turn_sandbox_policy: explicit_policy},\n             workspace: %Schema.Workspace{root: \"/tmp/ignored\"}\n           }) == explicit_policy\n\n    assert Schema.resolve_turn_sandbox_policy(%Schema{\n             codex: %Codex{turn_sandbox_policy: nil},\n             workspace: %Schema.Workspace{root: \"\"}\n           }) == %{\n             \"type\" => \"workspaceWrite\",\n             \"writableRoots\" => [Path.expand(Path.join(System.tmp_dir!(), \"symphony_workspaces\"))],\n             \"readOnlyAccess\" => %{\"type\" => \"fullAccess\"},\n             \"networkAccess\" => false,\n             \"excludeTmpdirEnvVar\" => false,\n             \"excludeSlashTmp\" => false\n           }\n\n    assert Schema.resolve_turn_sandbox_policy(\n             %Schema{\n               codex: %Codex{turn_sandbox_policy: nil},\n               workspace: %Schema.Workspace{root: \"/tmp/ignored\"}\n             },\n             \"/tmp/workspace\"\n           ) == %{\n             \"type\" => \"workspaceWrite\",\n             \"writableRoots\" => [Path.expand(\"/tmp/workspace\")],\n             \"readOnlyAccess\" => %{\"type\" => \"fullAccess\"},\n             \"networkAccess\" => false,\n             \"excludeTmpdirEnvVar\" => false,\n             \"excludeSlashTmp\" => false\n           }\n  end\n\n  test \"schema keeps workspace roots raw while sandbox helpers expand only for local use\" do\n    assert {:ok, settings} =\n             Schema.parse(%{\n               workspace: %{root: \"~/.symphony-workspaces\"},\n               codex: %{}\n             })\n\n    assert settings.workspace.root == \"~/.symphony-workspaces\"\n\n    assert Schema.resolve_turn_sandbox_policy(settings) == %{\n             \"type\" => \"workspaceWrite\",\n             \"writableRoots\" => [Path.expand(\"~/.symphony-workspaces\")],\n             \"readOnlyAccess\" => %{\"type\" => \"fullAccess\"},\n             \"networkAccess\" => false,\n             \"excludeTmpdirEnvVar\" => false,\n             \"excludeSlashTmp\" => false\n           }\n\n    assert {:ok, remote_policy} =\n             Schema.resolve_runtime_turn_sandbox_policy(settings, nil, remote: true)\n\n    assert remote_policy == %{\n             \"type\" => \"workspaceWrite\",\n             \"writableRoots\" => [\"~/.symphony-workspaces\"],\n             \"readOnlyAccess\" => %{\"type\" => \"fullAccess\"},\n             \"networkAccess\" => false,\n             \"excludeTmpdirEnvVar\" => false,\n             \"excludeSlashTmp\" => false\n           }\n  end\n\n  test \"runtime sandbox policy resolution passes explicit policies through unchanged\" do\n    test_root =\n      Path.join(\n        System.tmp_dir!(),\n        \"symphony-elixir-runtime-sandbox-#{System.unique_integer([:positive])}\"\n      )\n\n    try do\n      workspace_root = Path.join(test_root, \"workspaces\")\n      issue_workspace = Path.join(workspace_root, \"MT-100\")\n      File.mkdir_p!(issue_workspace)\n\n      write_workflow_file!(Workflow.workflow_file_path(),\n        workspace_root: workspace_root,\n        codex_turn_sandbox_policy: %{\n          type: \"workspaceWrite\",\n          writableRoots: [\"relative/path\"],\n          networkAccess: true\n        }\n      )\n\n      assert {:ok, runtime_settings} = Config.codex_runtime_settings(issue_workspace)\n\n      assert runtime_settings.turn_sandbox_policy == %{\n               \"type\" => \"workspaceWrite\",\n               \"writableRoots\" => [\"relative/path\"],\n               \"networkAccess\" => true\n             }\n\n      write_workflow_file!(Workflow.workflow_file_path(),\n        workspace_root: workspace_root,\n        codex_turn_sandbox_policy: %{\n          type: \"futureSandbox\",\n          nested: %{flag: true}\n        }\n      )\n\n      assert {:ok, runtime_settings} = Config.codex_runtime_settings(issue_workspace)\n\n      assert runtime_settings.turn_sandbox_policy == %{\n               \"type\" => \"futureSandbox\",\n               \"nested\" => %{\"flag\" => true}\n             }\n    after\n      File.rm_rf(test_root)\n    end\n  end\n\n  test \"path safety returns errors for invalid path segments\" do\n    invalid_segment = String.duplicate(\"a\", 300)\n    path = Path.join(System.tmp_dir!(), invalid_segment)\n    expanded_path = Path.expand(path)\n\n    assert {:error, {:path_canonicalize_failed, ^expanded_path, :enametoolong}} =\n             SymphonyElixir.PathSafety.canonicalize(path)\n  end\n\n  test \"runtime sandbox policy resolution defaults when omitted and ignores workspace for explicit policies\" do\n    test_root =\n      Path.join(\n        System.tmp_dir!(),\n        \"symphony-elixir-runtime-sandbox-branches-#{System.unique_integer([:positive])}\"\n      )\n\n    try do\n      workspace_root = Path.join(test_root, \"workspaces\")\n      issue_workspace = Path.join(workspace_root, \"MT-101\")\n\n      File.mkdir_p!(issue_workspace)\n\n      write_workflow_file!(Workflow.workflow_file_path(), workspace_root: workspace_root)\n\n      settings = Config.settings!()\n\n      assert {:ok, canonical_workspace_root} =\n               SymphonyElixir.PathSafety.canonicalize(workspace_root)\n\n      assert {:ok, default_policy} = Schema.resolve_runtime_turn_sandbox_policy(settings)\n      assert default_policy[\"type\"] == \"workspaceWrite\"\n      assert default_policy[\"writableRoots\"] == [canonical_workspace_root]\n\n      assert {:ok, blank_workspace_policy} =\n               Schema.resolve_runtime_turn_sandbox_policy(settings, \"\")\n\n      assert blank_workspace_policy == default_policy\n\n      read_only_settings = %{\n        settings\n        | codex: %{settings.codex | turn_sandbox_policy: %{\"type\" => \"readOnly\", \"networkAccess\" => true}}\n      }\n\n      assert {:ok, %{\"type\" => \"readOnly\", \"networkAccess\" => true}} =\n               Schema.resolve_runtime_turn_sandbox_policy(read_only_settings, 123)\n\n      future_settings = %{\n        settings\n        | codex: %{settings.codex | turn_sandbox_policy: %{\"type\" => \"futureSandbox\", \"nested\" => %{\"flag\" => true}}}\n      }\n\n      assert {:ok, %{\"type\" => \"futureSandbox\", \"nested\" => %{\"flag\" => true}}} =\n               Schema.resolve_runtime_turn_sandbox_policy(future_settings, 123)\n\n      assert {:error, {:unsafe_turn_sandbox_policy, {:invalid_workspace_root, 123}}} =\n               Schema.resolve_runtime_turn_sandbox_policy(settings, 123)\n    after\n      File.rm_rf(test_root)\n    end\n  end\n\n  test \"workflow prompt is used when building base prompt\" do\n    workflow_prompt = \"Workflow prompt body used as codex instruction.\"\n\n    write_workflow_file!(Workflow.workflow_file_path(), prompt: workflow_prompt)\n    assert Config.workflow_prompt() == workflow_prompt\n  end\n\n  test \"remote workspace lifecycle uses ssh host aliases from worker config\" do\n    test_root =\n      Path.join(\n        System.tmp_dir!(),\n        \"symphony-elixir-remote-workspace-#{System.unique_integer([:positive])}\"\n      )\n\n    previous_path = System.get_env(\"PATH\")\n    previous_trace = System.get_env(\"SYMP_TEST_SSH_TRACE\")\n\n    on_exit(fn ->\n      restore_env(\"PATH\", previous_path)\n      restore_env(\"SYMP_TEST_SSH_TRACE\", previous_trace)\n    end)\n\n    try do\n      trace_file = Path.join(test_root, \"ssh.trace\")\n      fake_ssh = Path.join(test_root, \"ssh\")\n      workspace_root = \"~/.symphony-remote-workspaces\"\n      workspace_path = \"/remote/home/.symphony-remote-workspaces/MT-SSH-WS\"\n\n      File.mkdir_p!(test_root)\n      System.put_env(\"SYMP_TEST_SSH_TRACE\", trace_file)\n      System.put_env(\"PATH\", test_root <> \":\" <> (previous_path || \"\"))\n\n      File.write!(fake_ssh, \"\"\"\n      #!/bin/sh\n      trace_file=\"${SYMP_TEST_SSH_TRACE:-/tmp/symphony-fake-ssh.trace}\"\n      printf 'ARGV:%s\\\\n' \"$*\" >> \"$trace_file\"\n\n      case \"$*\" in\n        *\"__SYMPHONY_WORKSPACE__\"*)\n          printf '%s\\\\t%s\\\\t%s\\\\n' '__SYMPHONY_WORKSPACE__' '1' '#{workspace_path}'\n          ;;\n      esac\n\n      exit 0\n      \"\"\")\n\n      File.chmod!(fake_ssh, 0o755)\n\n      write_workflow_file!(Workflow.workflow_file_path(),\n        workspace_root: workspace_root,\n        worker_ssh_hosts: [\"worker-01:2200\"],\n        hook_before_run: \"echo before-run\",\n        hook_after_run: \"echo after-run\",\n        hook_before_remove: \"echo before-remove\"\n      )\n\n      assert Config.settings!().worker.ssh_hosts == [\"worker-01:2200\"]\n      assert Config.settings!().workspace.root == workspace_root\n      assert {:ok, ^workspace_path} = Workspace.create_for_issue(\"MT-SSH-WS\", \"worker-01:2200\")\n      assert :ok = Workspace.run_before_run_hook(workspace_path, \"MT-SSH-WS\", \"worker-01:2200\")\n      assert :ok = Workspace.run_after_run_hook(workspace_path, \"MT-SSH-WS\", \"worker-01:2200\")\n      assert :ok = Workspace.remove_issue_workspaces(\"MT-SSH-WS\", \"worker-01:2200\")\n\n      trace = File.read!(trace_file)\n      assert trace =~ \"-p 2200 worker-01 bash -lc\"\n      assert trace =~ \"__SYMPHONY_WORKSPACE__\"\n      assert trace =~ \"~/.symphony-remote-workspaces/MT-SSH-WS\"\n      assert trace =~ \"${workspace#~/}\"\n      assert trace =~ \"echo before-run\"\n      assert trace =~ \"echo after-run\"\n      assert trace =~ \"echo before-remove\"\n      assert trace =~ \"rm -rf\"\n      assert trace =~ workspace_path\n    after\n      File.rm_rf(test_root)\n    end\n  end\nend\n"
  },
  {
    "path": "elixir/test/test_helper.exs",
    "content": "ExUnit.start()\nCode.require_file(\"support/snapshot_support.exs\", __DIR__)\nCode.require_file(\"support/test_support.exs\", __DIR__)\n"
  }
]