Skip to content

Commit b1ea493

Browse files
fix: add DSO_MECHANICAL_AMEND bypass for merge-to-main version bump (merge worktree-20260327-075020)
2 parents d97bc22 + 716a384 commit b1ea493

File tree

7 files changed

+227
-3
lines changed

7 files changed

+227
-3
lines changed

.test-index

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,10 @@ plugins/dso/agents/code-reviewer-deep-arch.md:tests/agents/test-reviewer-dimensi
151151
plugins/dso/agents/code-reviewer-deep-correctness.md:tests/agents/test-reviewer-dimension-names.sh
152152
plugins/dso/agents/code-reviewer-deep-hygiene.md:tests/agents/test-reviewer-dimension-names.sh
153153
plugins/dso/agents/code-reviewer-deep-verification.md:tests/agents/test-reviewer-dimension-names.sh
154-
plugins/dso/scripts/merge-to-main.sh:tests/scripts/test-merge-to-main.sh,tests/scripts/test-merge-to-main-auto-resolve.sh,tests/scripts/test-merge-to-main-cleanliness.sh
154+
plugins/dso/scripts/merge-to-main.sh:tests/scripts/test-merge-to-main.sh,tests/scripts/test-merge-to-main-auto-resolve.sh,tests/scripts/test-merge-to-main-cleanliness.sh,tests/hooks/test-mechanical-amend-bypass.sh
155+
plugins/dso/hooks/pre-commit-review-gate.sh:tests/hooks/test-mechanical-amend-bypass.sh
156+
plugins/dso/hooks/pre-commit-test-gate.sh:tests/hooks/test-mechanical-amend-bypass.sh
157+
plugins/dso/hooks/lib/review-gate-bypass-sentinel.sh:tests/hooks/test-mechanical-amend-bypass.sh
155158
plugins/dso/skills/implementation-plan/SKILL.md:tests/skills/test_implementation_plan_bidirectional_tdd.py
156159
tests/hooks/test-deps.sh:tests/scripts/test-v2-clean-guard.sh
157160
tests/scripts/test-lock-acquire-ticket-format.sh:tests/scripts/test-v2-clean-guard.sh

plugins/dso/hooks/lib/review-gate-bypass-sentinel.sh

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,30 @@ hook_review_bypass_sentinel() {
5252
return 0
5353
fi
5454

55+
# --- Pattern j: DSO_MECHANICAL_AMEND on non-amend commits ---
56+
# DSO_MECHANICAL_AMEND=1 is an internal bypass for merge-to-main.sh's mechanical
57+
# git commit --amend --no-edit calls. Block it on raw (non-amend) git commits to
58+
# prevent misuse as a general gate bypass.
59+
#
60+
# NOTE: This sentinel runs as a PreToolUse hook in Claude Code's process, NOT as
61+
# a child of the git subprocess. The inline env-var prefix (DSO_MECHANICAL_AMEND=1
62+
# git commit ...) is scoped to the git child process (Layer 1), so the sentinel's
63+
# process environment never contains this var. We must parse the COMMAND string
64+
# directly to detect the pattern.
65+
if [[ "$COMMAND" =~ DSO_MECHANICAL_AMEND=1 ]]; then
66+
# REVIEW-DEFENSE: Use `git[[:space:]].*commit` (word-boundary via [[:space:]].*) instead of
67+
# `git[[:space:]]+(commit|[^[:space:]]*[[:space:]]+commit)` so that `git -C /path commit`
68+
# is also matched. The existing Pattern b has the same regex limitation for -n detection,
69+
# but fixing it there risks false positives (git log -n). Here, since we already know
70+
# DSO_MECHANICAL_AMEND=1 is present, the broader match is safe and more correct.
71+
if [[ "$COMMAND" =~ git[[:space:]].*commit ]]; then
72+
if [[ "$COMMAND" != *"--amend"* ]]; then
73+
echo "BLOCKED [bypass-sentinel]: DSO_MECHANICAL_AMEND=1 on non-amend commit. This env var is only valid with git commit --amend --no-edit." >&2
74+
trap - ERR; return 2
75+
fi
76+
fi
77+
fi
78+
5579
# --- Pattern a: --no-verify ---
5680
if [[ "$COMMAND" == *"--no-verify"* ]]; then
5781
echo "BLOCKED [bypass-sentinel]: --no-verify flag detected. Use /dso:commit instead." >&2

plugins/dso/hooks/pre-commit-review-gate.sh

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,15 @@ if [[ ${#STAGED_FILES[@]} -eq 0 ]]; then
177177
exit 0
178178
fi
179179

180+
# ── Mechanical amend bypass (merge-to-main.sh version_bump / validate) ────────
181+
# DSO_MECHANICAL_AMEND=1 is set by merge-to-main.sh before git commit --amend
182+
# for mechanical operations (version bump, post-merge validation fold-in).
183+
# These are single-field or auto-fix changes that don't require code review.
184+
# Layer 2 (review-gate-bypass-sentinel.sh) blocks misuse on non-amend commits.
185+
if [[ "${DSO_MECHANICAL_AMEND:-}" == "1" ]]; then
186+
exit 0
187+
fi
188+
180189
# ── Merge commit: filter out incoming-only files (w21-0oc6, dso-k7fe) ────────
181190
# When MERGE_HEAD exists (e.g., `git merge --no-commit origin/main`), staged
182191
# files include changes from the incoming branch that were already reviewed

plugins/dso/hooks/pre-commit-test-gate.sh

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,15 @@ if [[ ${#STAGED_FILES[@]} -eq 0 ]]; then
9090
exit 0
9191
fi
9292

93+
# ── Mechanical amend bypass (merge-to-main.sh version_bump / validate) ────────
94+
# DSO_MECHANICAL_AMEND=1 is set by merge-to-main.sh before git commit --amend
95+
# for mechanical operations (version bump, post-merge validation fold-in).
96+
# These are single-field or auto-fix changes that don't require test verification.
97+
# Layer 2 (review-gate-bypass-sentinel.sh) blocks misuse on non-amend commits.
98+
if [[ "${DSO_MECHANICAL_AMEND:-}" == "1" ]]; then
99+
exit 0
100+
fi
101+
93102
# ── Determine repo root ────────────────────────────────────────────────────────
94103
REPO_ROOT=$(git rev-parse --show-toplevel 2>/dev/null || echo "")
95104

plugins/dso/scripts/merge-to-main.sh

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1096,7 +1096,8 @@ _phase_version_bump() {
10961096
fi
10971097
echo "OK: Version bumped."
10981098
git add -u 2>/dev/null || true
1099-
if ! git diff --cached --quiet 2>/dev/null; then git commit --amend --no-edit --quiet
1099+
if ! git diff --cached --quiet 2>/dev/null; then
1100+
DSO_MECHANICAL_AMEND=1 git commit --amend --no-edit --quiet
11001101
echo 'OK: Folded version bump into merge commit.'; fi
11011102
_state_mark_complete "version_bump"
11021103
}
@@ -1168,7 +1169,7 @@ _phase_validate() {
11681169
fi
11691170

11701171
if ! git diff --cached --quiet 2>/dev/null; then
1171-
git commit --amend --no-edit --quiet
1172+
DSO_MECHANICAL_AMEND=1 git commit --amend --no-edit --quiet
11721173
echo "OK: Folded post-merge changes into merge commit."
11731174
fi
11741175

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
#!/usr/bin/env bash
2+
# tests/hooks/test-mechanical-amend-bypass.sh
3+
# Behavioral tests for DSO_MECHANICAL_AMEND bypass in pre-commit hooks.
4+
#
5+
# Validates that:
6+
# 1. pre-commit-review-gate.sh exits 0 when DSO_MECHANICAL_AMEND=1
7+
# 2. pre-commit-test-gate.sh exits 0 when DSO_MECHANICAL_AMEND=1
8+
# 3. merge-to-main.sh sets DSO_MECHANICAL_AMEND=1 before git commit --amend
9+
# 4. review-gate-bypass-sentinel.sh blocks DSO_MECHANICAL_AMEND on non-amend commits
10+
#
11+
# Usage: bash tests/hooks/test-mechanical-amend-bypass.sh
12+
13+
set -uo pipefail
14+
15+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
16+
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
17+
DSO_PLUGIN_DIR="$REPO_ROOT/plugins/dso"
18+
REVIEW_GATE="$DSO_PLUGIN_DIR/hooks/pre-commit-review-gate.sh"
19+
TEST_GATE="$DSO_PLUGIN_DIR/hooks/pre-commit-test-gate.sh"
20+
MERGE_SCRIPT="$DSO_PLUGIN_DIR/scripts/merge-to-main.sh"
21+
SENTINEL_LIB="$DSO_PLUGIN_DIR/hooks/lib/review-gate-bypass-sentinel.sh"
22+
23+
source "$REPO_ROOT/tests/lib/assert.sh"
24+
25+
# =============================================================================
26+
# Helper: extract function body from a script file
27+
# =============================================================================
28+
_extract_fn() {
29+
local fn_name="$1" script="$2"
30+
awk "/^${fn_name}\\(\\)/{found=1} found{print; if(/^\\}$/){exit}}" "$script"
31+
}
32+
33+
# =============================================================================
34+
# Test 1: pre-commit-review-gate.sh checks DSO_MECHANICAL_AMEND
35+
# The review gate script must contain a check for DSO_MECHANICAL_AMEND that
36+
# exits 0 early. This is a structural test — the actual hook runs in a git
37+
# pre-commit context which is hard to simulate.
38+
# =============================================================================
39+
echo ""
40+
echo "--- test_review_gate_has_mechanical_amend_check ---"
41+
_snapshot_fail
42+
43+
_RG_HAS_CHECK="not_found"
44+
grep -q 'DSO_MECHANICAL_AMEND' "$REVIEW_GATE" 2>/dev/null && _RG_HAS_CHECK="found"
45+
assert_eq "test_review_gate_has_mechanical_amend_check" "found" "$_RG_HAS_CHECK"
46+
47+
assert_pass_if_clean "test_review_gate_has_mechanical_amend_check"
48+
49+
# =============================================================================
50+
# Test 2: pre-commit-test-gate.sh checks DSO_MECHANICAL_AMEND
51+
# The test gate script must contain a check for DSO_MECHANICAL_AMEND that
52+
# exits 0 early.
53+
# =============================================================================
54+
echo ""
55+
echo "--- test_test_gate_has_mechanical_amend_check ---"
56+
_snapshot_fail
57+
58+
_TG_HAS_CHECK="not_found"
59+
grep -q 'DSO_MECHANICAL_AMEND' "$TEST_GATE" 2>/dev/null && _TG_HAS_CHECK="found"
60+
assert_eq "test_test_gate_has_mechanical_amend_check" "found" "$_TG_HAS_CHECK"
61+
62+
assert_pass_if_clean "test_test_gate_has_mechanical_amend_check"
63+
64+
# =============================================================================
65+
# Test 3: merge-to-main.sh _phase_version_bump sets DSO_MECHANICAL_AMEND=1
66+
# The version bump phase must export DSO_MECHANICAL_AMEND=1 before git commit
67+
# --amend and unset it after.
68+
# =============================================================================
69+
echo ""
70+
echo "--- test_version_bump_sets_mechanical_amend ---"
71+
_snapshot_fail
72+
73+
_VB_FN=$(_extract_fn "_phase_version_bump" "$MERGE_SCRIPT" 2>/dev/null || echo "")
74+
_VB_HAS_AMEND_VAR="not_found"
75+
echo "$_VB_FN" | grep -q 'DSO_MECHANICAL_AMEND' 2>/dev/null && _VB_HAS_AMEND_VAR="found"
76+
assert_eq "test_version_bump_sets_mechanical_amend" "found" "$_VB_HAS_AMEND_VAR"
77+
78+
assert_pass_if_clean "test_version_bump_sets_mechanical_amend"
79+
80+
# =============================================================================
81+
# Test 4: merge-to-main.sh _phase_validate sets DSO_MECHANICAL_AMEND=1
82+
# The validate phase also uses git commit --amend and must set the var.
83+
# =============================================================================
84+
echo ""
85+
echo "--- test_validate_phase_sets_mechanical_amend ---"
86+
_snapshot_fail
87+
88+
_VAL_FN=$(_extract_fn "_phase_validate" "$MERGE_SCRIPT" 2>/dev/null || echo "")
89+
_VAL_HAS_AMEND_VAR="not_found"
90+
echo "$_VAL_FN" | grep -q 'DSO_MECHANICAL_AMEND' 2>/dev/null && _VAL_HAS_AMEND_VAR="found"
91+
assert_eq "test_validate_phase_sets_mechanical_amend" "found" "$_VAL_HAS_AMEND_VAR"
92+
93+
assert_pass_if_clean "test_validate_phase_sets_mechanical_amend"
94+
95+
# =============================================================================
96+
# Test 5: review gate exits 0 early when DSO_MECHANICAL_AMEND=1
97+
# The check must be an early exit (exit 0) not just a variable reference.
98+
# Look for the pattern: DSO_MECHANICAL_AMEND followed by exit 0 within ~3 lines.
99+
# =============================================================================
100+
echo ""
101+
echo "--- test_review_gate_exits_early_on_mechanical_amend ---"
102+
_snapshot_fail
103+
104+
# Check that the script has an exit 0 associated with DSO_MECHANICAL_AMEND
105+
_RG_EXIT_PATTERN=$(awk '/DSO_MECHANICAL_AMEND/{found=1} found && /exit 0/{print "yes"; exit}' "$REVIEW_GATE" 2>/dev/null || echo "")
106+
assert_eq "test_review_gate_exits_early_on_mechanical_amend" "yes" "$_RG_EXIT_PATTERN"
107+
108+
assert_pass_if_clean "test_review_gate_exits_early_on_mechanical_amend"
109+
110+
# =============================================================================
111+
# Test 6: test gate exits 0 early when DSO_MECHANICAL_AMEND=1
112+
# Same pattern as test 5 but for the test gate.
113+
# =============================================================================
114+
echo ""
115+
echo "--- test_test_gate_exits_early_on_mechanical_amend ---"
116+
_snapshot_fail
117+
118+
_TG_EXIT_PATTERN=$(awk '/DSO_MECHANICAL_AMEND/{found=1} found && /exit 0/{print "yes"; exit}' "$TEST_GATE" 2>/dev/null || echo "")
119+
assert_eq "test_test_gate_exits_early_on_mechanical_amend" "yes" "$_TG_EXIT_PATTERN"
120+
121+
assert_pass_if_clean "test_test_gate_exits_early_on_mechanical_amend"
122+
123+
# =============================================================================
124+
# Test 7: bypass sentinel blocks DSO_MECHANICAL_AMEND on non-amend commits
125+
# Layer 2 must detect DSO_MECHANICAL_AMEND=1 on raw "git commit" (without
126+
# --amend) and block it. This prevents misuse of the env var to bypass gates
127+
# on normal commits.
128+
# =============================================================================
129+
echo ""
130+
echo "--- test_sentinel_blocks_mechanical_amend_on_raw_commit ---"
131+
_snapshot_fail
132+
133+
_SENT_HAS_CHECK="not_found"
134+
grep -q 'DSO_MECHANICAL_AMEND' "$SENTINEL_LIB" 2>/dev/null && _SENT_HAS_CHECK="found"
135+
assert_eq "test_sentinel_has_mechanical_amend_check" "found" "$_SENT_HAS_CHECK"
136+
137+
# Functionally test: source the sentinel and call with a raw git commit that
138+
# has DSO_MECHANICAL_AMEND=1 as an inline prefix in the command string.
139+
# NOTE: The sentinel is a PreToolUse hook running in Claude Code's process, NOT
140+
# a child of the git subprocess. The env var must be detected in the command
141+
# string — exporting it to the test subshell would test the wrong execution model.
142+
_SENT_RC=0
143+
(
144+
source "$SENTINEL_LIB" 2>/dev/null
145+
# Simulate "DSO_MECHANICAL_AMEND=1 git commit -m test" (non-amend) — sentinel should block
146+
_INPUT='{"tool_name":"Bash","tool_input":{"command":"DSO_MECHANICAL_AMEND=1 git commit -m test"}}'
147+
hook_review_bypass_sentinel "$_INPUT" 2>/dev/null
148+
) || _SENT_RC=$?
149+
150+
assert_eq "test_sentinel_blocks_mechanical_amend_on_raw_commit" "2" "$_SENT_RC"
151+
152+
assert_pass_if_clean "test_sentinel_blocks_mechanical_amend_on_raw_commit"
153+
154+
# =============================================================================
155+
# Test 8: bypass sentinel allows DSO_MECHANICAL_AMEND on --amend --no-edit
156+
# Layer 2 must allow the env var when the command is specifically
157+
# "git commit --amend --no-edit" (the pattern used by merge-to-main.sh).
158+
# =============================================================================
159+
echo ""
160+
echo "--- test_sentinel_allows_mechanical_amend_on_amend_noedit ---"
161+
_snapshot_fail
162+
163+
_SENT_ALLOW_RC=0
164+
(
165+
source "$SENTINEL_LIB" 2>/dev/null
166+
# Simulate "DSO_MECHANICAL_AMEND=1 git commit --amend --no-edit --quiet" — sentinel should allow
167+
# The env var is in the command string (inline prefix), not exported to the environment,
168+
# matching the actual runtime model where the sentinel is a PreToolUse hook process.
169+
_INPUT='{"tool_name":"Bash","tool_input":{"command":"DSO_MECHANICAL_AMEND=1 git commit --amend --no-edit --quiet"}}'
170+
hook_review_bypass_sentinel "$_INPUT" 2>/dev/null
171+
) || _SENT_ALLOW_RC=$?
172+
173+
assert_eq "test_sentinel_allows_mechanical_amend_on_amend_noedit" "0" "$_SENT_ALLOW_RC"
174+
175+
assert_pass_if_clean "test_sentinel_allows_mechanical_amend_on_amend_noedit"
176+
177+
# =============================================================================
178+
print_summary

tests/hooks/test-review-gate-bypass-sentinel.sh

100644100755
File mode changed.

0 commit comments

Comments
 (0)