CGW uses a two-branch model. Create development before starting work:
git checkout -b development
git push -u origin developmentNote:
git push -u origin developmentabove is a one-time bootstrap exception — CGW isn't configured yet at this point so the wrapper scripts aren't available. All subsequent pushes should use./scripts/git/push_validated.sh.
Keep main as the GitHub default branch. Charlie reads its config from the default branch.
Conventional commit format (enforced by commit_enhanced.sh):
feat: add user authentication
fix: resolve memory leak in pipeline
docs: update API reference
chore: bump dependencies
test: add unit tests for parser
refactor: extract validation logic
style: fix formatting
perf: optimize image resizing
Add project-specific prefixes via CGW_EXTRA_PREFIXES="cuda|tensorrt" in .cgw.conf. Branches
that target another project with its own message style (e.g. an upstream PR branch) can skip
this check entirely via CGW_FREEFORM_MESSAGE_BRANCHES="up/*". The source, target, and any
protected branch are never exempted this way, even by a glob as broad as "*".
commit_enhanced.sh enforces Pro Git commit guidelines on the subject line (the text following the prefix):
- Soft length (50 characters): Subjects exceeding
CGW_COMMIT_SUBJECT_SOFT_LEN=50display an advisory tip. - Hard length (72 characters): Subjects exceeding
CGW_COMMIT_SUBJECT_HARD_LEN=72block the commit (prompting confirmation in interactive mode or aborting non-interactively) whenCGW_ENFORCE_SUBJECT_LENGTH=1. SetCGW_ENFORCE_SUBJECT_LENGTH=0to make this advisory-only.
# Auto-detect .venv
./scripts/git/commit_enhanced.sh "feat: add feature"
# Skip .venv (use system lint tool)
./scripts/git/commit_enhanced.sh --no-venv "feat: add feature"
# Non-interactive (CI/CD, Claude Code, Antigravity)
./scripts/git/commit_enhanced.sh --non-interactive "feat: add feature"
# Stage only specific paths, then commit
./scripts/git/commit_enhanced.sh --only src/foo.py --only src/bar.py "fix: narrow fix"
# Commit pre-staged files only (skip auto-staging)
./scripts/git/commit_enhanced.sh --staged-only "fix: pre-staged only"
# Force-stage all tracked changes (override pre-staged respect)
./scripts/git/commit_enhanced.sh --all "chore: bulk update"
# Skip lint checks (all)
./scripts/git/commit_enhanced.sh --skip-lint "feat: add feature"
# Skip markdown lint only
./scripts/git/commit_enhanced.sh --skip-md-lint "docs: update readme"
# Force interactive mode even without a TTY
./scripts/git/commit_enhanced.sh --interactive "feat: review before commit"Staging behaviour (non-interactive):
- Pre-staged files exist + unstaged changes exist → commits pre-staged only (warns about skipped files). Use
--allto override. - Nothing pre-staged → auto-stages all tracked changes (legacy behaviour).
--only <path>→ resets index, stages listed paths only.
Staged-Blob Congruence Guard:
commit_enhanced.sh validates code and markdown on the working tree, but git commit records index blobs. If a staged file's blob diverges from the disk content that was validated (e.g. from an un-staged format auto-fix or partial git add -p), the congruence guard fails closed (CGW_ALLOW_STAGED_DIVERGENCE=0) rather than committing unvalidated code. Set CGW_ALLOW_STAGED_DIVERGENCE=1 only if you explicitly intend to record divergent staged blobs. See ADR-0001.
./scripts/git/merge_with_validation.sh --dry-run # preview
./scripts/git/merge_with_validation.sh --non-interactive # executeAll four promotion scripts accept --source/--target to override the configured branch pair for a single invocation without modifying .cgw.conf:
# Merge hotfix → release branch (not the usual development → main)
./scripts/git/merge_with_validation.sh --source feature/hotfix --target release/1.2 --dry-run
./scripts/git/merge_with_validation.sh --source feature/hotfix --target release/1.2 --non-interactive
# Cherry-pick to a release branch
./scripts/git/cherry_pick_commits.sh --source feature/hotfix --target release/1.2 --commit abc1234
# Docs-only merge to a custom target
./scripts/git/merge_docs.sh --source feature/hotfix --target release/1.2 --non-interactive
# Open PR for a non-default pair
./scripts/git/create_pr.sh --source feature/hotfix --target release/1.2 --dry-runThe overrides are ephemeral — they do not mutate CGW_SOURCE_BRANCH / CGW_TARGET_BRANCH in config.
./scripts/git/create_pr.sh --dry-run # preview title + commits
./scripts/git/create_pr.sh # interactive — confirm title
./scripts/git/create_pr.sh --non-interactive # accept auto-generated title
./scripts/git/create_pr.sh --draft # open as draft (skip auto-review)Requires: gh CLI installed and authenticated (gh auth login).
Set CGW_MERGE_MODE="pr" in .cgw.conf to use PRs by default.
./scripts/git/merge_pr.sh 42 # merge commit (keeps the feature revertable as a unit)
./scripts/git/merge_pr.sh 42 --dry-run # validate and preview only
./scripts/git/merge_pr.sh 42 --retarget 43 # then retarget stacked PR #43 onto 42's base
./scripts/git/merge_pr.sh 42 --squash --allow-non-merge # flattens the PR; explicit ack required--retarget is for stacked PRs in repos that don't auto-delete merged branches: it is validated
before the merge (PR 43's base must be PR 42's head) and run right after it. The head branch is
never deleted unless you pass --delete-branch. Undo a merge with
./scripts/git/rollback_merge.sh --revert --target <merge-sha> (the SHA is printed on success).
./scripts/git/push_validated.sh # with lint check
./scripts/git/push_validated.sh --skip-lint # skip all lint
./scripts/git/push_validated.sh --skip-md-lint # skip markdown lint only
./scripts/git/push_validated.sh --skip-typecheck # skip typecheck only (blocking otherwise)
./scripts/git/push_validated.sh --dry-run # preview
./scripts/git/push_validated.sh --no-venv # use system lint tool (no .venv)
./scripts/git/push_validated.sh --branch hotfix/1.2 # push a different branch
./scripts/git/push_validated.sh --pushed-only # gate the committed branch, not the working tree
./scripts/git/push_validated.sh --worktree # gate the working tree (default)Pushed-only gate. By default the pre-push lint/typecheck runs against your working tree, so
uncommitted edits can make a push pass or fail. With --pushed-only (or
CGW_PUSH_LINT_SCOPE=pushed) the gate extracts the committed branch into a throwaway directory:
typecheck covers the whole snapshot, lint/format/markdown only the files the push publishes. It
calls check_lint.sh --ref <rev> [--base <rev> | --unpushed], which you can also run directly.
check_lint.sh exits 0 (ok), 1 (lint/markdown), 2 (typecheck; never overridable) or 3
(the snapshot could not be set up, so no checks ran; fatal). Known gaps are listed in
KNOWN_ISSUES.
Post-Push CI Verification Gate:
When operating through an AI agent harness (Claude Code or Antigravity), every push performed triggers the CI verification gate (CGW_CI_VERIFY=1). The agent automatically subscribes to and monitors triggered GitHub Actions or Charlie CI runs until they finish, automatically diagnosing, fixing, and retrying failures up to CGW_CI_MAX_FIX_ROUNDS=3. See CI Setup.
./scripts/git/sync_branches.sh # sync current branch
./scripts/git/sync_branches.sh --all # sync both source and target branches
./scripts/git/sync_branches.sh --branch main # sync a specific branch
./scripts/git/sync_branches.sh --dry-run # preview (fetch only, no merge)
./scripts/git/sync_branches.sh --prune # also remove stale remote-tracking refssync_branches.sh protects any skip-worktree local file (see CGW_LOCAL_FILES) that has
genuinely diverged from HEAD on disk: it surfaces the divergence instead of a false "clean",
backs up and resets the file to HEAD so pull --rebase can proceed, then restores the local
bytes, re-applies the bit, and prints the upstream diff so new shared content can be reconciled
by hand. No-op when nothing has diverged.
Branches listed in CGW_PROTECTED_BRANCHES (default: the target branch) sync with
git pull --ff-only; if one has diverged from its remote the script refuses rather than
rebasing it (see docs/adr/0004). All other branches use rebase.
./scripts/git/rollback_merge.sh # interactive (hard reset)
./scripts/git/rollback_merge.sh --revert # safe revert (preserves history, no force-push)
./scripts/git/rollback_merge.sh --non-interactive # auto-pick latest pre-merge tag, only if it equals HEAD^1; else refuses
./scripts/git/rollback_merge.sh --target pre-merge-20260101_120000-12345./scripts/git/cherry_pick_commits.sh # interactive
./scripts/git/cherry_pick_commits.sh --commit abc1234 # non-interactive
# Partial pick: only files matching the pathspecs (repeatable --only)
./scripts/git/cherry_pick_commits.sh --commit abc1234 --only src/a.py --only docs/./scripts/git/stash_work.sh push "wip: half-done refactor"
./scripts/git/stash_work.sh list
./scripts/git/stash_work.sh pop
./scripts/git/stash_work.sh apply stash@{1} # apply without removing./scripts/git/create_release.sh v1.2.3 # annotated tag only
./scripts/git/create_release.sh v1.2.3 --push # tag + push (triggers release.yml)
./scripts/git/create_release.sh v1.2.3 --sign --push # GPG/SSH-signed tag + push
./scripts/git/create_release.sh v1.2.3 --dry-run # preview
./scripts/git/create_release.sh archive/pre-rewrite --allow-non-semver # non-semver archive tag (release.yml fires only on v*)Enable signing globally in .cgw.conf: CGW_SIGN_TAGS=1. Requires a GPG or SSH signing key configured in git (gpg.signingKey / gpg.format=ssh). Verify a tag with: git tag -v v1.2.3.
./scripts/git/setup_attributes.sh --dry-run # preview
./scripts/git/setup_attributes.sh # write .gitattributes./scripts/git/clean_build.sh # dry-run (safe preview)
./scripts/git/clean_build.sh --execute # actually delete
./scripts/git/clean_build.sh --td --execute # TouchDesigner artifacts (plus common junk and any auto-detected categories)./scripts/git/repo_health.sh # integrity, size, large files
./scripts/git/repo_health.sh --gc # also run garbage collection
./scripts/git/repo_health.sh --large 5 # report files >5MB./scripts/git/undo_last.sh commit # undo last commit, keep changes staged
./scripts/git/undo_last.sh unstage src/file.py # remove file from staging area
./scripts/git/undo_last.sh discard src/file.py # discard working-tree changes (irreversible)
./scripts/git/undo_last.sh amend-message "fix: correct msg" # rewrite last commit messageundo_last.sh commit creates a pre-undo-commit-<timestamp>-<pid> backup tag first; unstage, discard and amend-message do not.
./scripts/git/branch_cleanup.sh # dry-run preview (safe default)
./scripts/git/branch_cleanup.sh --execute # delete merged local branches
./scripts/git/branch_cleanup.sh --execute --remote # also prune stale remote-tracking refs
./scripts/git/branch_cleanup.sh --tags --execute # also remove old backup tags
./scripts/git/branch_cleanup.sh --tags --older-than 30 --execute # only backup tags older than 30 days./scripts/git/rebase_safe.sh --onto main # rebase current branch onto main
./scripts/git/rebase_safe.sh --squash-last 3 # interactive squash of last 3 commits
./scripts/git/rebase_safe.sh --squash-last 3 --autosquash # auto-apply fixup!/squash! prefixes
./scripts/git/rebase_safe.sh --abort # abort in-progress rebase
./scripts/git/rebase_safe.sh --continue # continue after resolving conflictsCreates a backup tag (pre-rebase-<timestamp>-<pid>) before rebasing. The installed pre-rebase hook also enforces this at the git level — if commits are already pushed, the rebase is refused unless CGW_ALLOW_REBASE_PUBLISHED=1 is set (for controlled force-push workflows).
# Automated: find first-bad commit using a test script
./scripts/git/bisect_helper.sh --good v1.0.0 --run "bash tests/smoke_test.sh"
# Manual: guided interactive bisect
./scripts/git/bisect_helper.sh --good v1.0.0
# → git bisect good / git bisect bad after each checkout
./scripts/git/bisect_helper.sh --abort # stop in-progress bisect session./scripts/git/changelog_generate.sh # since latest semver tag → stdout
./scripts/git/changelog_generate.sh --from v1.0.0 # since specific tag
./scripts/git/changelog_generate.sh --from v1.0.0 --output CHANGELOG.md
./scripts/git/changelog_generate.sh --from v1.0.0 --format text # plain text
# Cutting a release before the tag exists: --version supplies the heading
# (git describe can't find the tag yet, so it would otherwise fall back to a hash).
./scripts/git/changelog_generate.sh --from v1.0.0 --version v1.1.0 --output CHANGELOG.md
# --prepend stacks the new section above CHANGELOG.md's existing content instead of
# overwriting it, building a cumulative history. Refuses if that heading is already present.
./scripts/git/changelog_generate.sh --from v1.0.0 --version v1.1.0 \
--output CHANGELOG.md --prependGit's reflog records every time a ref moves; git fsck surfaces unreachable objects when the reflog is gone. recover.sh wraps both:
./scripts/git/recover.sh reflog # show HEAD reflog with restore hints
./scripts/git/recover.sh reflog --limit 50 --ref main # different ref
./scripts/git/recover.sh show HEAD@{3} # inspect a specific entry or SHA
./scripts/git/recover.sh dangling # git fsck --full: find unreachable commits
./scripts/git/recover.sh restore abc1234 --branch recovered/lost-work # restore as a new branchrestore creates a pre-recover-<timestamp>-<pid> backup tag before touching any ref. All other subcommands are read-only.
Linked worktrees let you check out multiple branches simultaneously in separate directories — useful for hot-fixes without stashing:
./scripts/git/worktree_manage.sh list # show all worktrees
./scripts/git/worktree_manage.sh add ../hotfix hotfix/urgent # add linked worktree (creates branch)
./scripts/git/worktree_manage.sh add ../review existing-branch # check out existing branch
./scripts/git/worktree_manage.sh add ../part2 feat/part-2 feat/part-1 # new branch starting at <base>
./scripts/git/worktree_manage.sh link # link CGW tooling into the current worktree
./scripts/git/worktree_manage.sh remove --execute ../hotfix # remove worktree link
./scripts/git/worktree_manage.sh prune # dry-run: show stale admin files
./scripts/git/worktree_manage.sh prune --execute # remove stale admin filesremove and prune default to dry-run; pass --execute to apply.
scripts/git/ and .githooks/ are commonly gitignored, so git worktree add — which only
checks out tracked content — leaves a linked worktree without CGW's scripts or hooks. Any
CGW-installed hook now detects this and fails closed (it will not silently skip its check),
printing a CGW not found error instead of running. Fix it with:
./scripts/git/worktree_manage.sh link ../hotfix # from the main worktree, by path
# From inside the linked worktree itself, ./scripts/git/ isn't there to run --
# invoke the main worktree's copy instead (git worktree list's first entry is
# always the main worktree):
"$(git worktree list --porcelain | head -1 | cut -d' ' -f2-)/scripts/git/worktree_manage.sh" linkThis writes a real scripts/git/ directory in the linked worktree containing one small shim per
public script. Each shim finds the main worktree at run time and execs its copy, so both
worktrees share one set of tooling while the current directory (and so the project root, config
and logs) stays the worktree's. The libraries and .githooks are not copied — hooks and
install_hooks.sh fall back to the main worktree. It's idempotent (re-running refreshes the
shims) and refuses to overwrite a real directory that isn't its own shim directory.
worktree_manage.sh add runs link automatically for newly created worktrees.
Earlier CGW versions linked scripts/git and .githooks with a symlink / NTFS junction instead.
link replaces those on sight. Such a link is dangerous on Windows: a raw git worktree remove
follows it and deletes the main worktree's gitignored tooling. Shims have no such link, but
still remove worktrees with worktree_manage.sh remove --execute — it also clears any legacy
link first, and the agent guardrail blocks raw git worktree remove. To recover an emptied
main checkout, re-run cgw-install.cmd / cgw-batch-install.cmd from the CGW source repo (they
leave .cgw.conf alone).
branch_diff.sh auto-detects the repository's default branch (main, master, or a custom name) via ${CGW_REMOTE}/HEAD, falling back to CGW_TARGET_BRANCH when that ref is unset:
./scripts/git/branch_diff.sh # full patch vs default branch
./scripts/git/branch_diff.sh --files # changed file names only
./scripts/git/branch_diff.sh --stat --no-ws # diffstat, ignoring whitespace
./scripts/git/branch_diff.sh --base release/1.2 # compare against an explicit refRead-only — safe to run with a dirty working tree. Uses triple-dot diff (<base>...HEAD), so only changes made on the current branch are shown.
./scripts/git/pr_checkout.sh 42 # check out PR #42
./scripts/git/pr_checkout.sh --pr 42 --branch review/pr-42 # into a named local branch
./scripts/git/pr_checkout.sh 42 --dry-run # preview the gh command onlyWraps gh pr checkout; requires gh auth login. Refuses to switch branches over uncommitted tracked changes unless --force is passed.
./scripts/git/md_toc.sh docs/usage.md # print TOC to stdout
./scripts/git/md_toc.sh docs/usage.md --insert # insert/update the <!--ts-->...<!--te--> block in place
./scripts/git/md_toc.sh docs/usage.md --check # CI: exit 1 if the TOC is stale
./scripts/git/md_toc.sh --all # update every tracked *.md file with a <!--ts--> markerComputes GitHub-compatible anchor slugs offline (no network access or auth token needed).
./scripts/git/fix_lint.sh # auto-fix code lint + markdown, full repo
./scripts/git/fix_lint.sh --modified-only # only files changed vs HEAD
./scripts/git/fix_lint.sh --md-only # markdown auto-fix only, skip code lint
./scripts/git/fix_lint.sh --skip-md-lint # code lint auto-fix only, skip markdownMarkdown auto-fix uses CGW_MARKDOWNLINT_CMD (auto-detected at runtime — see
Configuration) with CGW_MARKDOWNLINT_FIX_ARGS (default --fix).
Only auto-fixable rules are corrected in place; manual-only violations (e.g. MD013,
MD041) still require a hand fix and are reported by check_lint.sh.
check_lint.sh accepts the matching --md-only flag to verify markdown alone
(./scripts/git/check_lint.sh --md-only) — fix_lint.sh --md-only forwards it
automatically to its own final verification step.
check_lint.sh and push_validated.sh also accept --skip-typecheck to bypass the whole-project
typecheck (CGW_TYPECHECK_CMD) for a single run — e.g. ./scripts/git/check_lint.sh --skip-typecheck. Unlike lint/format/markdown, a failing typecheck blocks both scripts by
default (advisory only in the pre-commit hook); see Configuration.
Gitignored .md files (local CLAUDE.md, session logs, ...) are skipped automatically via
.markdownlint-cli2.jsonc — see Configuration.
All scripts support CGW_* environment variables to override config at runtime:
CGW_NON_INTERACTIVE=1 ./scripts/git/commit_enhanced.sh "feat: message"
CGW_SOURCE_BRANCH=dev ./scripts/git/merge_with_validation.sh --dry-run
CGW_LINT_CMD="" ./scripts/git/check_lint.sh # skip lint for this run
CGW_NO_VENV=1 ./scripts/git/commit_enhanced.sh "feat: message" # system lint
CGW_SKIP_LINT=1 ./scripts/git/commit_enhanced.sh "feat: message" # skip all lint
CGW_SKIP_MD_LINT=1 ./scripts/git/commit_enhanced.sh "docs: update" # skip md lint only
CGW_SKIP_TYPECHECK=1 ./scripts/git/push_validated.sh # skip the blocking typecheck for this push
CGW_ALL=1 ./scripts/git/commit_enhanced.sh "chore: bulk stage" # force-stage allLegacy CLAUDE_GIT_* variables are still supported:
CLAUDE_GIT_NON_INTERACTIVE=1→CGW_NON_INTERACTIVE=1CLAUDE_GIT_NO_VENV=1→CGW_NO_VENV=1CLAUDE_GIT_STAGED_ONLY=1→CGW_STAGED_ONLY=1
CGW_LOCAL_FILES (and CGW_LOCAL_FILES_EXEMPT) is read from .cgw.conf by the
git hooks at run time, so editing the config takes effect immediately:
# Add a new local-only entry — no re-render needed
sed -i 's/^CGW_LOCAL_FILES=.*/CGW_LOCAL_FILES="CLAUDE.md MEMORY.md notes.md .claude\/ logs\/"/' .cgw.conf
git add notes.md && git commit -m "test" # → blocked by pre-commit on next tryMatch contract:
- A bare name (
CLAUDE.md) matches the path exactly —CLAUDE.md.bakdoes NOT match. - A trailing-slash entry (
logs/) matches the directory itself or anything inside it. CGW_LOCAL_FILES_EXEMPTaccepts exact paths that override a match.- No globs, no substring matches.
Migration note: if you upgraded from a CGW version older than the single-source-of-truth matcher, your
.git/hooks/pre-commitand.git/hooks/pre-pushmay still contain compiled-in patterns from the oldconfigure.sh. Re-run./scripts/git/install_hooks.shonce to refresh them — afterwards.cgw.confedits will be picked up at run time.