Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion plugins/source-control/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
"name": "source-control",
"version": "0.26.7",
"version": "0.26.8",
"description": "Git and GitHub delivery workflow: /commit (Conventional Commits + Co-Authored-By trailer via safe heredoc mechanics), /pull-request (prep, create, CI monitoring, review-comment triage, merge, CI-log fetch), /babysit-prs (self-pacing fleet loop — safe by default; opt-in worker/autopilot tiers add gate-checked merge and thread resolution behind a deterministic Python engine), /babysit-loop (the loop-lane merge lane: a standing or drain loop that invokes babysit-prs per cycle, configured through repo-scoped babysit_loop_* keys on the layered source-control.md seam, with merge authority human-only until the target repo's tracked config adopts the lane, a gate-proven C2-mechanical baseline once adopted, and merge-rung raises binding from the team-tracked layer only), /worktree (create, status, cleanup, audit for parallel-session isolation), /setup (check the effective commit-subject / PR-title convention merged across its config layers and the babysit-prs config, or apply — interview the repo and write the convention config to a chosen layer), and /resolve-conflicts (intent-first merge/rebase conflict resolution with a semantic-conflict sweep — never --abort). The commit-subject / PR-title convention is configurable via a source-control.md config written by a re-runnable setup skill, layered across a ~/.claude user-global file, the tracked team file, and a gitignored .claude/source-control.local.md personal overlay merged per key; Conventional Commits is the default when no convention is declared.",
"author": {
"name": "Melodic Software",
Expand Down
17 changes: 17 additions & 0 deletions plugins/source-control/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,23 @@
All notable changes to the `source-control` plugin are documented here. Format follows
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this plugin uses semantic versioning.

## [0.26.8]

### Fixed

- **`fetch-all-pr-comments.sh` output can choke a downstream Python consumer on Windows
(emoji/cp1252 mismatch) (#597).** The script's UTF-8 JSON output commonly carries non-ASCII
bytes — bot badge images, reaction emoji — from bot review comments. Reproduced directly: a
Python consumer that opens the output (or reads this script's stdout) without an explicit UTF-8
encoding inherits the interpreter's default ANSI code page on Windows (cp1252) and raises
`UnicodeDecodeError` on those bytes; this repo's own consumers (`babysit_findings.py`) already
pin `encoding="utf-8"` explicitly and are unaffected, so the gap is external/downstream
consumers. `fetch-all-pr-comments.sh --help` now documents the `PYTHONUTF8=1` (PEP 540)
requirement for Windows consumers that don't pin the encoding themselves.
`babysit-readiness-gate.sh` — the one `babysit_python` caller that parses this script's
comment-JSON schema and lacked the `export PYTHONUTF8=1` convention the two `bin/` babysit
wrappers already apply — now sets it too, closing the inconsistency.

## [0.26.7]

### Fixed
Expand Down
10 changes: 10 additions & 0 deletions plugins/source-control/scripts/babysit-readiness-gate.sh
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,16 @@
# for the cause (owner/repo may be unresolved; see Repo resolution above)

set -uo pipefail
# Matches the `export PYTHONUTF8=1` convention the bin/ babysit wrappers already
# apply before invoking babysit_python (source-control-babysit-merge,
# source-control-babysit-resolve-thread). This gate is the third babysit_python
# caller and the one that parses fetch-all-pr-comments.sh-shaped JSON (via
# babysit_findings.py --comments-json), which commonly carries non-ASCII bytes
# (bot badges, reaction emoji) from bot review comments — PEP 540 UTF-8 mode
# keeps the interpreter's default I/O encoding independent of the Windows ANSI
# code page (cp1252) for any code path that does not pin encoding="utf-8"
# itself (#597).
export PYTHONUTF8=1

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

Expand Down
45 changes: 45 additions & 0 deletions plugins/source-control/scripts/babysit-readiness-gate.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,20 @@ else
fail "portability: uses POSIX grep -ow" "present" "missing"
fi

# --- Case: PYTHONUTF8=1 exported before invoking babysit_python (#597) --------
# Matches the convention the bin/ babysit wrappers already apply
# (source-control-babysit-merge, source-control-babysit-resolve-thread): this
# gate is the third babysit_python caller and the one that parses
# fetch-all-pr-comments.sh-shaped JSON, which commonly carries non-ASCII bytes
# (bot badges, reaction emoji) that choke a Python code path relying on the
# interpreter's default I/O encoding on Windows (cp1252) instead of pinning
# encoding="utf-8" itself.
if [[ "$GATE_BODY" == *'export PYTHONUTF8=1'* ]]; then
pass "#597: exports PYTHONUTF8=1"
else
fail "#597: exports PYTHONUTF8=1" "present" "missing"
fi

# --- Case: adjacent severity words BOTH count (whole-word, no shared-boundary loss) -
# `grep -ow` matches each word even when adjacent; an alternation-boundary regex
# (^|[^w])WORD([^w]|$) would consume the shared space and undercount.
Expand Down Expand Up @@ -277,6 +291,37 @@ probe_py() {
"$@" -c 'import sys; raise SystemExit(0 if sys.version_info[:2] >= (3, 11) else 1)' \
>/dev/null 2>&1
}

# --- Case: PYTHONUTF8=1 actually reaches the Python child process (#597) ------
# A stubbed `py -3` records the PYTHONUTF8 value it inherited when
# babysit_python execs it, proving the export reaches the child process (not
# just present as dead source text in the static check above).
if probe_py py -3 || probe_py python3 || probe_py python; then
PYSTUB_BIN="$TEST_TMPDIR/bin-pystub"
mkdir -p "$PYSTUB_BIN"
PYUTF8_PROBE_FILE="$TEST_TMPDIR/pyutf8-probe.txt"
export PYUTF8_PROBE_FILE
cat >"$PYSTUB_BIN/py" <<'STUB'
#!/usr/bin/env bash
args=("$@")
for a in "${args[@]}"; do
if [[ "$a" == "-c" ]]; then
exit 0 # version probe
fi
done
printf '%s' "${PYTHONUTF8:-unset}" >"$PYUTF8_PROBE_FILE"
printf 'findings=0 classified=0\n'
STUB
chmod +x "$PYSTUB_BIN/py"
F=$(mkjson pyutf8-probe-fixture '[{author:"claude[bot]", body:"no findings here"}]')
PATH="$PYSTUB_BIN:$PATH" bash "$GATE" 123 --comments-json "$F" --self 'me[bot]' >/dev/null 2>&1
probe_seen="$(cat "$PYUTF8_PROBE_FILE" 2>/dev/null || echo "missing")"
assert_eq "#597: child process inherits PYTHONUTF8=1" "1" "$probe_seen"
unset PYUTF8_PROBE_FILE
else
pass "#597: PYTHONUTF8 child-inheritance probe skipped (no Python 3.11+)"
fi

if probe_py py -3 || probe_py python3 || probe_py python; then
F=$(mkjson lifetime-open '[
{author:"codex[bot]", body:"[CRITICAL] resolved earlier", isResolved:true},
Expand Down
11 changes: 10 additions & 1 deletion plugins/source-control/scripts/fetch-all-pr-comments.sh
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,15 @@
# FETCH_COMMENTS_OWNER default `gh repo view --json owner -q .owner.login`
# FETCH_COMMENTS_REPO default `gh repo view --json name -q .name`
#
# Windows Python consumers (#597):
# Output commonly carries non-ASCII bytes (bot badge images, reaction
# emoji) from bot review comments. A downstream Python consumer on Windows
# that opens the JSON output, or reads this script's stdout, WITHOUT an
# explicit UTF-8 encoding inherits the interpreter's default ANSI code page
# (cp1252) and raises UnicodeDecodeError on those bytes. Either open with
# `encoding="utf-8"` (or reconfigure the stream to UTF-8 at runtime), or run
# the consumer with `PYTHONUTF8=1` (PEP 540) set in the environment.
#
# Exit codes:
# 0 success (zero or more comments emitted)
# 1 invalid argument
Expand All @@ -30,7 +39,7 @@ set -uo pipefail # -e omitted: gh api failures explicitly guarded with || { exit
PR_NUMBER=""

usage() {
sed -n '2,18p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'
sed -n '2,27p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'
exit 0
}

Expand Down
9 changes: 9 additions & 0 deletions plugins/source-control/scripts/fetch-all-pr-comments.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,15 @@ out=$(FETCH_COMMENTS_OWNER=o FETCH_COMMENTS_REPO=r \
rc=$?
assert_eq "env-var override resolves owner/repo (exit 0)" "0" "$rc"

# Case 11 (#597): --help documents the Windows PYTHONUTF8=1 gotcha for a
# downstream Python consumer parsing this script's UTF-8 JSON output (bot
# badges / reaction emoji commonly present in review comment bodies raise
# UnicodeDecodeError under the Windows default cp1252 encoding if the
# consumer does not pin encoding="utf-8" or PYTHONUTF8=1 itself).
help_out=$(bash "$SCRIPT" --help 2>&1)
assert_contains "--help documents PYTHONUTF8=1 for Windows consumers" "$help_out" "PYTHONUTF8=1"
assert_contains "--help names the UnicodeDecodeError failure mode" "$help_out" "UnicodeDecodeError"

# ---- Summary ----------------------------------------------------------------

if [[ "$FAILED" -eq 0 ]]; then
Expand Down