현재 10개의 봇이 개발 워크플로를 작업하고 있습니다. 각자가 하는 일은 다음과 같습니다.

작성자

카테고리:

← 피드로
DEV Community · kshitij Bhatnagar · 2026-09-17 개발(SW)

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.

How to read this post

  1. The bug and the fix. Why shared directories break, and the one git command that solves it.
  2. The shared pattern. All 10 workflows have the same 5-step skeleton.
  3. The 10 workflows. What each one does, when it fires, and what surprised me.
  4. 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.

Part 1: Why it broke, and the one git command that fixed it

Every automation I had built started the same way:

cd ~/projects/my-repo
git fetch origin
git checkout <some-branch>
# do the work

Enter fullscreen mode Exit fullscreen mode

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.

The problem: one folder, multiple writers

Before: three automation scripts all running git checkout in the same working directory, causing a collision

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.

The fix: give every bot its own folder

After: each automation script gets its own isolated worktree, leaving the main directory completely untouched

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

Enter fullscreen mode Exit fullscreen mode

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.

Part 2: The pattern every workflow follows

All 10 use the same 5-step skeleton.

The 5-step automation pipeline: Trigger, Isolate, Work, Verify, Notify and stop

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"

Enter fullscreen mode Exit fullscreen mode

Step 5 is the one I never bend. Automation does the assembly. I keep the judgment.

Part 3: The 10 workflows

Reminder: these are sketches. Runnable code is in Part 4.

1. PR Comment Resolver

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

Enter fullscreen mode Exit fullscreen mode

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.

2. Ticket Implementer

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

Enter fullscreen mode Exit fullscreen mode

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.

3. PR Description Generator

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

Enter fullscreen mode Exit fullscreen mode

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.

4. QA Handoff Note Generator

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"

Enter fullscreen mode Exit fullscreen mode

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.

5. Best Practices Auto-Learner

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

Enter fullscreen mode Exit fullscreen mode

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.

6. Work State Tracker

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

Enter fullscreen mode Exit fullscreen mode

Why I trust this one most: it cannot break anything. It answers “what was I doing before that meeting?” without me having to remember.

7. Build Failure Notifier

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"

Enter fullscreen mode Exit fullscreen mode

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.

8. Daily Standup Draft

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.
"

Enter fullscreen mode Exit fullscreen mode

How I use it: read, edit for 30 seconds, paste. The assembly is automated. The editing is the point.

9. Stale Work Detector

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)")"

Enter fullscreen mode Exit fullscreen mode

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.

10. Relevant Merge Notifier

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

Enter fullscreen mode Exit fullscreen mode

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.”

Part 4: Build it yourself

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

Enter fullscreen mode Exit fullscreen mode

👉 github.com/kbhatnagar97/dev-workflow-automations (MIT)

What you need

  • 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.

The glue layer

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"
}

Enter fullscreen mode Exit fullscreen mode

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.

A complete workflow

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

Enter fullscreen mode Exit fullscreen mode

Scheduling

*/5 * * * * cd /path/to/dev-workflow-automations && ./workflows/pr-comment-resolver.sh

Enter fullscreen mode Exit fullscreen mode

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.

Before you turn any of this on

  • 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.

The one rule across all 10

Every automation here does three things:

  1. Works in an isolated worktree, never in your directory
  2. Runs in the background without blocking you
  3. 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.

원문에서 계속 ↗