leftibot is the automated assistant of Jason Turner (@lefticus).
It watches the GitHub notifications of the leftibot account. When @lefticus
@-mentions @leftibot on an issue or pull request, it hands the task to
Claude Code, checks the result, and publishes
it as a pull request or a comment.
leftibot only acts on mentions from @lefticus. Mentions from anyone else are ignored and their notifications are marked read.
Each run of bin/leftibot-tick.sh (a "tick") picks up one task, in this
priority order:
- PR review: @lefticus left feedback (not a question) on one of leftibot's own open PRs. leftibot addresses the feedback and pushes new commits to the PR branch.
- Comment reply: @lefticus asked leftibot a question (the mention contains
a
?) on an issue or PR. leftibot answers with a comment and pushes no code. Mentions on PRs that leftibot didn't open are always handled this way. - New issue: @lefticus told leftibot to fix an issue. leftibot forks the repository, writes a failing regression test, makes the fix, and opens a PR.
- Fork
owner/repotoleftibot/repoif needed, clone it intowork/<owner>-<repo>-<number>, and sync the fork's default branch with upstream. - Create a branch named
fix/issue-<number>-<slug>. - Fill in the
prompts/fix-issue.mdtemplate with the issue, the thread, the latest @lefticus directive, and build instructions for the detected build system (CMake, Cargo, npm, or Python), then run Claude Code on it. - Check the result:
- at least one commit was made,
- the diff touches a test or spec file,
- for CMake projects, the project configures, builds, and
ctestpasses.
- Publish:
- all checks pass → a regular PR;
- no test, broken build, or failing tests → a draft PR that says why;
- no commits → a comment on the issue saying leftibot gave up.
The @lefticus directive overrides the issue body. The issue body and other comments go into the prompt as untrusted reference material.
Attempts are recorded in state/seen-issues.json. Issues that already ended in
a PR are not retried. Issues that are in progress or failed are retried only
after a 24-hour cooldown.
| Path | Purpose |
|---|---|
bin/leftibot-tick.sh |
Runs one tick: poll notifications, do one task, exit. |
bin/leftibot-loop.sh |
Runs ticks repeatedly in the foreground. |
prompts/fix-issue.md |
Prompt template for fixing a new issue. |
prompts/address-review.md |
Prompt template for addressing PR review feedback. |
prompts/comment-reply.md |
Prompt template for answering a question in a comment. |
logs/, work/, state/ |
Created at runtime; ignored by git. |
Templates use {{KEY}} placeholders, which are filled in by render_template
in bin/leftibot-tick.sh.
The tick script checks for these commands at startup and exits if any are missing:
gh: the GitHub CLI, logged in as theleftibotusergit, with SSH push access to leftibot's forksclaude: the Claude Code CLIjqcmake: used to check CMake projectsflocktimeoutpython3: used to fill in prompt templates
leftibot expects to live in ~/leftibot. Set LEFTIBOT_HOME to use a
different location.
git clone git@github.com:leftibot/leftibot.git ~/leftibot
gh auth login # log in as leftibotTry a dry run first. It renders the prompt into logs/ and stops before
invoking Claude, forking, pushing, or commenting:
~/leftibot/bin/leftibot-tick.sh --verbose --dry-run
~/leftibot/bin/leftibot-tick.sh --verbose --issue https://github.com/foo/bar/issues/42 --dry-run
~/leftibot/bin/leftibot-tick.sh --verbose --pr https://github.com/foo/bar/pull/99 --dry-run| Flag | Effect |
|---|---|
--verbose, -v |
Print debug info, including the end of Claude's output. |
--dry-run |
Don't fork, push, open PRs, post comments, or mark notifications read. |
--issue <URL> |
Skip the notification poll and work on this issue. |
--pr <URL> |
Skip the notification poll and address review on this PR. |
--skip-trigger-check |
Skip the "must be @-mentioned by @lefticus" check (testing only). |
--no-cleanup |
Keep the workdir for inspection (it is deleted by default). |
-h, --help |
Print usage. |
Exit codes: 0 means no work was found, 10 means a task was processed, and
any other value is an error.
Run ticks in a loop in the foreground:
~/leftibot/bin/leftibot-loop.sh # default 15 minute interval
~/leftibot/bin/leftibot-loop.sh 5m -v # custom interval; later args go to the tickWhen a tick processes a task, the loop starts the next tick right away. When there is no work, or the tick fails, it sleeps for the interval.
Or install a cron entry:
*/15 * * * * flock -n ~/leftibot/state/tick.lock ~/leftibot/bin/leftibot-tick.sh >> ~/leftibot/logs/tick-$(date +\%Y-\%m-\%d).log 2>&1The loop and the cron entry share state/tick.lock, so both can be installed
at once without running two ticks together.

