Over the last few months I wrote several posts about my development workflow.
Some focused on the local environment. Others focused on Git, GitHub, and AI tools. The common idea was the same: AI can be part of the process, but it should not replace the process.
In the post about running VS Code, WSL, Conda, and unittest, I focused on the local development environment.
In the post about a Git workflow for CSM Researchers, I described a structured process around branches, commits, and pull requests.
Later, in the post about Codex CLI with Git worktree and Docker Compose, I used a more practical setup. The goal was to run AI-assisted work inside an isolated development environment.
After a few rounds of working this way, another recurring layer became clear.
This was especially visible with changes generated by AI, or delivered as ZIP files.
The problem was not the local environment itself. It was not only the code either. It was the layer that manages the lifecycle of a change.
That lifecycle is repetitive: start from a clean branch, apply the change, review the diff, run tests, commit, open a PR, merge, sync main, clean up branches, and sometimes create a tag.
Each step is simple on its own.
The problem is the full sequence. It repeats again and again. Each repetition leaves enough room for a small mistake.
The problem was not lack of Git knowledge.
I knew how to run the commands manually.
The problem was repetition. Every task required the same sequence, in the same order, with the same checks in between.
For example:
mainmainThese are not complex problems.
They are the kind of problems that are easy to miss. Especially when working quickly, switching between projects, or applying changes from an external process.
After a few rounds, the pattern became clear:
prepare -> change -> review -> test -> commit -> PR -> merge -> cleanup -> tag
In practice, it looked roughly like this:
1. Make sure main is up to date and clean
2. Create a dedicated branch for the task
3. Apply a ZIP file or make a manual change
4. Review the changes
5. Run tests
6. Create a controlled commit
7. Push the branch and open a PR
8. Merge through GitHub
9. Sync local main
10. Clean up branches
11. Create a tag for a stable version
This is the basis for ai-git-workflow-tools.
https://github.com/sagivba/ai-git-workflow-tools
I did not want a tool that hides Git. I wanted a tool that holds the process around Git.
ai-git-workflow-tools is a set of Bash functions.
It is meant for repeated work around Git, GitHub, AI-assisted changes, and ZIP files.
It does not try to replace Git.
It also does not try to merge automatically.
Its purpose is narrower: run the same workflow consistently, with checkpoints and validations that prevent simple mistakes.
The main principles are:
dry-run by defaultmainEvery operation that can change Git state first prints what it is going to do.
Only when I add --run does the operation actually execute.
For example:
agw_start_task --task T008 --slug some-task
# roughly:
# git fetch --prune origin && \
# git switch main && \
# git pull --ff-only origin main && \
# git status --short && \
# git log --oneline --decorate -5 && \
# git switch -c manual/T008-some-task
This command does not create a branch.
I did not add --run, so it only prints the sequence that would run in execution mode.
In this case, behind the scenes, the tool builds a branch name from two parts:
manual/T008-some-task
Then it plans to run a sequence like this:
git fetch --prune origin
git switch main
git pull --ff-only origin main
git status --short
git log --oneline --decorate -5
git switch -c manual/T008-some-task
In --run mode, the tool performs the checks first.
It checks that the working tree is clean. It also checks that a local or remote branch with the same name does not already exist.
If one of the checks fails, the operation stops.
To execute it for real:
agw_start_task --task T008 --slug some-task --run
# roughly:
# git fetch --prune origin && \
# git switch main && \
# git pull --ff-only origin main && \
# test -z "$(git status --short)" && \
# git show-ref --verify --quiet refs/heads/manual/T008-some-task || \
# true && \
# git switch -c manual/T008-some-task
Note: a
slugis a short, readable name that is safe to use in a branch name or URL.Instead of a sentence with spaces and special characters, it uses lowercase letters, numbers, and hyphens.
For example,
Update README examplesbecomesupdate-readme-examples.
The slug is the short readable part of the branch name. In this case, some-task. It briefly describes the task using lowercase letters, numbers, and hyphens.
For example:
agw_start_task --task T012 --slug update-readme-examples --run
# roughly:
# git fetch --prune origin && \
# git switch main && \
# git pull --ff-only origin main && \
# test -z "$(git status --short)" && \
# git switch -c manual/T012-update-readme-examples
Will create a branch named:
manual/T012-update-readme-examples
I use a slug when I have a task number or work identifier.
The task number gives traceability. The slug gives human context.
Together they make the branch easy to understand in git branch, GitHub, or a list of PRs.
This sounds like a small detail, but in practice it is one of the most important parts of the tool. I want to see the operation before it changes the repository.
The tool encourages working in a dedicated branch for every change.
The naming pattern I use is:
manual/T<number>-short-slug
For example:
manual/T008-some-task
Creating a branch is done like this:
agw_start_task --task T008 --slug some-task --run
# roughly:
# git fetch --prune origin && \
# git switch main && \
# git pull --ff-only origin main && \
# test -z "$(git status --short)" && \
# git switch -c manual/T008-some-task
Before creating the branch, the tool checks that the repository is in a suitable state.
It checks that I am in the expected repository. It checks that the working tree is clean. It also avoids starting a new task on top of an unclear state.
One thing that happened repeatedly was a ZIP file sitting at the root of the repository.
From Git’s perspective, that is an untracked file.
Even if the file is only temporary input, it still makes the working tree dirty.
So before starting a task, I move it out of the repository, for example:
mkdir -p /tmp/agw-zips
mv T008-some-task.zip /tmp/agw-zips/
# equivalent:
# mkdir -p /tmp/agw-zips && \
# mv T008-some-task.zip /tmp/agw-zips/
This is not a new idea introduced by the tool. It is simply a check that the tool forces me not to skip.
One of the easiest mistakes is to run:
git add .
In a small project this sometimes works fine.
But with ZIP files, temporary outputs, or AI-generated changes, I prefer to be explicit.
So the tool provides a commit function where the files are listed explicitly:
agw_commit_controlled_change \
--message "Describe the change" \
--files "file1 file2" \
--run
# roughly:
# git status --short && \
# git add file1 file2 && \
# git diff --cached --stat && \
# git commit -m "Describe the change"
The goal is not to save typing. The goal is to avoid accidentally including unrelated files in the commit.
A basic workflow looks like this.
The important part is understanding what each command actually does.
Load the tool functions into the current shell:
source scripts/load-agw.sh
# equivalent:
# test -f scripts/load-agw.sh && \
# source scripts/load-agw.sh
This command does not change the repository.
It only loads the functions. It also lets the tool know where it was loaded from, and which repository is current.
After loading, check the tool and repository state:
agw_status
# roughly:
# agw_version && \
# pwd && \
# git rev-parse --show-toplevel && \
# git branch --show-current && \
# git status --short
This prints basic information.
It includes the tool version, the path it was loaded from, and the current repository.
This helps verify that the tool is operating on the correct project. Not on the tool repository itself, and not on another directory.
Create a new branch for the task:
agw_start_task --task T008 --slug some-task --run
# roughly:
# git fetch --prune origin && \
# git switch main && \
# git pull --ff-only origin main && \
# test -z "$(git status --short)" && \
# git switch -c manual/T008-some-task
In practice, the command performs a few checks.
It checks that the working tree is clean. It makes sure I am not starting from an unclear state. Then it creates a predictable branch name:
manual/T008-some-task
Without --run, the tool only prints the expected operations. With --run, it executes them.
At this point I apply the actual change. It can be a manual edit, code generated by AI, or files extracted from a ZIP:
# apply changes manually or unzip task files
# example:
# unzip /tmp/T008-some-task.zip -d . && \
# git status --short
After the change is in the repository, review what changed:
agw_review_output --run
# roughly:
# git status --short && \
# git diff --stat && \
# git diff
This command groups the first checks around the change.
In practice, it shows the Git state and the diff. This lets me understand which files changed before moving on to a commit.
Then run the project tests:
make test
# equivalent: make test
This is not a tool command.
It is the project’s validation point. In another project it might be pytest, npm test, or another local test command.
Only after review and tests do I create a controlled commit:
agw_commit_controlled_change \
--message "Describe the change" \
--files "file1 file2" \
--run
# roughly:
# git status --short && \
# git add file1 file2 && \
# git diff --cached --stat && \
# git commit -m "Describe the change"
Instead of running git add ., the command receives an explicit list of files through --files.
It stages only those files. Then it creates a commit with the provided message.
This avoids adding unrelated files by mistake.
Then create a PR body, for example:
cat > /tmp/T008-pr-body.md <<'EOF'
## Summary
- Describe the change.
## Validation
- `make test`
EOF
# equivalent:
# cat > /tmp/T008-pr-body.md <<'EOF' && \
# test -s /tmp/T008-pr-body.md
This is also not a tool command.
It is a temporary text file for the Pull Request body. I prefer creating it as a file, so I can read and edit it before opening the PR.
Then push the branch and open a PR:
agw_push_and_pr \
--title "T008 Describe the change" \
--body-file /tmp/T008-pr-body.md \
--run
# roughly:
# git push -u origin HEAD && \
# gh pr create \
# --title "T008 Describe the change" \
# --body-file /tmp/T008-pr-body.md
In practice, the command pushes the branch to the remote.
Then it uses the GitHub CLI to open a Pull Request with the provided title and body.
Here too, without --run, it prints the expected commands and does not execute them.
The merge itself still happens through GitHub.
That is intentional. I do not want the tool to merge automatically. The PR is the last checkpoint before the change enters main.
After the PR is merged through GitHub, sync local main:
agw_post_merge_sync --run
# roughly:
# git switch main && \
# git pull --ff-only origin main && \
# git log --oneline --decorate -5
Then clean up branches:
agw_cleanup_branches --branch manual/T008-some-task --run
agw_cleanup_branches --branch manual/T008-some-task --remote --run
# roughly:
# git branch --merged main | grep manual/T008-some-task && \
# git branch -d manual/T008-some-task && \
# git push origin --delete manual/T008-some-task
Only after the merge and sync are complete, create a tag:
agw_create_tag --tag v0.7.0 --note "Describe the change" --run
# roughly:
# git switch main && \
# git pull --ff-only origin main && \
# git rev-parse v0.7.0 >/dev/null 2>&1 || \
# git tag -a v0.7.0 -m "Describe the change" && \
# git push origin v0.7.0
Here too, the tool is not trying to be clever. It simply keeps the order correct.
One point was important to me.
The tool should be loadable from one place, but operate on the project I am currently in.
For example:
source tools/ai-git-workflow-tools/scripts/load-agw.sh
agw_status
# roughly:
# test -f tools/ai-git-workflow-tools/scripts/load-agw.sh && \
# source tools/ai-git-workflow-tools/scripts/load-agw.sh && \
# agw_version && \
# git rev-parse --show-toplevel && \
# git status --short
The status output shows two things.
It shows where the tool was loaded from. It also shows the current repository on which the commands operate.
This matters because I do not want to confuse the tool repository with the repository I am actually working on.
There are two simple ways to integrate it into another project.
Using a submodule:
git submodule add https://github.com/sagivba/ai-git-workflow-tools.git tools/ai-git-workflow-tools
cd tools/ai-git-workflow-tools
git checkout v0.6.0
cd ../..
source tools/ai-git-workflow-tools/scripts/load-agw.sh
# equivalent:
# git submodule add https://github.com/sagivba/ai-git-workflow-tools.git tools/ai-git-workflow-tools && \
# cd tools/ai-git-workflow-tools && \
# git checkout v0.6.0 && \
# cd ../.. && \
# source tools/ai-git-workflow-tools/scripts/load-agw.sh
Or by copying the files:
mkdir -p tools/ai-git-workflow-tools/scripts
cp ~/src/ai-git-workflow-tools/scripts/ai-git-workflows.sh \
tools/ai-git-workflow-tools/scripts/
cp ~/src/ai-git-workflow-tools/scripts/load-agw.sh \
tools/ai-git-workflow-tools/scripts/
source tools/ai-git-workflow-tools/scripts/load-agw.sh
# equivalent:
# mkdir -p tools/ai-git-workflow-tools/scripts && \
# cp ~/src/ai-git-workflow-tools/scripts/ai-git-workflows.sh tools/ai-git-workflow-tools/scripts/ && \
# cp ~/src/ai-git-workflow-tools/scripts/load-agw.sh tools/ai-git-workflow-tools/scripts/ && \
# source tools/ai-git-workflow-tools/scripts/load-agw.sh
The tool was not built to hide the process.
It was built to make it clearer.
That is why there are several things it intentionally does not do:
git add .--runIn this case, guardrails matter more than full automation.
I still want to see the diff. I still want to run tests. I still want the merge to be a deliberate action through GitHub.
The tool only makes sure I do not skip steps that repeated enough times to justify an automatic check.
ai-git-workflow-tools is currently a personal workflow tool that reached a useful state.
It is not a complete distribution-ready product.
It is also not a general-purpose CLI library.
It was built from a real workflow. It mostly fits people who work in a similar way: one branch per task, PRs through GitHub, manual merge, cleanup, and sometimes a tag at the end.
The project is here:
https://github.com/sagivba/ai-git-workflow-tools
Future development will probably come from using it in more real projects, not from planning more features up front.