What
Lunchpad is a Python script that runs on a systemd timer and polls our Plane Postgres database every minute. It looks for deploy requests in task comments, works out which repository and branch they refer to, and performs the full sequence: fetch, merge, push, run the deploy script, and post the result back to the task.
A comment like “deploy this to dev” on a Plane task is enough. The rest happens without anyone touching git.
The judgment is two-layer. A cheap keyword filter runs first. Only comments that look like deploy requests reach DeepSeek, which makes the final call. If the LLM says no, nothing happens. If it says yes, the comment enters a deploy queue and a worker takes it through merge and deployment.
None of that is the interesting part. The interesting part is the guardrails. A bot that can merge to main and deploy production code is a loaded gun, and every safety decision in Lunchpad exists because a specific way of getting shot was identified in advance.
Why
Before Lunchpad, a finished task worked like this. A developer finished work on a branch, pushed it, and commented “deploy this” on the Plane task. Then a human had to read the comment, find the right repository, identify the source and target branch, merge, push, run the deploy script, and reply to the task. The work was done; the ceremony took another twenty minutes.
The twenty minutes are the small cost. The split is the real one.
A team always runs two systems. The code and its review conversation live in git. The work and its planning conversation live in the task manager. A branch, a pull request, and a task are three views of the same piece of work, and each view holds a different slice of the story. The two systems almost never talk to each other, so the story never assembles itself anywhere.
The damage shows up later, when somebody asks why a decision was made. Part of the answer sits in a PR thread nobody reopened, part in task comments, part in a merge commit nobody reads. The information was never lost. It was scattered, which is worse, because scattered information looks findable and keeps failing to be found.
Two ways out exist. One is integration: keep both systems and build bridges so they stay in sync. The other is demotion: pick one system as the interface and let the other stop being a place where humans work. We chose demotion.
The task manager is the primary system. The task is where work gets discussed, decided, and closed. Git gets demoted to disk storage that happens to have merge machinery. A developer does not need to know where the repository lives or what the branch is called; the task knows, or can find out. The merge, the push, the deploy are just things that happen when the task says deploy. Git becomes a black box behind the task manager.
Demotion has a cost, though. The git operations still have to be performed by somebody. If the human stays in the task manager and a machine handles git, the machine now works in a place no human is watching anymore. That is the trade Lunchpad makes, and it is the reason every design decision in the next section exists. The guardrails are not extras bolted onto a deploy bot. They are the price of taking humans out of git.
Automating the ceremony is easy. Automating it safely is where the design questions start.
Who is allowed to deploy to main? What happens when the merge conflicts? What happens when the push is rejected? What happens when the bot crashes halfway through a deploy? And the one nobody thinks about first: what stops the bot from reading its own “Deployed to dev” reply as a fresh deploy request?
Each of those questions has an answer in the code, and each answer was chosen deliberately.
How
Two-layer judgment
The bot’s only input is the task conversation, and that conversation is natural language. People write “deploy it”, not “git push origin development”. Somebody still has to translate between the two.
The trigger keywords are deploy, merge, dev, push, ready, done, build, release, pull, and approval. A comment must contain one of them to move on. Anything else is dropped without an API call.
That filter is cheap and dumb on purpose. Most Plane comments are discussion, not deploy requests, and sending every comment to an LLM would burn tokens on noise. The LLM only sees candidates, which keeps cost proportional to the number of real deploy requests instead of the total comment volume.
The LLM call is where the judgment gets flexible. “deploy it” resolves to the default working branch. “deploy the backend” picks between multiple repositories in the same project folder. “merge dev to main” names both branches explicitly. The keyword filter cannot do that; the LLM can. It is the translator that turns the way people talk in tasks into the way git wants to be addressed.
Resolving the repository and branch
If git is infrastructure, its layout is the bot’s problem, not the team’s. The bot discovers repositories at deploy time, no static list. It scans the project folder two levels deep. Most projects have one repository and the selection is automatic. A project with a frontend, a backend, and a portfolio site has several.
With multiple repositories, an explicit comment wins. “deploy the backend” tells the LLM which one to pick. A vague “deploy it” does not, so the bot comments back listing the repositories and asks the developer to say which one. Zero repositories found means the task is marked done and nothing else happens.
Branch resolution works the same way. The LLM reads the comment and proposes a source and target. The source defaults to the issue identifier, a branch name like REMITALL-9. The target defaults to the working branch, development for most projects.
The source branch is then verified against the remote before any merge. The bot looks for an exact match first, then a numeric-suffix fallback by issue number. If the branch is not in the resolved repository at all, the deploy is skipped, the task is marked done, and a comment names the missing branch and repository:
Couldn’t find branch
BDISECURIT-24in thebdi-securitiesrepo. Push the branch (or check its name) and comment again.
A developer who forgot to push gets a visible answer instead of a silent skip. Git’s failure is reported where the team actually looks.
Branch permissions by identity
The LLM resolves both the source and target branch from each comment. The target determines who may trigger it.
| Target branch | Who can trigger |
|---|---|
| dev / development | Anyone |
| sandbox | Sagar only |
| main / master | Sagar only |
If a non-Sagar user asks for main, the request is rejected and the bot comments back: “Merging to main is restricted. Only Sagar can do this.”
Notice where the check happens. It happens in the task manager, on the task manager’s identity, before any git command runs. When the task is the only interface, the task’s author is the only authority the system recognizes.
This is why the bot posts as its own Plane account, taskbot, and never uses Sagar’s token. Sagar’s identity is the authorization mechanism. If the bot borrowed Sagar’s credentials, every comment would appear to come from Sagar, and the permission check would approve everything. The separation of identities is what keeps the permission check meaningful.
The self-echo filter
Comments authored by taskbot are excluded at ingest. This is load-bearing, not cosmetic.
Once the conversation is unified in the task manager, the bot’s own replies become part of the conversation. And the result comments contain the trigger words. “Deployed REMITALL-9 to dev” has both deploy and dev in it. Without the filter, the bot would read its own success reply as a new deploy request, deploy again, post another reply, and answer itself forever. The filter stops the bot from hearing its own voice and mistaking an answer for a request.
Git safety
Each deploy runs a fixed sequence with return-code checks.
git fetch --all --prune
git checkout <target-branch>
git pull origin <target-branch>
git merge origin/<source-branch> --no-edit
Three outcomes get handled explicitly. If the merge succeeds and HEAD changed, the bot pushes and proceeds to deploy. If the merge succeeds with no changes, the branch was already merged, so the deploy is skipped and the task marked done. If the merge conflicts, the bot aborts the merge and posts a comment naming the conflicting files. It never attempts to resolve a conflict, because resolving a conflict is a decision about the work, and decisions belong to the task, not to git.
A push rejection gets the same treatment. The bot resets to the pre-merge HEAD, marks the task failed, and comments. No half-merged state is left on the target branch.
A state machine that survives crashes
Every comment moves through a state machine in the state file.
registered -> keyword_checking -> llm_pending -> deploy_queued -> deploying -> done / failed
Two states are terminal: done and failed. Terminal entries get purged on the next cycle.
The recovery rules matter more than the states. If a comment sits in Phase 1 (judgment) for more than 10 minutes, it resets to registered and gets judged again. If it sits in Phase 2 (deploy) for more than 30 minutes, it resets to failed.
This is a direct consequence of taking humans out of git. When a human runs a deploy and the terminal dies, the human is standing there and cleans up. When a bot dies, it leaves silence. Nobody is watching the git side anymore, so the bot has to guarantee its own recovery. A crash at any point cannot wedge a project: the next timer run finds the stuck entries and resets them.
Locks only where they are needed
The state file is the coordination mechanism. A file lock guards it, but the lock is held only for brief state reads and writes. LLM calls take up to 15 seconds each, and git operations take longer. Both run outside the lock.
That split lets different projects run their LLM judgment in parallel while state stays consistent. One slow LLM call never blocks another project’s deploy.
The actual deploy
Once the merge is pushed, the bot looks for a deploy script in the repository. Ours are named per environment, scripts/deploy-development.sh and similar. If the script exists, it runs in the background with a timestamped log per deploy under /tmp/lunchpad/. If no script exists, the project deploys through CI, and the push alone was the whole job.
Either way, the bot posts the result back to the Plane task in plain language: deployed, or why not. The task is the only place anyone looks, so the task is where the result lands.
What this does not do
The LLM judgment can be wrong. A sarcastic comment containing “deploy” might still pass both layers. The protection against that is visibility: every decision posts a comment back to the task, so a wrong deploy is immediately attributable and reversible, not silent.
The project folder mapping is the off switch. Lunchpad only deploys projects listed in its configuration. A Plane project that is not mapped is never touched, no matter what its comments say. Adding a project to the mapping opts it into automatic deploys; removing it turns them off. No code change in either direction.
Lunchpad also does not build anything. It runs the same deploy script a human would run. The bot removed the ceremony, not the pipeline.
Demotion is not free either. Everything git knows that never makes it into a comment becomes invisible. That is why the bot reports so much back: every skip, every conflict, every missing branch becomes a line in the task, so the task stays an honest record of what actually happened in the machinery behind it.