I have 10 bots working on my dev workflow right now. Here is what each one does.
I was mid-deploy when my terminal froze for three seconds. It continued on the wrong branch. A scheduled automation had spotted a new PR comment, run git checkout in my working directory, and switched my branch while the deploy was still in flight. Silently. I watched files land on the device from t

I was mid-deploy when my terminal froze for three seconds. It continued on the wrong branch. A scheduled automation had spotted a new PR comment, run git checkout in my working directory, and switched my branch while the deploy was still in flight. Silently. I watched files land on the device from the wrong build and sat there for a moment not understanding what had just happened. That incident made me rebuild everything. While I was rebuilding, I wrote down every automation I run. This post is that document. Short version: the fix was git worktree. Every automation now gets its own folder, so nothing can ever touch the directory I am typing in. The 10 workflows below all sit on that one idea. The bug and the fix. Why shared directories break, and the one git command that solves it. The shared pattern. All 10 workflows have the same 5-step skeleton. The 10 workflows. What each one does, when it fires, and what surprised me. Build it yourself. A working starter kit you can clone and run today. One note before the code. The snippets in section 3 are simplified sketches. Names like send-notification and fetch-unprocessed-comments stand in for glue I am not going to make you read twice. They show the shape of each workflow, not something you can paste into a terminal. Section 4 has the real, runnable versions. If you just want working code, skip to the end. Every automation I had built started the same way: cd ~/projects/my-repo git fetch origin git checkout <some-branch> # do the work That cd was the problem. Every script operated in the same folder I was sitting in. Two firing at once meant a collision. One firing while I was deploying meant chaos. I had already added a mutex so two automations could not run at the same time. That stopped them fighting each other. It never stopped one fighting me, because the lock only asked "is another automation running?" It never asked "is the human using this folder right now?" Detection cannot reliably answer that question. Separation makes it unnecessary. All three bots converge on ~/projects/my-repo. When one runs git checkout while you are mid-deploy, the deploy picks up the wrong branch's files. No error, no warning. git worktree add creates a second folder attached to the same repository but checked out to its own branch: git worktree add ~/projects/my-repo-automation feature/pr-comment-fix No copying. No cloning. Both folders share one .git object store, so this is close to instant even on a large repo. The important property: checking out a branch in one worktree has no effect on any other. Each automation gets a disposable folder, does its work, and the folder is deleted. My directory is never involved. Requires git 2.5 or newer, which in practice means any git you have. All 10 use the same 5-step skeleton. Step What happens 1. Trigger A schedule or a webhook fires 2. Isolate Create a fresh worktree 3. Work Do the actual task in there 4. Verify Run the build or tests 5. Notify and stop Tell me. Do not commit. Clean up. In code: TICKET_ID="$1" BRANCH="$2" WORKTREE="$HOME/.automation/worktrees/$TICKET_ID" # 2. Isolate git -C ~/projects/my-repo worktree add "$WORKTREE" "$BRANCH" cd "$WORKTREE" # 3 and 4: work and verify happen here # 5. Notify, then stop git worktree remove "$WORKTREE" Step 5 is the one I never bend. Automation does the assembly. I keep the judgment. Reminder: these are sketches. Runnable code is in Part 4. Does: Watches your open PRs for new review comments and attempts the clear ones in an isolated worktree, leaving a diff for you. Fires: Every 5 minutes, only on PRs you authored. For each comment it has not seen before, it reads the comment plus the surrounding file context, decides whether the request is unambiguous, implements it if so, flags it if not, then runs the build. UNPROCESSED=$(fetch-unprocessed-comments "$PR_ID") for COMMENT in $UNPROCESSED; do CONTEXT=$(get-file-context-for-comment "$COMMENT") claude " Comment: $COMMENT File context: $CONTEXT If this is a clear, unambiguous change request: implement it. If it is unclear or subjective: flag it for human review. Run the build. Do not commit. " mark-processed "$COMMENT" done What surprised me: roughly 70% of review comments resolve with no input from me. The other 30% are the ones I actually need to think about. Sorting comments into those two piles turned out to be worth more than the fixes. Does: When a ticket is assigned to you, creates a branch, reads the spec, and starts implementing. Fires: Every 5 minutes, for tickets assigned in the last 2 hours. The 2-hour window matters. Polling for "ever assigned to me" re-triggers on old work forever. JQL="assignee = currentUser() AND updated >= -2h AND status = 'In Progress'" NEW_TICKETS=$(fetch-jql "$JQL" | filter-unprocessed) for TICKET in $NEW_TICKETS; do BRANCH=$(create-branch-for-ticket "$TICKET") WORKTREE="$HOME/.automation/worktrees/$TICKET" git -C ~/projects/my-repo worktree add "$WORKTREE" "$BRANCH" cd "$WORKTREE" SPEC=$(get-ticket-body "$TICKET") claude "Implement this: $SPEC. Run the build. Do not commit." mark-processed "$TICKET" done What surprised me: output quality tracks ticket quality almost exactly. This has quietly made me write better tickets, because I know something is going to try to implement them literally. Does: On a new branch push, reads the diff and writes the PR description. Fires: Webhook on branch push. DIFF=$(git diff "origin/$BASE_BRANCH"...HEAD) COMMITS=$(git log "origin/$BASE_BRANCH"...HEAD --oneline) claude " Diff: $DIFF Commits: $COMMITS Write a PR description: - What changed (plain language, name real files and functions) - Why this change was needed - How to test it, step by step - What reviewers should look at carefully Be specific. No generic filler. " > .pr-description.md Where it earns its keep: the "what reviewers should look at" section. It flags things I would not have thought to call out, and has caught real issues before a reviewer had to. Does: When a ticket hits "Ready for Testing", writes a QA note from the actual diff rather than the ticket text. Fires: Every 5 minutes, on tickets that just transitioned. DIFF=$(git diff origin/main...HEAD) SPEC=$(get-ticket-body "$TICKET_ID") DRAFT=$(claude " Ticket: $SPEC Diff: $DIFF Write a QA handoff note: - Environment setup steps - Happy path test cases - Edge cases (derive these from the diff, not the spec) - Out of scope ") post-staging-comment "$TICKET_ID" "$DRAFT" send-notification "QA note drafted, approve before it goes visible" The guard: it posts to a staging comment and waits for me. A wrong test case is worse than no test case, because someone will trust it. Does: On PR merge, reads the resolved review comments and proposes additions to the team guidelines file. Fires: Webhook on merge. A slow burn. Does nothing visible on day one. COMMENTS=$(get-resolved-comments-for-pr "$PR_ID") CURRENT_GUIDELINES=$(cat GUIDELINES.md) PROPOSED=$(claude " Resolved review comments: $COMMENTS Current guidelines: $CURRENT_GUIDELINES Identify any new convention these comments suggest. Only propose an addition if the pattern is clear and specific. Do not repeat anything already covered. Output the addition only, or nothing. ") if [ -n "$PROPOSED" ]; then echo "$PROPOSED" >> GUIDELINES.md fi What surprised me: after three months this file was better than anything I would have written deliberately, because every rule came from a real argument about real code. Does: Logs your branch, working tree state, and active tickets to a JSON file. Fires: Every 30 minutes. Read-only by design. It never checks anything out. BRANCH=$(git -C ~/projects/my-repo rev-parse --abbrev-ref HEAD) STATUS=$(git -C ~/projects/my-repo status --short) ACTIVE=$(get-my-active-tickets) jq -n \ --arg ts "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \ --arg branch "$BRANCH" \ --arg status "$STATUS" \ --argjson tickets "$ACTIVE" \ '{timestamp: $ts, branch: $branch, dirty: ($status | length > 0), active_tickets: $tickets}' \ >> ~/.automation/work-log.json Why I trust this one most: it cannot break anything. It answers "what was I doing before that meeting?" without me having to remember. Does: On a failed build, finds the first real error rather than the cascade it triggered, and sends you the file, line, and message. Fires: Polls CI every 2 minutes while a build is running. Most CI alerts say "build failed". This one says TypeScript error: `src/auth.ts` line 42, type `string` is not assignable to `number`. BUILD_LOG=$(fetch-build-log "$BUILD_ID") FIRST_ERROR=$(echo "$BUILD_LOG" | claude " Read this CI log. Find the first real error that caused the failure. Ignore downstream failures it triggered. Output: filename, line number, message. One line. ") send-notification "Build failed: $FIRST_ERROR" The insight: CI logs are mostly cascade noise. Digging out the real error costs 30 seconds every single time, which is exactly the kind of tax worth automating away. Does: Reads yesterday's commits and ticket activity and drafts your standup. Fires: 08:45 on weekdays. YESTERDAY=$(date -d "yesterday" +%Y-%m-%d 2>/dev/null || date -v-1d +%Y-%m-%d) COMMITS=$(git -C ~/projects/my-repo log \ --author="$(git config user.name)" \ --after="$YESTERDAY 00:00" --before="$YESTERDAY 23:59" --oneline) claude " Commits yesterday: $COMMITS Open PRs: $(fetch-my-open-prs) Active tickets: $(get-my-active-tickets) Write a three-sentence standup: what I did, what I am on today, blockers. Name real tickets and code areas. No filler. " How I use it: read, edit for 30 seconds, paste. The assembly is automated. The editing is the point. Does: Finds branches and PRs with no activity in 5+ days. Fires: Mondays at 09:00. CUTOFF=$(date -d '5 days ago' +%Y-%m-%d 2>/dev/null || date -v-5d +%Y-%m-%d) STALE_BRANCHES=$(git -C ~/projects/my-repo for-each-ref \ --sort=committerdate refs/remotes/origin \ --format='%(committerdate:short) %(refname:short)' | awk -v cutoff="$CUTOFF" '$1 < cutoff {print $2}') send-notification "Stale work: $(claude "Summarise what needs attention: $STALE_BRANCHES $(fetch-prs-with-no-activity-in-days 5)")" Why it matters: stale work is silent. It never comes up in standup. A scheduled nudge makes it visible without anyone needing to remember to look. Does: When a teammate merges, tells you only if their changes touch files you are currently working on. Fires: Webhook on every merge. MERGED_FILES=$(get-changed-files-in-pr "$PR_ID") MY_FILES=$(git -C ~/projects/my-repo diff --name-only "origin/$BASE_BRANCH"...HEAD) OVERLAP=$(comm -12 <(echo "$MERGED_FILES" | sort) <(echo "$MY_FILES" | sort)) if [ -n "$OVERLAP" ]; then send-notification "PR merged, overlaps your branch: $(echo $OVERLAP | tr '\n' ', ')" fi The filter is the feature: without the overlap check this is noise on every merge. With it, every notification means "you are going to have to rebase, and here is exactly what changed." Everything above rests on about 80 lines of shared glue. Here it is for real, with nothing invented. Clone it: git clone https://github.com/kbhatnagar97/dev-workflow-automations.git cd dev-workflow-automations cp .env.example .env # fill in three values chmod +x workflows/*.sh ./workflows/standup-draft.sh ๐ github.com/kbhatnagar97/dev-workflow-automations (MIT) git 2.5+, jq, and gh (run gh auth login once) Any LLM CLI. The scripts call claude, but anything taking a prompt on -p works. Change the binary name. ntfy on your phone for alerts. Free, no account. This is common.sh. Every workflow sources it. #!/usr/bin/env bash set -euo pipefail [ -f "$(dirname "${BASH_SOURCE[0]}")/.env" ] && . "$(dirname "${BASH_SOURCE[0]}")/.env" : "${REPO_DIR:?not set}"; : "${GH_REPO:?not set}"; : "${NTFY_TOPIC:?not set}" STATE_DIR="${STATE_DIR:-$HOME/.automation/state}" WORKTREE_DIR="${WORKTREE_DIR:-$HOME/.automation/worktrees}" mkdir -p "$STATE_DIR" "$WORKTREE_DIR" # Notifications, no account needed. notify() { curl -fsS -d "$*" "https://ntfy.sh/$NTFY_TOPIC" >/dev/null || true; } # Idempotency. Without this a 5-minute poll redoes the same work forever. seen() { [ -e "$STATE_DIR/$1" ]; } mark_seen() { : > "$STATE_DIR/$1"; } # One automation at a time. mkdir is atomic everywhere, so no flock needed. lock() { local dir="$STATE_DIR/.lock" mkdir "$dir" 2>/dev/null || { echo "another automation is running"; exit 0; } # Expand $dir now: it is local and gone by the time the trap fires. trap "rmdir '$dir' 2>/dev/null || true" EXIT } # The whole point: run something in a throwaway worktree, always clean up. in_worktree() { local name="$1" branch="$2"; shift 2 local wt="$WORKTREE_DIR/$name" rc=0 git -C "$REPO_DIR" fetch --quiet origin 2>/dev/null || true git -C "$REPO_DIR" worktree add --force "$wt" "$branch" >/dev/null ( cd "$wt" && "$@" ) || rc=$? git -C "$REPO_DIR" worktree remove --force "$wt" >/dev/null 2>&1 || true return "$rc" } Four things worth pointing out, because each one is a bug I actually hit: fetch must not be fatal. With set -e, a failed fetch (offline, or no remote) kills the whole run before your work starts. Hence || true. The trap uses double quotes. $dir is a local, so it no longer exists when the trap fires at exit. Expand it at definition time or the lock never releases. rc is captured, not inherited. set -e would abort before the cleanup line and leak worktrees. seen/mark_seen are not optional. They are the difference between a useful poll and an infinite loop. Workflow 1 from above, for real this time: #!/usr/bin/env bash set -euo pipefail . "$(dirname "$0")/../common.sh" lock gh pr list --repo "$GH_REPO" --author "@me" --state open \ --json number,headRefName --jq '.[] | @json' | while read -r pr; do num=$(jq -r '.number' <<<"$pr") branch=$(jq -r '.headRefName' <<<"$pr") gh api "repos/$GH_REPO/pulls/$num/comments" \ --jq '.[] | {id, path, body} | @json' | while read -r comment; do id=$(jq -r '.id' <<<"$comment") if seen "comment-$id"; then continue; fi path=$(jq -r '.path' <<<"$comment") body=$(jq -r '.body' <<<"$comment") in_worktree "pr-$num" "$branch" \ claude -p "A reviewer left this on $path: $body Implement it only if it is a clear, unambiguous change request. If it is subjective or ambiguous, change nothing and say why. Do not commit anything." mark_seen "comment-$id" notify "PR #$num: looked at a comment on $path. Review the diff." done done */5 * * * * cd /path/to/dev-workflow-automations && ./workflows/pr-comment-resolver.sh cron runs with a minimal PATH. If a script cannot find gh or jq, set PATH explicitly at the top of your crontab. This is the single most common reason a script that works in your terminal does nothing on a schedule. Start with standup-draft.sh. It is read-only. It cannot damage anything. Add the writing ones once you have watched the isolation work. Your ntfy topic is a password. Anyone who guesses it reads your alerts. Make it random. Treat PR comments as untrusted input. You are feeding text written by other people straight into a model that can edit your code. A comment can contain instructions aimed at the model rather than at you. This is exactly why nothing commits automatically. Watch it for a week before trusting it. Every one of these started as a script that just printed what it would have done. Every automation here does three things: Works in an isolated worktree, never in your directory Runs in the background without blocking you Stops before committing anything that matters The third is the one I never break. The machine does the assembly. I keep the judgment. What are you still doing by hand that a script could pick up? Tell me below, I am genuinely curious what I have not thought of.
Key Takeaways
- โขI was mid-deploy when my terminal froze for three seconds. It continued on the wrong branch. A scheduled automation had spotted a new PR comment, run git checkout in my working directory, and switched my branch while the deploy was still in flight
- โขThis story was reported by Dev.to, covering developments in the dev space.
- โขAI advancements continue to reshape industries โ read the full article on Dev.to for complete coverage.
๐ Continue reading the full article:
Read Full Article on Dev.to โShare this article


