Skip to content

bot-commit.sh commits as the operator in every pipeline worktree (gitignored config never resolves) #116

Description

@cadenza-dev-pipeline

Summary

bot-commit.sh silently commits as the operator instead of the bot for every commit the dev-pipeline makes, because it resolves the consumer config relative to the pipeline worktree — where that config never exists.

  • .claude/second-shift.config.json is gitignored (.gitignore:25), so it is present only in the main checkout and never in a git worktree add checkout.
  • bot-commit.sh reads CFG="${SECOND_SHIFT_CONFIG:-$DIR/.claude/second-shift.config.json}", then falls back to ROOT="$(git -C "$DIR" rev-parse --show-toplevel)". In a worktree, --show-toplevel is the worktree itself, not the main checkout — so the fallback resolves to the same missing path.
  • With no config found, BOT_ENABLED stays false and the helper takes its documented "bot disabled or unconfigured" branch: exec git -C "$DIR" commit "$@" — the operator's identity.

Every pipeline stage that commits (plan, implement, docs, quality pass) goes through this helper, and SECOND_SHIFT_CONFIG is not exported by the pipeline, so the fallback is the normal path rather than an edge case.

Impact

This is the exact failure the helper was written to prevent. Its own header says:

WHY THIS EXISTS: the gh bot wrapper covers GitHub API writes only — it does NOT touch git user.name/user.email, so a bare git commit in the pipeline session silently commits as the operator. Observed in a retro: 4 of 5 PR commits carried the operator identity while only the engine-agent commit used the bot. Prose said "use the bot identity"; prose failed — this helper owns the command + identity resolution (enforcement-ladder rung 2).

The helper is enforcement-ladder rung 2, and it has regressed to rung-1 (prose) reliability while appearing to work. It fails silently: there is no WARN on this path (the WARN branch only fires when the config IS found and the bot user id cannot be resolved), so the operator sees a successful commit and nothing else.

Consequence: PR commit attribution is wrong on every autonomous run, and the audit trail claims a human authored code the pipeline wrote.

Repro (measured, during the #100 run)

Ordinary pipeline commit into the Stage-2 worktree, no env pin:

$ bash .../tools/bot-commit.sh -C "$WT" -m "docs(dev-pipeline): plan for #100"
$ git -C "$WT" log -1 --format='%an <%ae>'
Manol Donev <manol.donev@gmail.com>          # <-- operator, not the bot

Config resolution in that worktree:

$ echo "${SECOND_SHIFT_CONFIG:-<unset>}"
<unset>
$ [[ -f "$WT/.claude/second-shift.config.json" ]] && echo YES || echo NO
NO
$ git -C "$WT" rev-parse --show-toplevel
/Users/mdonev/github/second-shift-worktrees/second-shift-100   # the worktree, not the main checkout
$ grep -n 'second-shift.config.json' .gitignore
25:.claude/second-shift.config.json

Pinning the config to the main checkout fixes it, which isolates the config read as the sole cause — bot-user-id resolution itself is healthy:

$ gh api "users/cadenza-dev-pipeline[bot]" --jq .id
272027008
$ export SECOND_SHIFT_CONFIG=/Users/mdonev/github/second-shift/.claude/second-shift.config.json
$ bash .../tools/bot-commit.sh -C "$WT" --amend --reset-author --no-edit
$ git -C "$WT" log -1 --format='%an <%ae>'
cadenza-dev-pipeline[bot] <272027008+cadenza-dev-pipeline[bot]@users.noreply.github.com>

Note --amend alone is not enough — git preserves the original author, so --reset-author is required to repair an already-mis-attributed commit.

Suggested fix

Resolve the config against the main checkout, not the worktree. git rev-parse --git-common-dir already gives the shared .git for a worktree (the helper uses it a few lines later for the bot-user-id cache); the main checkout is its parent. Roughly:

COMMON_DIR="$(cd "$DIR" && cd "$(git rev-parse --git-common-dir)" && pwd)"
MAIN_ROOT="$(dirname "$COMMON_DIR")"
[[ -f "$CFG" ]] || CFG="$MAIN_ROOT/.claude/second-shift.config.json"

Two things worth deciding alongside:

  1. The silent fallback is the real hazard. "Config not found" is indistinguishable from "bot deliberately disabled". Consider a WARN (matching the existing unresolvable-bot-id WARN) when a config is absent but $DIR is a worktree of a repo whose main checkout does have one — that is a misconfiguration, not an opt-out.
  2. Should the pipeline export SECOND_SHIFT_CONFIG? Fixing the helper is necessary either way, but exporting it once at pre-flight would make every downstream tool agree on one config path instead of each re-deriving.

Scope note

Any other pipeline tool that resolves the consumer config relative to a worktree has the same latent bug and is worth a sweep — the gitignored-config + worktree combination is the general trap, and bot-commit.sh is just where it surfaced.


Found during the #100 dev-pipeline run (run_id 2026-07-19T174237Z-imOne-dc554fcc); worked around there by pinning SECOND_SHIFT_CONFIG per commit. See PR #114.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions