Skip to content

Commit 832abc0

Browse files
lapc506claude
andcommitted
feat(hooks): pre-push stale-branch detection + review-open-prs extension
Adds a proactive warning when force-pushing a branch that is >5 commits behind its base. Also extends the review-open-prs skill report with a new "Stale Branches (Drift Risk)" section that flags PRs likely failing CI because the base moved (e.g., file renames in merged PRs). Motivating incident: 2026-05-20 — DOJ-4134 atomic migration moved src/components/agent/ChatWidget.tsx. PRs #2105, #2107, #1713 in dojo-os each spent ~10 min diagnosing the same ENOENT failure that a 30-second rebase resolved. This package surfaces the pattern proactively. - hooks/pre-bash-stale-push.sh (new, warn-only) - hooks/pre-bash.sh (wire new hook before rule loop, after kill-switch) - skills/review-open-prs/SKILL.md (new section + Action 2a rebase batch) - hooks/test-hooks.sh (6 new hermetic tests; 216/216 pass, was 210/210) - package.json + plugin.json + marketplace.json + README + CHANGELOG bumped 1.15.0 → 1.16.0 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 7e92b02 commit 832abc0

9 files changed

Lines changed: 321 additions & 6 deletions

File tree

.claude-plugin/marketplace.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"$schema": "https://anthropic.com/claude-code/marketplace.schema.json",
33
"name": "make-no-mistakes",
4-
"version": "1.17.0",
4+
"version": "1.18.0",
55
"description": "The disciplined dev lifecycle — implement issues, review PRs, sync releases, test E2E, manage sessions, and stash secrets via OS-native prompts. One plugin to make no mistakes.",
66
"owner": {
77
"name": "Luis Andres Pena Castillo",
@@ -11,7 +11,7 @@
1111
{
1212
"name": "make-no-mistakes",
1313
"description": "Dev lifecycle orchestrator: disciplined Linear issue execution with worktree isolation, PR review with Greptile gating, team release sync, E2E test generation and execution, test suite previewer, security pentesting, MoSCoW + RICE prioritization, cross-platform secret stash via OS-native GUI prompts (zenity / kdialog / osascript / Get-Credential), and session management. 18 commands, 6 auto-activating skills, 2 specialized agents.",
14-
"version": "1.17.0",
14+
"version": "1.18.0",
1515
"author": {
1616
"name": "Luis Andres Pena Castillo",
1717
"email": "lapc506@users.noreply.github.com"

.claude-plugin/plugin.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "make-no-mistakes",
3-
"version": "1.17.0",
3+
"version": "1.18.0",
44
"description": "The disciplined dev lifecycle — implement issues, review PRs, sync releases, test E2E, manage sessions, stash secrets, and enforce manifest-driven tool-call hooks. One plugin to make no mistakes.",
55
"author": {
66
"name": "Luis Andres Pena Castillo",

CHANGELOG.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,42 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1818
1919
## [Unreleased]
2020

21+
## [1.18.0] - 2026-05-26
22+
23+
### Added
24+
- New hook: `hooks/pre-bash-stale-push.sh` (warn-only). Fires when a Bash
25+
tool call is a force-push (`git push --force-with-lease`, `--force`, or
26+
`-f`) AND the current `HEAD` is more than 5 commits behind the resolved
27+
base (preferring `origin/HEAD`, falling back to `develop``main`
28+
`master`). Emits a multi-line stderr warning with a copy-pasteable
29+
three-line rebase recipe. Never blocks — the hook always exits 0.
30+
Threshold tunable via `MAKE_NO_MISTAKES_STALE_THRESHOLD` env var. Wired
31+
into `hooks/pre-bash.sh` after the kill-switch check so
32+
`CLAUDE_DISABLE_PLUGIN_HOOKS=1` disables it alongside everything else.
33+
- New section in `skills/review-open-prs/SKILL.md`: **My PRs — Stale
34+
Branches (Drift Risk)**. Surfaces PRs that are >5 commits behind base
35+
AND have failing CI checks, separately from real CI bugs. Includes a
36+
matching **Action 2a** in the report's Suggested Course of Action that
37+
proposes a batched rebase before drilling into the failures —
38+
drift-induced failures often resolve themselves on rebase, and isolating
39+
them up front prevents wasted investigation cycles.
40+
- 6 new hook tests in `hooks/test-hooks.sh` covering the stale-push hook
41+
(non-push silent, in-threshold silent, stale warns, --dry-run skipped,
42+
-f short form detected, non-force-push silent). Tests are hermetic —
43+
each spins up a throwaway upstream + local clone in `mktemp -d`.
44+
45+
### Motivation
46+
- **2026-05-20 incident**: DOJ-4134 atomic migration moved
47+
`src/components/agent/ChatWidget.tsx``src/components/agent/organisms/ChatWidget.tsx`
48+
and updated a Vitest fixture in the same atomic merge. PRs in `dojo-os`
49+
that were cut from `develop` BEFORE that merge (#2105 DOJ-4135 accordion,
50+
#2107 VerificationBanner /home suppression, #1713 welcome flow) each kept
51+
the old test path, so their next CI run failed with
52+
`ENOENT: src/components/agent/ChatWidget.tsx`. Diagnosis took ~10 minutes
53+
per PR. Fix was always the same 30-second rebase + force-push-with-lease.
54+
This release surfaces that drift proactively (hook) and retroactively
55+
(skill section) so the pattern never has to be diagnosed again.
56+
2157
## [1.17.0] - 2026-05-25
2258

2359
### Added

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# make-no-mistakes
22

3-
**Version: 1.17.0** · [CHANGELOG](./CHANGELOG.md) · [Marketplace](https://github.com/DojoCodingLabs/make-no-mistakes-toolkit)
3+
**Version: 1.18.0** · [CHANGELOG](./CHANGELOG.md) · [Marketplace](https://github.com/DojoCodingLabs/make-no-mistakes-toolkit)
44

55
The disciplined dev lifecycle — implement issues, review PRs, sync releases, test E2E, and manage sessions. One plugin to make no mistakes.
66

hooks/pre-bash-stale-push.sh

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
#!/usr/bin/env bash
2+
# =============================================================================
3+
# pre-bash-stale-push.sh — Warn when force-pushing a branch that is far behind
4+
# its base (likely to fail CI because the base moved).
5+
#
6+
# Reads the Bash tool's tool_input JSON from stdin (same convention as
7+
# pre-bash.sh), extracts .command, and if it looks like a force push,
8+
# computes how many commits the local HEAD is behind the resolved base
9+
# (origin/HEAD → develop → main → master). If the gap exceeds the threshold,
10+
# emits a multi-line warning to stderr and exits 0.
11+
#
12+
# This hook is WARN-ONLY by design — it never blocks. Force-pushing a stale
13+
# branch is sometimes the correct action (e.g. intentionally diverging fork).
14+
# The hook surfaces the risk; the human decides.
15+
#
16+
# Motivating incident: 2026-05-20 — DOJ-4134 atomic migration moved
17+
# src/components/agent/ChatWidget.tsx. PRs #2105, #2107, #1713 in dojo-os
18+
# each spent ~10 min diagnosing the same ENOENT failure that a 30-second
19+
# rebase resolved.
20+
# =============================================================================
21+
set -uo pipefail
22+
23+
# Threshold: behind > N triggers the warning. Override with env var if you
24+
# want to tune sensitivity per-developer; 5 is conservative enough that the
25+
# noise floor is low while still catching real drift windows.
26+
THRESHOLD="${MAKE_NO_MISTAKES_STALE_THRESHOLD:-5}"
27+
28+
# Capture stdin (the Bash tool_input JSON).
29+
INPUT_RAW="$(cat)"
30+
31+
# If jq is missing, we can't parse the command — fail open silently. The
32+
# user shouldn't be blocked by our installation issues.
33+
if ! command -v jq >/dev/null 2>&1; then
34+
exit 0
35+
fi
36+
37+
COMMAND="$(printf '%s' "$INPUT_RAW" | jq -r '.tool_input.command // empty' 2>/dev/null || true)"
38+
if [ -z "$COMMAND" ]; then
39+
exit 0
40+
fi
41+
42+
# Detect force-push variants:
43+
# git push ... --force-with-lease (any form, with or without =refname)
44+
# git push ... --force
45+
# git push ... -f (short form, word-boundary so -fwd-style flags don't match)
46+
# Skip --dry-run since the user is explicitly previewing, not pushing.
47+
if printf '%s' "$COMMAND" | grep -qE '(^|[[:space:]])--dry-run([[:space:]]|$|=)'; then
48+
exit 0
49+
fi
50+
51+
if ! printf '%s' "$COMMAND" | grep -qE '(^|[[:space:]])git[[:space:]]+push\b'; then
52+
exit 0
53+
fi
54+
55+
if ! printf '%s' "$COMMAND" | grep -qE '(--force-with-lease|--force|(^|[[:space:]])-f([[:space:]]|$))'; then
56+
exit 0
57+
fi
58+
59+
# Resolve base branch. Prefer origin's HEAD symbolic ref; fall back to a
60+
# fixed candidate list. If none exist, exit 0 — we can't help.
61+
BASE=""
62+
63+
if SYMREF="$(git symbolic-ref refs/remotes/origin/HEAD --short 2>/dev/null)"; then
64+
# SYMREF is like "origin/develop"; strip "origin/" prefix.
65+
BASE="${SYMREF#origin/}"
66+
fi
67+
68+
if [ -z "$BASE" ]; then
69+
# Fall back to first existing remote ref. Order matters — develop wins
70+
# over main when both exist (most active repos in this org use develop).
71+
for candidate in develop main master; do
72+
if git show-ref --verify --quiet "refs/remotes/origin/${candidate}"; then
73+
BASE="$candidate"
74+
break
75+
fi
76+
done
77+
fi
78+
79+
if [ -z "$BASE" ]; then
80+
exit 0
81+
fi
82+
83+
# Compute behind count. If git fails (not in a repo, no fetch yet, etc.),
84+
# silently exit 0 — we never want this hook to be noisy on unrelated repos.
85+
BEHIND="$(git rev-list --count "HEAD..origin/${BASE}" 2>/dev/null || true)"
86+
87+
# Guard against empty / non-numeric results from git (e.g. "fatal: ...").
88+
case "$BEHIND" in
89+
''|*[!0-9]*) exit 0 ;;
90+
esac
91+
92+
if [ "$BEHIND" -le "$THRESHOLD" ]; then
93+
exit 0
94+
fi
95+
96+
# Fired. Multi-line warning to stderr; exit 0 (warn-only, never blocks).
97+
{
98+
echo ""
99+
echo "[make-no-mistakes:stale-push] WARNING"
100+
echo "You're force-pushing a branch that is ${BEHIND} commits behind origin/${BASE}."
101+
echo ""
102+
echo "CI failures on this push are likely if any of those ${BEHIND} commits touched"
103+
echo "files your branch also touches (test paths moved, exports renamed, etc.)."
104+
echo ""
105+
echo "Suggested: cancel this push, then run:"
106+
echo " git fetch origin ${BASE}"
107+
echo " git rebase origin/${BASE}"
108+
echo " git push --force-with-lease"
109+
echo ""
110+
echo "Or push as-is and accept the risk. This is a warning, not a block."
111+
} >&2
112+
113+
exit 0

hooks/pre-bash.sh

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,14 @@ fi
3333
# Capture stdin once. Each rule invocation re-pipes this same payload.
3434
INPUT_RAW="$(cat)"
3535

36+
# Stale-push warning hook — runs alongside the manifest rule loop and shares
37+
# the same stdin payload. It is warn-only by contract (never exits non-zero
38+
# in a way that should abort the dispatcher), but we also guard with `|| true`
39+
# so a future bug in that script cannot turn into a hard block here.
40+
if [ -x "$HOOKS_DIR/pre-bash-stale-push.sh" ]; then
41+
printf '%s' "$INPUT_RAW" | "$HOOKS_DIR/pre-bash-stale-push.sh" || true
42+
fi
43+
3644
EXIT_CODE=0
3745
while IFS= read -r RULE_ID; do
3846
[ -z "$RULE_ID" ] && continue

hooks/test-hooks.sh

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,114 @@ while IFS=$'\t' read -r RULE_ID TEST_IDX; do
8787
rm -f "$STDERR_FILE"
8888
done <<< "$PAIRS"
8989

90+
# =============================================================================
91+
# Standalone hook tests — pre-bash-stale-push.sh
92+
#
93+
# This hook lives outside the rules manifest because its predicate is shell
94+
# logic (calls `git` to compute behind-by count), not a regex. Each case
95+
# stages an isolated git repo in a tempdir so the test is hermetic — we
96+
# never depend on the toolkit repo's own branch state.
97+
# =============================================================================
98+
STALE_HOOK="$HOOKS_DIR/pre-bash-stale-push.sh"
99+
100+
if [ -x "$STALE_HOOK" ]; then
101+
# Helper: spin up two work trees ("upstream" + local clone) with the local
102+
# clone's HEAD `$1` commits behind origin/main.
103+
# Returns the local clone path on stdout.
104+
setup_stale_repo() {
105+
local behind="$1"
106+
local base
107+
base="$(mktemp -d)"
108+
(
109+
# Upstream working tree — we'll push commits here to simulate develop
110+
# moving forward.
111+
mkdir -p "$base/upstream" "$base/local"
112+
cd "$base/upstream" || exit 1
113+
git init -q --initial-branch=main
114+
git config user.email t@t.t
115+
git config user.name t
116+
git commit -q --allow-empty -m "base"
117+
118+
# Local clone — HEAD points at the same base commit
119+
cd "$base" || exit 1
120+
git clone -q upstream local >/dev/null 2>&1
121+
cd "$base/local" || exit 1
122+
git config user.email t@t.t
123+
git config user.name t
124+
git checkout -q -b feature
125+
git symbolic-ref refs/remotes/origin/HEAD refs/remotes/origin/main
126+
127+
# Advance upstream by N commits, then fetch into local so origin/main
128+
# is N ahead of our feature HEAD.
129+
cd "$base/upstream" || exit 1
130+
local i=0
131+
while [ "$i" -lt "$behind" ]; do
132+
git commit -q --allow-empty -m "upstream-$i"
133+
i=$((i + 1))
134+
done
135+
cd "$base/local" || exit 1
136+
git fetch -q origin
137+
)
138+
echo "$base/local"
139+
}
140+
141+
run_stale_case() {
142+
local name="$1" command="$2" behind="$3" expected_warn="$4"
143+
local repo input stderr_file exit_code
144+
repo="$(setup_stale_repo "$behind")"
145+
input="$(jq -nc --arg c "$command" '{tool_input:{command:$c}}')"
146+
stderr_file="$(mktemp)"
147+
exit_code=0
148+
(cd "$repo" && printf '%s' "$input" | bash "$STALE_HOOK") 2>"$stderr_file" || exit_code=$?
149+
150+
local status="PASS" reason=""
151+
if [ "$exit_code" != "0" ]; then
152+
status="FAIL"; reason="exit expected=0 actual=$exit_code"
153+
elif [ "$expected_warn" = "yes" ]; then
154+
if ! grep -qF "[make-no-mistakes:stale-push]" "$stderr_file"; then
155+
status="FAIL"; reason="expected warning, got none"
156+
fi
157+
else
158+
if grep -qF "[make-no-mistakes:stale-push]" "$stderr_file"; then
159+
status="FAIL"; reason="expected silence, got warning"
160+
fi
161+
fi
162+
163+
if [ "$status" = "PASS" ]; then
164+
PASS=$((PASS + 1))
165+
echo " PASS stale-push / ${name}"
166+
else
167+
FAIL=$((FAIL + 1))
168+
echo " FAIL stale-push / ${name} -- ${reason}"
169+
FAIL_DETAILS+=("stale-push/${name}: ${reason}")
170+
fi
171+
172+
# setup_stale_repo returns "$base/local" — clean up the parent so we
173+
# also remove the upstream sibling, not just the local clone.
174+
rm -rf "$(dirname "$repo")" "$stderr_file"
175+
}
176+
177+
# Case A: not a push command — must exit silently, no warning
178+
run_stale_case "skips-non-push-command" "ls -la" 100 "no"
179+
180+
# Case B: force-push but branch is only 1 commit behind (≤ threshold of 5) — silent
181+
run_stale_case "skips-push-within-threshold" "git push --force-with-lease" 1 "no"
182+
183+
# Case C: force-push and branch is 10 commits behind (> threshold) — warn
184+
run_stale_case "warns-on-stale-force-push" "git push --force-with-lease" 10 "yes"
185+
186+
# Case D: --dry-run should always skip even when stale
187+
run_stale_case "skips-dry-run" "git push --force-with-lease --dry-run" 20 "no"
188+
189+
# Case E: -f short form should be detected
190+
run_stale_case "detects-short-form-f-flag" "git push -f origin feature" 8 "yes"
191+
192+
# Case F: non-force push (no --force / --force-with-lease / -f) — silent
193+
run_stale_case "skips-non-force-push" "git push origin feature" 50 "no"
194+
else
195+
echo " SKIP stale-push (hook not executable at $STALE_HOOK)"
196+
fi
197+
90198
echo ""
91199
TOTAL=$((PASS + FAIL))
92200
echo "Results: ${PASS} / ${TOTAL} passed"

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@lapc506/make-no-mistakes",
3-
"version": "1.17.0",
3+
"version": "1.18.0",
44
"description": "The disciplined dev lifecycle — implement issues, review PRs, sync releases, test E2E, manage sessions, stash secrets, and enforce manifest-driven tool-call hooks (no SSH+DB, no manual prod, no minified build, no secret leaks, Slack format). OpenCode + Claude Code plugin.",
55
"type": "module",
66
"main": "./dist/index.js",

skills/review-open-prs/SKILL.md

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -198,8 +198,36 @@ PR #number — Title
198198
URL
199199
```
200200

201+
### My PRs — Stale Branches (Drift Risk)
202+
PRs where the branch is >5 commits behind base AND has ANY failing CI check. These are likely failing because the base moved (e.g., a test file was moved/renamed in a merged PR but this branch still has the old path). Recommend a preventive rebase BEFORE more CI cycles burn.
203+
204+
**Investigate this section BEFORE the "CI Failures" section below** — stale-with-CI-fail is a likely false positive that can be resolved with a 30-second rebase. Pulling apart real test bugs from drift-induced failures is much faster when you've already ruled out drift.
205+
206+
For each candidate PR, collect the behind-by count using the GitHub Compare API (one call per PR; do this inside the per-PR loop in Step 5):
207+
208+
```bash
209+
# Inside the per-PR loop in Step 5 — after fetching CI status:
210+
base_branch=$(gh pr view "$pr_number" --repo "$ORG/<repo>" --json baseRefName --jq '.baseRefName')
211+
head_sha=$(gh pr view "$pr_number" --repo "$ORG/<repo>" --json headRefOid --jq '.headRefOid')
212+
behind=$(gh api "repos/$ORG/<repo>/compare/${base_branch}...${head_sha}" --jq '.behind_by' 2>/dev/null)
213+
```
214+
215+
A PR qualifies for this section if:
216+
- `behind` > 5, AND
217+
- At least one CI check has `conclusion=FAILURE`
218+
219+
Format:
220+
```
221+
PR #number — Title — {behind} commits behind {base}
222+
Failed checks: list of failed check names
223+
Suggested: git fetch && git rebase origin/{base} && git push --force-with-lease
224+
URL
225+
```
226+
227+
If you can pattern-match the failing test path against files renamed in recent commits to `{base}` (e.g., `gh api repos/$ORG/<repo>/compare/${head_sha}...origin/${base_branch}` and grep `.files[].previous_filename`), call that out as "Likely cause: file renamed in merged PR #X".
228+
201229
### My PRs — CI Failures
202-
PRs with any check conclusion=FAILURE:
230+
PRs with any check conclusion=FAILURE that are NOT already listed under "Stale Branches" above:
203231
```
204232
PR #number — Title
205233
Failed checks: list of failed check names
@@ -281,6 +309,28 @@ Rules:
281309
- If unsure who to CC, ask the user
282310
- Do NOT use unicode bullet points (no ``, ``, ``) — use `-` at all levels
283311

312+
### Action 2a: Rebase stale branches (drift risk)
313+
314+
List every PR from "Stale Branches (Drift Risk)". These should be addressed BEFORE Action 3 (CI Failures) because the rebase often resolves the failures for free:
315+
316+
```
317+
The following PRs are >5 commits behind base AND have failing CI. The failures
318+
are likely caused by the base moving (e.g., a test file was renamed in a merged
319+
PR). A rebase will likely make them go green:
320+
321+
repo#number — Title — {N} commits behind {base}
322+
...
323+
324+
Shall I rebase them against the base branch? (yes/no/pick)
325+
```
326+
327+
If the user says "yes", for each stale PR:
328+
1. Check out the branch locally or in a worktree
329+
2. `git fetch origin <base>`
330+
3. `git rebase origin/<base>` (auto-resolve trivial conflicts; flag manual ones)
331+
4. `git push --force-with-lease` (with user confirmation)
332+
5. Re-poll CI after a few minutes — if it's now green, the failure was drift; if it's still red, escalate to Action 3.
333+
284334
### Action 2: Fix merge conflicts / rebase
285335

286336
List every PR from "Merge Conflicts / Needs Rebase":

0 commit comments

Comments
 (0)