June

Troubleshooting

Predictable failure modes. Fix command first; explanation second.

Predictable failure modes. Fix command first; explanation second.

If a behaviour isn't listed here, start with:

jun doctor          # green/red checklist with fix commands
jun logs -f         # follow logs/agent.log
jun config show     # see the resolved config (secrets masked)
jun status          # is the orchestrator running? listening where?

jun doctor is the first call. It checks the Linear key, the team, the workflow states, the labels, every runner binary, the webhook registration, and pushes a signed fake webhook through the in-process orchestrator. The failure rows print the exact fix command.

Issue is stuck in Backlog and nothing happens

Fix: move the issue out of Backlog (e.g. to Todo) and post a comment.

Why: Backlog is hard-ignored. june never engages with an issue in Backlog — no DB row, no comment-thread mapping, no run, even on a fresh comment. The state is deliberately inert so unfiled drafts don't get a Claude session every time someone comments on them. See concepts.md → State machine.

If an issue you care about moved to Backlog while a run was active, that run was killed at the transition (moved to Backlog by user).

Webhook fires but I never see a run

Fix: jun doctor and look at the Webhook registered and Local smoke rows. Most common causes:

  1. Signature mismatch. jun logs -f shows invalid signature 401s. Check that LINEAR_WEBHOOK_SECRET in your env matches the secret on the Linear webhook config. If you rotated one and not the other, re-running jun setup will pick a fresh URL/secret and re-register.
  2. Wrong URL. Linear is delivering to a stale public URL (the tunnel rotated, ngrok restarted). Restart the tunnel, then re-run jun setup and paste the new URL — it'll update the registered webhook in place.
  3. Team mismatch. The webhook is scoped to a team, and the issue isn't in that team. Linear delivers nothing in that case. jun config show will print the resolved teamKey.
  4. Webhook disabled in Linear. Doctor's Webhook registered row will say disabled. Toggle it in Linear's webhook settings.

Webhook delivers but the issue moves to Blocked with runner CLI unavailable

Fix: install the runner CLI for the label on the issue (claude, codex, opencode, agy, or hermes), and restart june (jun stop && jun start, or for a service: jun uninstall-service && jun install-service). jun setup and jun doctor both report which runners they found on $PATH.

Why: june probes each runner binary at boot. A missing CLI gets a null runner; any Linear label that would route to it falls back to Claude, with a redirect comment on the issue. If Claude itself is missing, june refuses to boot — that's the one runner that must be installed.

If the binary is installed but in a non-default location:

JUNE_CLAUDE_BIN=/Users/you/.bun/bin/claude
JUNE_CODEX_BIN=/opt/homebrew/opt/codex/bin/codex
JUNE_OPENCODE_BIN=...
JUNE_ANTIGRAVITY_BIN=...
JUNE_HERMES_BIN=...

This is especially common under launchd / systemd, which strip PATH down to /usr/bin:/bin and lose ~/.local/bin and /opt/homebrew/bin along the way.

Runner says "not authenticated" or fails immediately with empty output

Fix: log in on the host as the user june runs as:

RunnerCommandState dir
Claudeclaude (interactive once)~/.claude/
Codexcodex login~/.codex/
OpenCodeopencode auth login~/.opencode/
AntigravitySign into the desktop appdesktop-managed
Hermeshermes init then configure a provider~/.hermes/

Pay attention to $HOME: if june runs under launchd as your normal user, it uses your normal $HOME. If it runs under a dedicated _june user, that user needs its own login session for each runner.

The Claude runner deletes ANTHROPIC_API_KEY from the child env before spawning, so even if you set the API key, Claude will look for the logged-in user account. This is on purpose; see security.md → How runner auth works.

The orchestrator started but isn't moving the issue's state

Fix: re-run jun setup (it ensures the missing states and labels are present, idempotent), or call the targeted scripts directly: npm run ensure-states && npm run ensure-labels. Or check that your team has Ready for Review and Blocked (or whatever you set JUNE_REVIEW_STATES / JUNE_BLOCKED_STATES to).

Why: When state lookup returns nothing, the orchestrator logs getStateMap failed (or waiting_state_missing) and skips the move. The run still completes; the comment is still posted; the state just stays where it was.

If your team uses different state names than the defaults (In Progress, Ready for Review, Blocked), the right knob is JUNE_WORKING_STATES / JUNE_REVIEW_STATES / JUNE_BLOCKED_STATES, not renaming the states. See configuration.md → States.

The agent is replying to itself in a loop

Fix: add the bot's Linear user email to JUNE_BOT_USER_EMAILS (comma-separated). The viewer email of your LINEAR_API_KEY is added automatically at boot, so this only kicks in when you have multiple Linear bots in the workspace.

Why: june filters out the bot's own comments before passing them to the agent and ignores incoming comment webhooks whose author email matches the bot's. The filter is keyed on email; an additional bot account needs to be declared.

Cloudflare Tunnel URL changed and Linear is still pointing at the old one

Fix: restart the tunnel, then re-run jun setup — it finds the existing june-labelled webhook for your team and offers to update its URL in place. Idempotent.

For a permanent URL, set up a named tunnel: cloudflared tunnel login, cloudflared tunnel create june, cloudflared tunnel route dns june june.<your-domain>. Docs: https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/get-started/create-local-tunnel/.

Run is taking forever and I want to stop it

Fix:

curl -X POST -H "Authorization: Bearer $JUNE_ADMIN_TOKEN" \
  http://127.0.0.1:8787/admin/runs/<run-id>/stop

Find the <run-id> from logs/agent.log (look for starting agent run / runId) or from the SQLite DB at data/agent.sqlite:

sqlite3 data/agent.sqlite "SELECT id, issue_id, status, started_at FROM runs WHERE status='running'"

The endpoint SIGTERMs the run's process group, SIGKILLs it after 5 s, marks the row stopped, posts a Blocked. comment, and moves the issue to Blocked.

If JUNE_ADMIN_TOKEN isn't set, the endpoint responds 403 admin disabled. Set the token, restart, retry. Or just kill the orchestrator and let next-boot reconciliation handle it.

Orchestrator crashed mid-run; the next boot keeps the issue in In Progress

Fix: the orchestrator will reconcile on next boot — wait a few seconds, you'll see a Blocked. comment and the issue will move to Blocked. Or force it now:

curl -X POST -H "Authorization: Bearer $JUNE_ADMIN_TOKEN" \
  http://127.0.0.1:8787/admin/reconcile

Why: Each runner is spawned detached: true. If the orchestrator dies, the child can survive. On next boot, june checks every DB row marked running:

  • If the child pid is still alive, june watches it and reconciles when it exits.
  • If the pid is dead, the row is marked orphaned immediately and reconciled.

Reconciliation = a Linear comment + a state move to Blocked. The issue is never silently abandoned.

Run keeps restarting on a tight loop

Fix: stop commenting on the issue. Each new comment during an active run steers the run — SIGTERM + restart with the new comment as input. If you're typing fast, you can get into a state where the orchestrator chains restart after restart.

The cap is JUNE_COMMENT_MAX_AUTO_FLUSHES (default 3). After hitting that cap, queued comments stay in the DB and the next routing webhook drains them. So just stop commenting for ~5 s and the storm subsides.

If you actually want to redirect mid-task and don't want the steer behaviour, set JUNE_COMMENT_DEBOUNCE_MS=0 (defeats the debounce side; steer behaviour is separate) — actually no: steer happens any time a comment arrives during an active run, period. The only knob is JUNE_COMMENT_MAX_AUTO_FLUSHES.

Linear shows Health check at 25 min → TAKEOVER and the run restarted

Fix: that's the watchdog working as designed. The run wasn't writing to its session log; june killed it and queued a fresh start with a "previous run was hung" directive. If this is happening too aggressively, raise JUNE_CLAUDE_WATCHDOG_HARD_IDLE_MINUTES (default 25).

Why: the watchdog Claude makes a verdict every JUNE_CLAUDE_HEALTH_CHECK_INTERVAL_MS (default 15 min). The hard-idle takeover (silent for 25+ min) skips spending a watchdog Claude and just kills. See concepts.md → Health watchdog. Disable entirely with JUNE_CLAUDE_HEALTH_CHECK_INTERVAL_MS=0.

SQLite "database is locked" in logs

Fix: rare. Usually means two node processes are pointed at the same data/agent.sqlite. jun status reports a stale pid file if one is lingering; otherwise ps aux | grep server.mjs and kill the duplicate. Running both jun start and jun install-service's background unit at the same time will race — pick one.

june opens the DB with journal_mode=WAL so concurrent readers are fine; the lock is on writes only.

Issue not getting marked Waiting even though it's been sitting in Ready for Review for hours

Fix: check that JUNE_WAITING_SWEEP_ENABLED=1 (the default — confirm with jun config show) and that your team has a Waiting state (or whatever you set JUNE_WAITING_STATES[0] to). jun doctor does not currently exercise the sweep — easiest verify is to jun logs -f and look for waiting sweep moved issue lines.

Why: sweep ticks at JUNE_WAITING_SWEEP_INTERVAL_MS (default 5 min) and moves issues whose latest comment is from the bot and is older than JUNE_WAITING_AFTER_MS (default 1 h). Common reasons the sweep skips an issue:

  • the latest comment is from a human (good — the human is still engaged);
  • the latest comment is from the bot but too recent (wait longer);
  • the issue has an active run or a start pending;
  • no Waiting state in the team (sweep no-ops with waiting_state_missing).

JUNE_AGENT_ROOT change isn't picked up by sessions

Fix: move the issue out of an active state (e.g. to Backlog and back), or just accept that switching roots starts fresh sessions.

Why: june stores the working directory alongside each session id. If the stored root doesn't match the current JUNE_AGENT_ROOT, the session is dropped and a fresh one starts. This is intentional — a Claude session bound to /Users/old/workspace/ doesn't make sense when the orchestrator is now running in /Users/new/workspace/.

Comment never appears even though the run succeeded

Fix: jun logs -f for commentCreate errors. If Linear is returning a 403 / 401, the API key has been revoked or scoped down (re-run jun setup to rotate it). If it's returning a 400, the comment body might be too large; june doesn't currently truncate posted comments (only the prompt input is truncated, at 25 000 chars).

The final stdout is the deliverable; if it's empty or starts with BLOCKED:, the comment will say Blocked.\n\nfinal answer is empty instead. That's the verification gate doing its job.

Doctor passes but real webhooks still fail

Fix: the doctor smoke test bypasses the runner CLI and uses a FakeClaude. It exercises HMAC verification, the orchestrator's state machine, comment posting, and state transitions, but it does not validate that the real Claude / Codex / etc. binary is callable end-to-end. Run npm run acceptance:live for a true end-to-end probe — it creates a disposable Linear issue and waits for it to reach Ready for Review.

If acceptance:live hangs in In Progress, the runner isn't producing output. jun logs -f and look for final answer is empty or Claude exited <code>.

I can't find JUNE_LINEAR_API_KEY in the config

It's LINEAR_API_KEY (no JUNE_ prefix). Same for LINEAR_WEBHOOK_SECRET. The JUNE_*-prefixed env vars are june-specific tunables; the Linear credentials reuse Linear's own naming.

Where to look for more

  • jun logs -f (or <appDir>/logs/agent.log) — every webhook, every run, every state transition.
  • <appDir>/data/agent.sqlite — open with sqlite3 and query runs, linear_issues, queued_messages, linear_events. jun config show prints databasePath.
  • ~/.claude/projects/<slug>/<sessionId>.jsonl — Claude's per-session event log (what the watchdog reads).
  • ~/.opencode/, ~/.codex/, ~/.hermes/ — analogous state dirs for the other runners.
  • npm run phase0 — sanity-checks the Linear API contracts and the exact non-interactive Claude command. Useful when something feels broken at the GraphQL level.

If you've gotten this far and you still don't know why something is wrong, the codebase is small enough to read end to end:

On this page