Blog 18 — PR #52: v0.8.0 Support Engineer Agent

Series: Building toward full autonomous multi-agent dispatch
PR: #52 — feat/v0.8.0-support-engineer-agent
Date: 2026-06-21


Where we left off (PR #51)

PR #51 solved the Codex headless dispatch problem: codex exec - reads a prompt from stdin without requiring a TTY, and -s workspace-write confines writes to the workdir. The key lesson was that --dangerously-bypass-approvals-and-sandbox silently overrides -s entirely — caught by both AGY and Codex in a dual-agent review. The goalpost after PR #51: get an autonomous monitoring agent running so synlynk watches itself.


Strategic shift: Definition B decomposed

The original Definition B spec described three agents — Support Engineer, PM Agent, and Marketing Intern — as a single milestone. AGY's gap analysis (committed as docs/proposals/autopilot-gap-analysis-agy.md) raised five unresolved questions about ownership models, HITL thresholds, and cold-start logistics. Rather than spec all three at once, we scoped down to Support Engineer only. PM Agent and Marketing Intern remain as .agents/pm.yaml / .agents/marketing.yaml files to create later — no engine changes needed to add them.


What shipped

Agent allocation model

New subcommand: synlynk agent run <name>

synlynk agent run support          # collect signals, investigate, file issues, attempt fixes
synlynk agent run support --dry-run   # print findings only, no dispatch or GitHub calls
synlynk agent run support --install-cron  # idempotent crontab install (0 */6 * * *)
synlynk agent list                 # show .agents/*.json files and last run timestamp

Agent config: .agents/support.json

Agents are defined by a JSON file, not code. The engine reads it; adding PM Agent later = a new .agents/pm.json, no engine changes:

{
  "name": "support-engineer",
  "investigator": "claude",
  "fixer": "claude",
  "signals": [
    {"type": "test_suite",      "command": "pytest tests/ -q --tb=short"},
    {"type": "sentinel_alerts", "path": ".synlynk/sentinel.md"},
    {"type": "telemetry_anomaly", "failure_rate_threshold": 0.30},
    {"type": "capability_drop",   "drop_threshold": 1.5},
    {"type": "github_issues",     "labels": ["bug", "needs-triage"]}
  ],
  "hitl": {"auto_merge": false}
}

auto_merge: false is B-mode — draft PRs only. Flipping to true upgrades to C-mode without any code change.

5 signal collectors

Signal Method Severity
test_suite subprocess.run(["pytest", ...]) Any failure → high
sentinel_alerts Read .synlynk/sentinel.md for lines FLATLINE/QUOTA → high, others → medium
telemetry_anomaly Last 20 telemetry entries, compute failure rate >60% → high, >30% → medium
capability_drop 7-day vs prior-7-day avg quality per agent Δ>3pts → high, Δ>1.5pts → medium
github_issues gh issue list --label bug,needs-triage --json Always medium

CI detection: GITHUB_ACTIONS=true skips sentinel_alerts and telemetry_anomaly (which require local .synlynk/ state).

7-day dedup window (autopilot_runs table)

New SQLite table prevents re-investigating the same signal within 7 days (30 days for github_issues, to avoid re-filing duplicate issues on persistent open bugs). signal_hash = MD5 of the finding's key content.

Investigation pipeline

For each new finding (not in dedup window):

  1. Create a story in state.db with deterministic ID support-<hash[:8]>
  2. Build prompt: signal detail + project context from generate_context() + task instruction
  3. Run investigator foreground via subprocess.run() (300s timeout) — not the background dispatch_agent()
  4. Detect fix signal: # FIX: marker or --- a/ unified diff in log output
  5. File GitHub issue via gh issue create --label bug,support-engineer
  6. If fix signal: extract diff, git apply, run tests, open draft PR or post failure comment
  7. Write autopilot_runs row with status + PR URL

Engine caps at 5 findings per run (highest severity first).

Key implementation decisions

JSON not YAML for config files: synlynk is stdlib-only. Python 3 has no built-in YAML parser. JSON is equivalent for this config structure.

subprocess.run foreground, not dispatch_agent: dispatch_agent() uses Popen with start_new_session=True (background, fire-and-forget). The investigation pipeline needs to block and read the output before proceeding to file an issue. Building the same shell command pattern with subprocess.run(timeout=300) was the cleanest path without modifying dispatch_agent.

_attempt_fix returns (status, pr_url) tuple: The final code review caught that pr_url was always NULL in autopilot_runs. Changed return type to tuple so the PR URL from gh pr create is captured and stored.

Temp patch file cleanup: NamedTemporaryFile(delete=False) + git apply is wrapped in try/finally with os.unlink(patch_path) to prevent accumulation in /tmp/.

GitHub Actions trigger

.github/workflows/support-engineer.yml fires on push to main and on a 0 */6 * * * schedule. Requires ANTHROPIC_API_KEY and uses GITHUB_TOKEN (auto-provided) for issue/PR creation.

Tests: 200 passing (up from 176)

24 new tests cover every signal collector, dedup windows, the investigation dispatch, issue filing, diff extraction, fix attempt, engine orchestration (including dry-run no-side-effects), and cron install idempotency.


What was achieved

synlynk now watches itself. On every push to main (and every 6 hours), the Support Engineer collects health signals across 5 dimensions, deduplicates against a 7/30-day history, dispatches Claude to investigate new findings, files GitHub issues with investigation results, and opens draft fix PRs when the investigation produces a diff that passes the test suite.

The agent config file pattern means adding PM Agent or Marketing Intern = a new .agents/*.json file. The engine routes to them automatically.


New goalpost

v0.9.0: Support Engineer running stably in CI for at least one full week. Once confirmed, write .agents/pm.json for the PM Agent (growth-signal collector: star velocity, clone counts, install rate).