@reddb-io/red-skills-dev 2.75.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +30 -0
- package/package.json +32 -0
- package/skills/engineering/README.md +36 -0
- package/skills/engineering/_report-runtime/WRAPPER.md +47 -0
- package/skills/engineering/afk/AGENT-PROMPT.md +295 -0
- package/skills/engineering/afk/SAFETY.md +103 -0
- package/skills/engineering/afk/SKILL.md +160 -0
- package/skills/engineering/afk/TROUBLESHOOTING.md +260 -0
- package/skills/engineering/afk/actions-lane.md +213 -0
- package/skills/engineering/afk/bin/README.md +29 -0
- package/skills/engineering/afk/bin/afk.mjs +51 -0
- package/skills/engineering/afk/defaults/cargo-pre-worktree.sh +37 -0
- package/skills/engineering/afk/defaults/envelope-post-attempt.sh +47 -0
- package/skills/engineering/afk/defaults/gradle-pre-worktree.sh +42 -0
- package/skills/engineering/afk/defaults/heartbeat-post-attempt.sh +43 -0
- package/skills/engineering/afk/defaults/validation-post-merge.sh +77 -0
- package/skills/engineering/afk/detectors/README.md +72 -0
- package/skills/engineering/afk/detectors/cargo.sh +27 -0
- package/skills/engineering/afk/detectors/gradle.sh +35 -0
- package/skills/engineering/afk/docs/BOOT-SWEEPS.md +81 -0
- package/skills/engineering/afk/docs/CONFIG.md +238 -0
- package/skills/engineering/afk/docs/ENVELOPE.md +177 -0
- package/skills/engineering/afk/docs/HANDOFF.md +70 -0
- package/skills/engineering/afk/docs/LIVENESS.md +56 -0
- package/skills/engineering/afk/docs/OPERATIONS.md +409 -0
- package/skills/engineering/afk/examples/only-mine.sh +32 -0
- package/skills/engineering/afk/examples/red-afk-attempt-action.yml +53 -0
- package/skills/engineering/afk/examples/rs-afk-attempt.yml +76 -0
- package/skills/engineering/afk/fleet.md +127 -0
- package/skills/engineering/afk/hooks/red-cargo +44 -0
- package/skills/engineering/afk/hooks/red-envelope +50 -0
- package/skills/engineering/afk/hooks/red-gradle +50 -0
- package/skills/engineering/afk/hooks/red-heartbeat +46 -0
- package/skills/engineering/afk/hooks/red-validation +79 -0
- package/skills/engineering/afk/monitor.md +170 -0
- package/skills/engineering/afk/runner-claude-minimax.md +71 -0
- package/skills/engineering/afk/runner-claude.md +67 -0
- package/skills/engineering/afk/runner-codex.md +138 -0
- package/skills/engineering/afk/runner-hermes.md +40 -0
- package/skills/engineering/afk/runner-opencode.md +142 -0
- package/skills/engineering/ask-red/SKILL.md +137 -0
- package/skills/engineering/audit-skills/SKILL.md +46 -0
- package/skills/engineering/code-review/SKILL.md +106 -0
- package/skills/engineering/context/SKILL.md +142 -0
- package/skills/engineering/curate/SKILL.md +133 -0
- package/skills/engineering/daily-review/SKILL.md +68 -0
- package/skills/engineering/dashboard/SKILL.md +44 -0
- package/skills/engineering/diagnose/SKILL.md +155 -0
- package/skills/engineering/diagnose/scripts/hitl-loop.template.sh +41 -0
- package/skills/engineering/go/SKILL.md +144 -0
- package/skills/engineering/go/TROUBLESHOOTING.md +81 -0
- package/skills/engineering/ground-truth/SKILL.md +84 -0
- package/skills/engineering/hitl/SKILL.md +171 -0
- package/skills/engineering/hitl/TROUBLESHOOTING.md +76 -0
- package/skills/engineering/implement/SKILL.md +69 -0
- package/skills/engineering/improve-codebase-architecture/DEEPENING.md +37 -0
- package/skills/engineering/improve-codebase-architecture/INTERFACE-DESIGN.md +44 -0
- package/skills/engineering/improve-codebase-architecture/LANGUAGE.md +53 -0
- package/skills/engineering/improve-codebase-architecture/SKILL.md +81 -0
- package/skills/engineering/model-tier-policy/SKILL.md +83 -0
- package/skills/engineering/prototype/LOGIC.md +79 -0
- package/skills/engineering/prototype/SKILL.md +30 -0
- package/skills/engineering/prototype/UI.md +112 -0
- package/skills/engineering/red-doctor/APPLY.md +35 -0
- package/skills/engineering/red-doctor/SKILL.md +172 -0
- package/skills/engineering/red-gains/SKILL.md +49 -0
- package/skills/engineering/red-setup/INTERVIEW.md +266 -0
- package/skills/engineering/red-setup/ISSUE-SWEEP.md +18 -0
- package/skills/engineering/red-setup/REFERENCE.md +35 -0
- package/skills/engineering/red-setup/SKILL.md +58 -0
- package/skills/engineering/red-setup/WORKFLOWS.md +74 -0
- package/skills/engineering/red-setup/WRITE-CONTRACT.md +151 -0
- package/skills/engineering/red-setup/config-template.yaml +174 -0
- package/skills/engineering/red-setup/domain.md +52 -0
- package/skills/engineering/red-setup/issue-tracker-github.md +88 -0
- package/skills/engineering/red-setup/rsp-retirement-runbook.md +35 -0
- package/skills/engineering/red-setup/scripts/install-runtime-shim.sh +189 -0
- package/skills/engineering/red-setup/triage-labels.md +176 -0
- package/skills/engineering/red-setup/workflows/red-issues-needs-triage.yml +36 -0
- package/skills/engineering/red-statusline/HOST-NOTES.md +180 -0
- package/skills/engineering/red-statusline/SKILL.md +58 -0
- package/skills/engineering/report-bug/SKILL.md +155 -0
- package/skills/engineering/resolving-merge-conflicts/SKILL.md +143 -0
- package/skills/engineering/retake/SKILL.md +184 -0
- package/skills/engineering/review-adrs/SKILL.md +125 -0
- package/skills/engineering/start/ADR-FORMAT.md +47 -0
- package/skills/engineering/start/CONTEXT-FORMAT.md +77 -0
- package/skills/engineering/start/DOC-LANDING-FINALIZER.md +9 -0
- package/skills/engineering/start/SKILL.md +179 -0
- package/skills/engineering/tdd/SKILL.md +96 -0
- package/skills/engineering/tdd/deep-modules.md +33 -0
- package/skills/engineering/tdd/interface-design.md +31 -0
- package/skills/engineering/tdd/mocking.md +59 -0
- package/skills/engineering/tdd/tests.md +85 -0
- package/skills/engineering/to-spec/SKILL.md +104 -0
- package/skills/engineering/to-tickets/SKILL.md +158 -0
- package/skills/engineering/triage/AGENT-BRIEF.md +207 -0
- package/skills/engineering/triage/OUT-OF-SCOPE.md +101 -0
- package/skills/engineering/triage/SKILL.md +176 -0
- package/skills/engineering/verify/SKILL.md +136 -0
- package/skills/engineering/wayfinder/SKILL.md +128 -0
- package/skills/engineering/zoom-out/SKILL.md +51 -0
- package/skills/engineering/zoom-out/scripts/graph-mode-plumbing.sh +36 -0
- package/skills/engineering/zoom-out/scripts/tests/contract.test.sh +168 -0
- package/skills/knowledge/README.md +5 -0
- package/skills/knowledge/research/SKILL.md +76 -0
- package/skills/misc/README.md +8 -0
- package/skills/misc/branch-lock/SKILL.md +187 -0
- package/skills/misc/branch-lock/scripts/branch-lock-hook.sh +113 -0
- package/skills/misc/branch-lock/scripts/branch-lock-session-start.sh +81 -0
- package/skills/misc/branch-lock/scripts/branch-lock.sh +68 -0
- package/skills/misc/branch-lock/scripts/lib/dev-config.sh +126 -0
- package/skills/misc/branch-lock/scripts/lib/git-command-classifier.sh +198 -0
- package/skills/misc/branch-lock/scripts/lib/lock-store.sh +67 -0
- package/skills/misc/branch-lock/scripts/lib/scope-resolver.sh +43 -0
- package/skills/misc/branch-lock/scripts/tests/branch-lock-cli.test.sh +114 -0
- package/skills/misc/branch-lock/scripts/tests/claude-plugin-hook.test.sh +117 -0
- package/skills/misc/branch-lock/scripts/tests/codex-hook.test.sh +229 -0
- package/skills/misc/branch-lock/scripts/tests/dev-config.test.sh +99 -0
- package/skills/misc/branch-lock/scripts/tests/git-command-classifier.test.sh +187 -0
- package/skills/misc/branch-lock/scripts/tests/lock-store.test.sh +90 -0
- package/skills/misc/branch-lock/scripts/tests/scope-resolver.test.sh +63 -0
- package/skills/misc/branch-lock/scripts/tests/session-start.test.sh +123 -0
- package/skills/misc/git-guardrails-claude-code/SKILL.md +105 -0
- package/skills/misc/git-guardrails-claude-code/scripts/block-dangerous-git.sh +231 -0
- package/skills/misc/git-guardrails-claude-code/scripts/tests/block-dangerous-git.test.sh +143 -0
- package/skills/misc/migrate-to-shoehorn/SKILL.md +96 -0
- package/skills/misc/setup-pre-commit/SKILL.md +94 -0
- package/skills/productivity/README.md +8 -0
- package/skills/productivity/ff/SKILL.md +116 -0
- package/skills/productivity/handoff/SKILL.md +37 -0
- package/skills/productivity/reflect/SKILL.md +18 -0
- package/skills/productivity/write-a-skill/SKILL.md +190 -0
- package/skills/productivity/write-a-skill/WRITING-STYLE.md +62 -0
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Unit test for lib/scope-resolver.sh (issue #60).
|
|
3
|
+
#
|
|
4
|
+
# The lock enforces in the primary checkout and is exempt in registered
|
|
5
|
+
# .red/tmp worktree lanes plus legacy .red/tmp/work-* worktrees. Verifies both
|
|
6
|
+
# scope_is_worktree and its inverse scope_should_enforce across primary roots,
|
|
7
|
+
# worktree roots, and paths nested inside a worktree.
|
|
8
|
+
|
|
9
|
+
set -uo pipefail
|
|
10
|
+
|
|
11
|
+
HERE="$(cd "$(dirname "$0")" && pwd)"
|
|
12
|
+
LIB="$(dirname "$HERE")/lib/scope-resolver.sh"
|
|
13
|
+
|
|
14
|
+
# shellcheck source=../lib/scope-resolver.sh
|
|
15
|
+
source "$LIB"
|
|
16
|
+
|
|
17
|
+
pass=0
|
|
18
|
+
fail=0
|
|
19
|
+
|
|
20
|
+
expect_eq() {
|
|
21
|
+
local label="$1" expected="$2" actual="$3"
|
|
22
|
+
if [[ "$expected" == "$actual" ]]; then
|
|
23
|
+
echo "PASS $label"; pass=$((pass + 1))
|
|
24
|
+
else
|
|
25
|
+
printf 'FAIL %s\n expected: %q\n actual: %q\n' "$label" "$expected" "$actual"
|
|
26
|
+
fail=$((fail + 1))
|
|
27
|
+
fi
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
rc_of() { "$@"; echo $?; }
|
|
31
|
+
|
|
32
|
+
# ===========================================================================
|
|
33
|
+
# primary checkout => enforce (not a worktree)
|
|
34
|
+
# ===========================================================================
|
|
35
|
+
PRIMARY="/home/dev/Work/reddb.io/red-skills"
|
|
36
|
+
expect_eq "primary/is_worktree rc 1" "1" "$(rc_of scope_is_worktree "$PRIMARY")"
|
|
37
|
+
expect_eq "primary/should_enforce rc 0" "0" "$(rc_of scope_should_enforce "$PRIMARY")"
|
|
38
|
+
|
|
39
|
+
# a path that merely contains '.red/tmp' but is not a work-* worktree still enforces
|
|
40
|
+
NEAR="/home/dev/Work/reddb.io/red-skills/.red/tmp"
|
|
41
|
+
expect_eq "near/is_worktree rc 1" "1" "$(rc_of scope_is_worktree "$NEAR")"
|
|
42
|
+
|
|
43
|
+
# ===========================================================================
|
|
44
|
+
# registered worktree lane root => exempt
|
|
45
|
+
# ===========================================================================
|
|
46
|
+
WT="/home/dev/Work/reddb.io/red-skills/.red/tmp/worktrees/manual/fix-docs"
|
|
47
|
+
expect_eq "worktree/is_worktree rc 0" "0" "$(rc_of scope_is_worktree "$WT")"
|
|
48
|
+
expect_eq "worktree/should_enforce rc 1" "1" "$(rc_of scope_should_enforce "$WT")"
|
|
49
|
+
|
|
50
|
+
# legacy .red/tmp/work-* remains exempt during migration
|
|
51
|
+
WTROOT="/home/dev/Work/reddb.io/red-skills/.red/tmp/work-abc-i1"
|
|
52
|
+
expect_eq "worktree-root/is_worktree rc 0" "0" "$(rc_of scope_is_worktree "$WTROOT")"
|
|
53
|
+
|
|
54
|
+
# ===========================================================================
|
|
55
|
+
# nested path inside a worktree => still exempt
|
|
56
|
+
# ===========================================================================
|
|
57
|
+
NESTED="/home/dev/Work/reddb.io/red-skills/.red/tmp/worktrees/manual/fix-docs/plugins/dev"
|
|
58
|
+
expect_eq "nested/is_worktree rc 0" "0" "$(rc_of scope_is_worktree "$NESTED")"
|
|
59
|
+
expect_eq "nested/should_enforce rc 1" "1" "$(rc_of scope_should_enforce "$NESTED")"
|
|
60
|
+
|
|
61
|
+
echo
|
|
62
|
+
echo "summary: $pass passed, $fail failed"
|
|
63
|
+
[ "$fail" -eq 0 ]
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Unit test for branch-lock-session-start.sh (issue #62).
|
|
3
|
+
#
|
|
4
|
+
# The SessionStart hook offers — once, at session start — to lock the agent to
|
|
5
|
+
# the current branch. It must:
|
|
6
|
+
# - emit an instruction (exit 0, JSON on stdout) in the primary checkout when
|
|
7
|
+
# the branch is unlocked, naming the current branch;
|
|
8
|
+
# - stay silent (no stdout) inside an /afk worktree (scope exemption);
|
|
9
|
+
# - stay silent when a lock is already present (nothing to offer);
|
|
10
|
+
# - stay silent on a detached HEAD (no branch to lock).
|
|
11
|
+
#
|
|
12
|
+
# Exercises the pure decision/text helpers sourced from the hook script, plus an
|
|
13
|
+
# end-to-end run of the hook against throwaway git repos.
|
|
14
|
+
|
|
15
|
+
set -uo pipefail
|
|
16
|
+
|
|
17
|
+
HERE="$(cd "$(dirname "$0")" && pwd)"
|
|
18
|
+
HOOK="$(dirname "$HERE")/branch-lock-session-start.sh"
|
|
19
|
+
|
|
20
|
+
# Source the script for its pure helpers (the script guards main on direct run).
|
|
21
|
+
# shellcheck source=../branch-lock-session-start.sh
|
|
22
|
+
source "$HOOK"
|
|
23
|
+
|
|
24
|
+
pass=0
|
|
25
|
+
fail=0
|
|
26
|
+
|
|
27
|
+
expect_eq() {
|
|
28
|
+
local label="$1" expected="$2" actual="$3"
|
|
29
|
+
if [[ "$expected" == "$actual" ]]; then
|
|
30
|
+
echo "PASS $label"; pass=$((pass + 1))
|
|
31
|
+
else
|
|
32
|
+
printf 'FAIL %s\n expected: %q\n actual: %q\n' "$label" "$expected" "$actual"
|
|
33
|
+
fail=$((fail + 1))
|
|
34
|
+
fi
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
expect_contains() {
|
|
38
|
+
local label="$1" needle="$2" haystack="$3"
|
|
39
|
+
if [[ "$haystack" == *"$needle"* ]]; then
|
|
40
|
+
echo "PASS $label"; pass=$((pass + 1))
|
|
41
|
+
else
|
|
42
|
+
printf 'FAIL %s\n missing: %q\n in: %q\n' "$label" "$needle" "$haystack"
|
|
43
|
+
fail=$((fail + 1))
|
|
44
|
+
fi
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
rc_of() { "$@"; echo $?; }
|
|
48
|
+
|
|
49
|
+
# ===========================================================================
|
|
50
|
+
# session_start_should_prompt <root> <lockfile> <branch>
|
|
51
|
+
# prompt only when: primary checkout AND unlocked AND a real branch.
|
|
52
|
+
# ===========================================================================
|
|
53
|
+
PRIMARY="/home/dev/Work/reddb.io/red-skills"
|
|
54
|
+
WT="/home/dev/Work/reddb.io/red-skills/.red/tmp/work-abc-i1/worktree"
|
|
55
|
+
ABSENT_LOCK="$(mktemp -u)" # path that does not exist => unlocked
|
|
56
|
+
|
|
57
|
+
expect_eq "primary+unlocked+branch => prompt (rc 0)" \
|
|
58
|
+
"0" "$(rc_of session_start_should_prompt "$PRIMARY" "$ABSENT_LOCK" "main")"
|
|
59
|
+
|
|
60
|
+
expect_eq "worktree => no prompt (rc 1)" \
|
|
61
|
+
"1" "$(rc_of session_start_should_prompt "$WT" "$ABSENT_LOCK" "main")"
|
|
62
|
+
|
|
63
|
+
expect_eq "detached HEAD => no prompt (rc 1)" \
|
|
64
|
+
"1" "$(rc_of session_start_should_prompt "$PRIMARY" "$ABSENT_LOCK" "HEAD")"
|
|
65
|
+
|
|
66
|
+
expect_eq "empty branch => no prompt (rc 1)" \
|
|
67
|
+
"1" "$(rc_of session_start_should_prompt "$PRIMARY" "$ABSENT_LOCK" "")"
|
|
68
|
+
|
|
69
|
+
PRESENT_LOCK="$(mktemp)"; printf 'main\n' > "$PRESENT_LOCK"
|
|
70
|
+
expect_eq "already locked => no prompt (rc 1)" \
|
|
71
|
+
"1" "$(rc_of session_start_should_prompt "$PRIMARY" "$PRESENT_LOCK" "feature/x")"
|
|
72
|
+
rm -f "$PRESENT_LOCK"
|
|
73
|
+
|
|
74
|
+
# ===========================================================================
|
|
75
|
+
# session_start_prompt_text <branch> — names the branch, instructs to ask.
|
|
76
|
+
# ===========================================================================
|
|
77
|
+
TEXT="$(session_start_prompt_text "feature/login")"
|
|
78
|
+
expect_contains "prompt text names the branch" "feature/login" "$TEXT"
|
|
79
|
+
expect_contains "prompt text says to ask the user" "ask" "$TEXT"
|
|
80
|
+
expect_contains "prompt text points at /branch-lock" "/branch-lock" "$TEXT"
|
|
81
|
+
|
|
82
|
+
# ===========================================================================
|
|
83
|
+
# End-to-end: run the hook against throwaway repos.
|
|
84
|
+
# ===========================================================================
|
|
85
|
+
mk_repo() {
|
|
86
|
+
local dir="$1"
|
|
87
|
+
git init -q "$dir"
|
|
88
|
+
git -C "$dir" config user.email t@t.t
|
|
89
|
+
git -C "$dir" config user.name t
|
|
90
|
+
git -C "$dir" commit -q --allow-empty -m init
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
run_hook() { CLAUDE_PROJECT_DIR="$1" bash "$HOOK" <<<'{"hook_event_name":"SessionStart"}'; }
|
|
94
|
+
|
|
95
|
+
# primary checkout, unlocked => emits JSON with additionalContext naming branch
|
|
96
|
+
TMP_PRIMARY="$(mktemp -d)/red-skills"
|
|
97
|
+
mk_repo "$TMP_PRIMARY"
|
|
98
|
+
git -C "$TMP_PRIMARY" branch -m work-branch
|
|
99
|
+
OUT="$(run_hook "$TMP_PRIMARY")"
|
|
100
|
+
expect_contains "e2e primary: emits SessionStart event" "SessionStart" "$OUT"
|
|
101
|
+
expect_contains "e2e primary: names the branch" "work-branch" "$OUT"
|
|
102
|
+
expect_contains "e2e primary: valid additionalContext" "additionalContext" "$OUT"
|
|
103
|
+
if command -v jq >/dev/null 2>&1; then
|
|
104
|
+
CTX="$(jq -r '.hookSpecificOutput.additionalContext' <<<"$OUT" 2>/dev/null)"
|
|
105
|
+
expect_contains "e2e primary: JSON parses, branch in context" "work-branch" "$CTX"
|
|
106
|
+
fi
|
|
107
|
+
|
|
108
|
+
# already locked => silent (empty stdout)
|
|
109
|
+
mkdir -p "$TMP_PRIMARY/.red/tmp"
|
|
110
|
+
printf 'work-branch\n' > "$TMP_PRIMARY/.red/tmp/branch-lock.yaml"
|
|
111
|
+
OUT_LOCKED="$(run_hook "$TMP_PRIMARY")"
|
|
112
|
+
expect_eq "e2e locked: silent" "" "$OUT_LOCKED"
|
|
113
|
+
|
|
114
|
+
# worktree => silent even when unlocked
|
|
115
|
+
TMP_WT="$(mktemp -d)/red-skills/.red/tmp/work-zzz-i1/worktree"
|
|
116
|
+
mkdir -p "$(dirname "$TMP_WT")"
|
|
117
|
+
mk_repo "$TMP_WT"
|
|
118
|
+
OUT_WT="$(run_hook "$TMP_WT")"
|
|
119
|
+
expect_eq "e2e worktree: silent" "" "$OUT_WT"
|
|
120
|
+
|
|
121
|
+
echo
|
|
122
|
+
echo "summary: $pass passed, $fail failed"
|
|
123
|
+
[ "$fail" -eq 0 ]
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: git-guardrails-claude-code
|
|
3
|
+
description: Set up Claude Code hooks to block dangerous git commands (push, reset --hard, clean, branch -D, etc.) before they execute. Use when user wants to prevent destructive git operations, add git safety hooks, or block git push/reset in Claude Code.
|
|
4
|
+
disable-model-invocation: true
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Setup Git Guardrails
|
|
8
|
+
|
|
9
|
+
**Block destructive git ops before they run.**
|
|
10
|
+
|
|
11
|
+
Sets up a PreToolUse hook that intercepts and blocks dangerous git commands before Claude executes them.
|
|
12
|
+
|
|
13
|
+
<what-to-do>
|
|
14
|
+
|
|
15
|
+
## 1. Ask scope
|
|
16
|
+
|
|
17
|
+
Ask the user: install for **this project only** (`.claude/settings.json`) or **all projects** (`~/.claude/settings.json`)?
|
|
18
|
+
|
|
19
|
+
## 2. Copy the hook script
|
|
20
|
+
|
|
21
|
+
The bundled script is at: [scripts/block-dangerous-git.sh](scripts/block-dangerous-git.sh)
|
|
22
|
+
|
|
23
|
+
Copy it to the target location based on scope, then make it executable with `chmod +x`:
|
|
24
|
+
|
|
25
|
+
- **Project**: `.claude/hooks/block-dangerous-git.sh`
|
|
26
|
+
- **Global**: `~/.claude/hooks/block-dangerous-git.sh`
|
|
27
|
+
|
|
28
|
+
## 3. Add hook to settings
|
|
29
|
+
|
|
30
|
+
Add the `PreToolUse` hook to the appropriate settings file. The block is identical for both scopes — only the `command` path differs: **project** uses `"$CLAUDE_PROJECT_DIR"/.claude/hooks/block-dangerous-git.sh`, **global** uses `~/.claude/hooks/block-dangerous-git.sh`.
|
|
31
|
+
|
|
32
|
+
```json
|
|
33
|
+
{
|
|
34
|
+
"hooks": {
|
|
35
|
+
"PreToolUse": [
|
|
36
|
+
{
|
|
37
|
+
"matcher": "Bash",
|
|
38
|
+
"hooks": [
|
|
39
|
+
{
|
|
40
|
+
"type": "command",
|
|
41
|
+
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/block-dangerous-git.sh"
|
|
42
|
+
}
|
|
43
|
+
]
|
|
44
|
+
}
|
|
45
|
+
]
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
If the settings file already exists, merge the hook into the existing `hooks.PreToolUse` array — don't overwrite other settings.
|
|
51
|
+
|
|
52
|
+
## 4. Ask about customization
|
|
53
|
+
|
|
54
|
+
Ask if the user wants to add or remove any patterns from the blocked list. Edit the copied script accordingly.
|
|
55
|
+
|
|
56
|
+
## 5. Verify
|
|
57
|
+
|
|
58
|
+
Run a quick test:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
echo '{"tool_input":{"command":"git push origin main"}}' | <path-to-script>
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Should exit with code 2 and print a BLOCKED message to stderr.
|
|
65
|
+
|
|
66
|
+
</what-to-do>
|
|
67
|
+
|
|
68
|
+
<supporting-info>
|
|
69
|
+
|
|
70
|
+
## What gets blocked
|
|
71
|
+
|
|
72
|
+
- `git push` (all variants including `--force`)
|
|
73
|
+
- `git reset --hard`
|
|
74
|
+
- `git clean -f` / `git clean -fd`
|
|
75
|
+
- `git branch -D`
|
|
76
|
+
- `git checkout .` / `git restore .`
|
|
77
|
+
|
|
78
|
+
When blocked, Claude sees a message telling it that it does not have authority to access these commands.
|
|
79
|
+
|
|
80
|
+
## Primary branch guard
|
|
81
|
+
|
|
82
|
+
Once the dev plugin is enabled (`plugins.dev.enabled: true`), this hook blocks the
|
|
83
|
+
agent from switching the primary checkout's branch (`git switch <branch>`,
|
|
84
|
+
`git checkout <branch>`, `git switch -b <new>`) **unconditionally** — with no
|
|
85
|
+
branch-lock file and no config toggle (ADR 0083 §2, untouchable primary). `git
|
|
86
|
+
commit`, `git worktree add`, read-only git, and registered `.red/tmp/worktrees/<lane>/<slug>` worktrees stay
|
|
87
|
+
allowed; the human terminal is not intercepted. The historical
|
|
88
|
+
`dev.lock.primary-branch` key is retained as read-only legacy history only — it no
|
|
89
|
+
longer enables or disables the guard.
|
|
90
|
+
|
|
91
|
+
## Branch-lock awareness
|
|
92
|
+
|
|
93
|
+
When an opt-in branch lock is active, this hook also blocks the branch-leaving /
|
|
94
|
+
work-loss family in the primary checkout. That behaviour is documented in full at
|
|
95
|
+
[`branch-lock`](../branch-lock/SKILL.md); this hook is self-contained and reaches
|
|
96
|
+
the same verdict without sourcing it.
|
|
97
|
+
|
|
98
|
+
## Tests
|
|
99
|
+
|
|
100
|
+
The full behaviour (dangerous patterns + branch-lock awareness + worktree
|
|
101
|
+
scope + the no-dependency contract) is pinned by
|
|
102
|
+
[scripts/tests/block-dangerous-git.test.sh](scripts/tests/block-dangerous-git.test.sh),
|
|
103
|
+
run directly with `bash`.
|
|
104
|
+
|
|
105
|
+
</supporting-info>
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# block-dangerous-git.sh — git-guardrails PreToolUse(Bash) hook.
|
|
3
|
+
#
|
|
4
|
+
# Two independent layers, both ending in exit 2 (Claude Code's "deny this tool
|
|
5
|
+
# call") with a message on stderr:
|
|
6
|
+
#
|
|
7
|
+
# 1. Always-on dangerous patterns — push, reset --hard, clean -f, branch -D,
|
|
8
|
+
# whole-tree checkout/restore. These fire regardless of any branch lock and
|
|
9
|
+
# are the skill's original purpose.
|
|
10
|
+
#
|
|
11
|
+
# 2. Branch-lock awareness — when a branch lock is active in the primary
|
|
12
|
+
# checkout (an opt-in `.red/tmp/branch-lock.yaml` whose content is the
|
|
13
|
+
# locked branch), the hook ALSO blocks the branch-leaving / work-loss family
|
|
14
|
+
# (switch/checkout to another branch, `switch -`, bare `git stash`). This is
|
|
15
|
+
# a self-contained re-implementation: git-guardrails reads the lock and
|
|
16
|
+
# classifies the command on its own and never sources or requires the
|
|
17
|
+
# branch-lock skill (issue #65 — independent, no dependency). If branch-lock
|
|
18
|
+
# is also installed the two hooks reach the same verdict and stack
|
|
19
|
+
# idempotently — both deny, neither conflicts.
|
|
20
|
+
#
|
|
21
|
+
# The lock layer stays silent (no block) when there is no lock file, and is
|
|
22
|
+
# scope-exempt inside registered worktree lanes under .red/tmp/worktrees/,
|
|
23
|
+
# mirroring the branch-lock hook so the autonomous loop is never strangled.
|
|
24
|
+
#
|
|
25
|
+
# 3. Primary branch guard — when `.red/config.yaml` sets
|
|
26
|
+
# `dev.lock.primary-branch: true`, the hook blocks branch-switching in the
|
|
27
|
+
# primary checkout even without a branch-lock file. This is also
|
|
28
|
+
# self-contained and keeps the same worktree exemption.
|
|
29
|
+
|
|
30
|
+
INPUT=$(cat)
|
|
31
|
+
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command')
|
|
32
|
+
|
|
33
|
+
DANGEROUS_PATTERNS=(
|
|
34
|
+
"git push"
|
|
35
|
+
"git reset --hard"
|
|
36
|
+
"git clean -fd"
|
|
37
|
+
"git clean -f"
|
|
38
|
+
"git branch -D"
|
|
39
|
+
"git checkout \."
|
|
40
|
+
"git restore \."
|
|
41
|
+
"push --force"
|
|
42
|
+
"reset --hard"
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
for pattern in "${DANGEROUS_PATTERNS[@]}"; do
|
|
46
|
+
if echo "$COMMAND" | grep -qE "$pattern"; then
|
|
47
|
+
echo "BLOCKED: '$COMMAND' matches dangerous pattern '$pattern'. The user has prevented you from doing this." >&2
|
|
48
|
+
exit 2
|
|
49
|
+
fi
|
|
50
|
+
done
|
|
51
|
+
|
|
52
|
+
# --- Branch-lock awareness (self-contained; no branch-lock skill dependency) ---
|
|
53
|
+
|
|
54
|
+
[ -z "$COMMAND" ] && exit 0
|
|
55
|
+
|
|
56
|
+
# Project root: prefer the harness-provided dir, fall back to the git toplevel.
|
|
57
|
+
ROOT="${CLAUDE_PROJECT_DIR:-}"
|
|
58
|
+
if [ -z "$ROOT" ]; then
|
|
59
|
+
ROOT=$(git rev-parse --show-toplevel 2>/dev/null || true)
|
|
60
|
+
fi
|
|
61
|
+
[ -z "$ROOT" ] && exit 0
|
|
62
|
+
|
|
63
|
+
# Scope: registered worktree lanes under .red/tmp/worktrees/ are exempt even when locked.
|
|
64
|
+
case "$ROOT" in
|
|
65
|
+
*/.red/tmp/worktrees/*/*) exit 0 ;;
|
|
66
|
+
*/.red/tmp/worktrees/*/*/*) exit 0 ;;
|
|
67
|
+
*/.red/tmp/work-*) exit 0 ;;
|
|
68
|
+
*/.red/tmp/work-*/*) exit 0 ;;
|
|
69
|
+
esac
|
|
70
|
+
|
|
71
|
+
# Runtime reader for the one dev flag ADR 0043 needs. Missing file/key or
|
|
72
|
+
# malformed input is disabled, so the plugin-level hook is dormant by default.
|
|
73
|
+
_dev_lock_primary_branch_enabled() {
|
|
74
|
+
local _file="$1"
|
|
75
|
+
[ -f "$_file" ] || return 1
|
|
76
|
+
|
|
77
|
+
local -a _stack=()
|
|
78
|
+
local -a _indents=()
|
|
79
|
+
local _raw _line _indent_str _indent _rest _key _value _full _i
|
|
80
|
+
|
|
81
|
+
while IFS= read -r _raw || [ -n "$_raw" ]; do
|
|
82
|
+
_raw="${_raw%$'\r'}"
|
|
83
|
+
_line="$_raw"
|
|
84
|
+
if [[ "$_line" != *\"* && "$_line" != *\'* ]]; then
|
|
85
|
+
_line="${_line%%#*}"
|
|
86
|
+
fi
|
|
87
|
+
_line="${_line%"${_line##*[![:space:]]}"}"
|
|
88
|
+
[ -z "${_line//[[:space:]]/}" ] && continue
|
|
89
|
+
|
|
90
|
+
_indent_str="${_line%%[![:space:]]*}"
|
|
91
|
+
_indent=${#_indent_str}
|
|
92
|
+
(( _indent % 2 == 0 )) || return 1
|
|
93
|
+
_rest="${_line:$_indent}"
|
|
94
|
+
|
|
95
|
+
[[ "$_rest" =~ ^[A-Za-z_][A-Za-z0-9_-]*: ]] || return 1
|
|
96
|
+
_key="${_rest%%:*}"
|
|
97
|
+
_value="${_rest#*:}"
|
|
98
|
+
_value="${_value#"${_value%%[![:space:]]*}"}"
|
|
99
|
+
|
|
100
|
+
while ((${#_indents[@]} > 0 && _indents[${#_indents[@]} - 1] >= _indent)); do
|
|
101
|
+
local _last_stack=$(( ${#_stack[@]} - 1 ))
|
|
102
|
+
local _last_indent=$(( ${#_indents[@]} - 1 ))
|
|
103
|
+
unset "_stack[$_last_stack]"
|
|
104
|
+
unset "_indents[$_last_indent]"
|
|
105
|
+
done
|
|
106
|
+
|
|
107
|
+
_full=""
|
|
108
|
+
for ((_i = 0; _i < ${#_stack[@]}; _i++)); do
|
|
109
|
+
[ -n "$_full" ] && _full+="."
|
|
110
|
+
_full+="${_stack[$_i]}"
|
|
111
|
+
done
|
|
112
|
+
[ -n "$_full" ] && _full+="."
|
|
113
|
+
_full+="$_key"
|
|
114
|
+
|
|
115
|
+
if [ -z "$_value" ]; then
|
|
116
|
+
_stack+=("$_key")
|
|
117
|
+
_indents+=("$_indent")
|
|
118
|
+
continue
|
|
119
|
+
fi
|
|
120
|
+
|
|
121
|
+
if [[ "$_value" == \"*\" && "$_value" == *\" ]]; then
|
|
122
|
+
_value="${_value:1:${#_value}-2}"
|
|
123
|
+
elif [[ "$_value" == \'*\' && "$_value" == *\' ]]; then
|
|
124
|
+
_value="${_value:1:${#_value}-2}"
|
|
125
|
+
fi
|
|
126
|
+
|
|
127
|
+
if [ "$_full" = "dev.lock.primary-branch" ] || [ "$_full" = "plugins.dev.lock.primary-branch" ]; then
|
|
128
|
+
[ "$_value" = "true" ] && return 0
|
|
129
|
+
fi
|
|
130
|
+
done < "$_file"
|
|
131
|
+
|
|
132
|
+
return 1
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
_primary_branch_switch_verdict() {
|
|
136
|
+
local _cmd="$1"
|
|
137
|
+
read -ra _pb_toks <<<"$_cmd"
|
|
138
|
+
local _pb_n=${#_pb_toks[@]} _pb_i _pb_j
|
|
139
|
+
for ((_pb_i = 0; _pb_i < _pb_n; _pb_i++)); do
|
|
140
|
+
[ "${_pb_toks[_pb_i]}" = "git" ] || continue
|
|
141
|
+
local _pb_sub="${_pb_toks[_pb_i + 1]:-}"
|
|
142
|
+
case "$_pb_sub" in
|
|
143
|
+
worktree)
|
|
144
|
+
echo "allow"; return 0 ;;
|
|
145
|
+
checkout|switch)
|
|
146
|
+
local _pb_target="" _pb_sawdd=0
|
|
147
|
+
for ((_pb_j = _pb_i + 2; _pb_j < _pb_n; _pb_j++)); do
|
|
148
|
+
local _pb_t="${_pb_toks[_pb_j]}"
|
|
149
|
+
if [ "$_pb_t" = "--" ]; then _pb_sawdd=1; continue; fi
|
|
150
|
+
if [ "$_pb_t" = "-" ]; then _pb_target="-"; break; fi
|
|
151
|
+
case "$_pb_t" in -*) continue ;; esac
|
|
152
|
+
_pb_target="$_pb_t"; break
|
|
153
|
+
done
|
|
154
|
+
[ "$_pb_sawdd" -eq 1 ] && { echo "allow"; return 0; }
|
|
155
|
+
[ -z "$_pb_target" ] && { echo "allow"; return 0; }
|
|
156
|
+
[ "$_pb_target" = "." ] && { echo "allow"; return 0; }
|
|
157
|
+
echo "block"; return 0 ;;
|
|
158
|
+
esac
|
|
159
|
+
done
|
|
160
|
+
echo "allow"; return 0
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if _dev_lock_primary_branch_enabled "$ROOT/.red/config.yaml" &&
|
|
164
|
+
[ "$(_primary_branch_switch_verdict "$COMMAND")" = "block" ]; then
|
|
165
|
+
cat >&2 <<EOF
|
|
166
|
+
BLOCKED by primary branch guard: dev.lock.primary-branch is true.
|
|
167
|
+
The command '$COMMAND' would switch the agent's primary checkout branch.
|
|
168
|
+
|
|
169
|
+
Allowed in the primary checkout: git commit, git worktree add, read-only git,
|
|
170
|
+
and non-branch-changing commands. To work on another branch, create/use a
|
|
171
|
+
worktree under .red/tmp/worktrees/manual/<slug> or ask the user to change the primary branch.
|
|
172
|
+
EOF
|
|
173
|
+
exit 2
|
|
174
|
+
fi
|
|
175
|
+
|
|
176
|
+
# Read the lock target (first line, trailing whitespace stripped). Absent or
|
|
177
|
+
# empty file means unlocked — nothing more to enforce.
|
|
178
|
+
LOCKFILE="$ROOT/.red/tmp/branch-lock.yaml"
|
|
179
|
+
[ -s "$LOCKFILE" ] || exit 0
|
|
180
|
+
IFS= read -r LOCK_BRANCH < "$LOCKFILE"
|
|
181
|
+
LOCK_BRANCH="${LOCK_BRANCH%"${LOCK_BRANCH##*[![:space:]]}"}"
|
|
182
|
+
[ -z "$LOCK_BRANCH" ] && exit 0
|
|
183
|
+
|
|
184
|
+
# Classify the command against the lock. Scans the token stream for a `git` token
|
|
185
|
+
# followed by a recognised subcommand, so compound commands (`cd x && git switch
|
|
186
|
+
# y`) are still caught. Only the branch-leaving / work-loss family blocks; the
|
|
187
|
+
# work-loss members already caught by the dangerous patterns above are harmless
|
|
188
|
+
# duplicates here.
|
|
189
|
+
read -ra _toks <<<"$COMMAND"
|
|
190
|
+
_n=${#_toks[@]}
|
|
191
|
+
_verdict="allow"
|
|
192
|
+
for ((_i = 0; _i < _n; _i++)); do
|
|
193
|
+
[ "${_toks[_i]}" = "git" ] || continue
|
|
194
|
+
_sub="${_toks[_i + 1]:-}"
|
|
195
|
+
case "$_sub" in
|
|
196
|
+
worktree)
|
|
197
|
+
_verdict="allow"; break ;;
|
|
198
|
+
stash)
|
|
199
|
+
_op="${_toks[_i + 2]:-}"
|
|
200
|
+
if [ -z "$_op" ] || [ "$_op" = "push" ] || [ "$_op" = "save" ]; then
|
|
201
|
+
_verdict="block"
|
|
202
|
+
fi
|
|
203
|
+
break ;;
|
|
204
|
+
checkout|switch)
|
|
205
|
+
_target=""; _sawdd=0
|
|
206
|
+
for ((_j = _i + 2; _j < _n; _j++)); do
|
|
207
|
+
_t="${_toks[_j]}"
|
|
208
|
+
if [ "$_t" = "--" ]; then _sawdd=1; continue; fi
|
|
209
|
+
if [ "$_t" = "-" ]; then _target="-"; break; fi # `switch -` = previous branch
|
|
210
|
+
case "$_t" in -*) continue ;; esac # skip flags (-b, -c, --track, …)
|
|
211
|
+
_target="$_t"; break
|
|
212
|
+
done
|
|
213
|
+
if [ "$_target" = "." ]; then _verdict="block"; break; fi # whole-tree restore (already a pattern)
|
|
214
|
+
[ "$_sawdd" -eq 1 ] && break # file restore: allow
|
|
215
|
+
[ -z "$_target" ] && break # bare checkout: allow
|
|
216
|
+
[ "$_target" = "$LOCK_BRANCH" ] && break # back to the lock target: allow
|
|
217
|
+
_verdict="block"; break ;;
|
|
218
|
+
esac
|
|
219
|
+
done
|
|
220
|
+
|
|
221
|
+
if [ "$_verdict" = "block" ]; then
|
|
222
|
+
cat >&2 <<EOF
|
|
223
|
+
BLOCKED by branch lock: this session is locked to '$LOCK_BRANCH'.
|
|
224
|
+
The command '$COMMAND' would switch the agent away from the locked branch or
|
|
225
|
+
shelve the working tree. To change or release the lock, ask the user — they
|
|
226
|
+
drive it with '/branch-lock <branch>' or '/branch-lock clear'.
|
|
227
|
+
EOF
|
|
228
|
+
exit 2
|
|
229
|
+
fi
|
|
230
|
+
|
|
231
|
+
exit 0
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Unit test for block-dangerous-git.sh — the git-guardrails PreToolUse(Bash) hook
|
|
3
|
+
# (issue #65: make git-guardrails lock-aware, independent, no dependency).
|
|
4
|
+
#
|
|
5
|
+
# Two responsibilities, exercised end-to-end against throwaway repos:
|
|
6
|
+
# 1. The always-on dangerous-pattern block (push, reset --hard, …) is
|
|
7
|
+
# unchanged and lock-independent.
|
|
8
|
+
# 2. When a branch lock is active in the primary checkout, the hook also blocks
|
|
9
|
+
# the branch-leaving / work-loss family — the same verdicts the branch-lock
|
|
10
|
+
# hook reaches — so git-guardrails alone enforces the lock (AC1). With both
|
|
11
|
+
# hooks installed the overlap is idempotent: both deny, never conflict (AC2).
|
|
12
|
+
# And the script depends on nothing from the branch-lock skill (AC3).
|
|
13
|
+
|
|
14
|
+
set -uo pipefail
|
|
15
|
+
|
|
16
|
+
HERE="$(cd "$(dirname "$0")" && pwd)"
|
|
17
|
+
SCRIPTS="$(dirname "$HERE")"
|
|
18
|
+
HOOK="$SCRIPTS/block-dangerous-git.sh"
|
|
19
|
+
|
|
20
|
+
pass=0
|
|
21
|
+
fail=0
|
|
22
|
+
|
|
23
|
+
expect_eq() {
|
|
24
|
+
local label="$1" expected="$2" actual="$3"
|
|
25
|
+
if [[ "$expected" == "$actual" ]]; then
|
|
26
|
+
echo "PASS $label"; pass=$((pass + 1))
|
|
27
|
+
else
|
|
28
|
+
printf 'FAIL %s\n expected: %q\n actual: %q\n' "$label" "$expected" "$actual"
|
|
29
|
+
fail=$((fail + 1))
|
|
30
|
+
fi
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
# A throwaway repo, optionally locked to <branch> via the gitignored lockfile.
|
|
34
|
+
mk_repo() {
|
|
35
|
+
local dir="$1" lock="${2:-}"
|
|
36
|
+
git init -q -b main "$dir"
|
|
37
|
+
git -C "$dir" config user.email t@t.t
|
|
38
|
+
git -C "$dir" config user.name t
|
|
39
|
+
git -C "$dir" commit -q --allow-empty -m init
|
|
40
|
+
git -C "$dir" branch feature
|
|
41
|
+
if [[ -n "$lock" ]]; then
|
|
42
|
+
mkdir -p "$dir/.red/tmp"
|
|
43
|
+
printf '%s\n' "$lock" > "$dir/.red/tmp/branch-lock.yaml"
|
|
44
|
+
fi
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
# Run the hook against a Bash command with the project root pinned; echo exit code.
|
|
48
|
+
run_hook() {
|
|
49
|
+
local root="$1" cmd="$2"
|
|
50
|
+
( cd "$root" && CLAUDE_PROJECT_DIR="$root" bash "$HOOK" \
|
|
51
|
+
<<<"{\"tool_input\":{\"command\":\"$cmd\"}}" ) >/dev/null 2>&1
|
|
52
|
+
echo $?
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
# ===========================================================================
|
|
56
|
+
# Always-on dangerous patterns — unchanged, lock-independent.
|
|
57
|
+
# ===========================================================================
|
|
58
|
+
RU="$(mktemp -d)/red-skills"; mk_repo "$RU" # unlocked
|
|
59
|
+
expect_eq "guardrail/push blocked (unlocked)" "2" "$(run_hook "$RU" "git push origin main")"
|
|
60
|
+
expect_eq "guardrail/reset --hard blocked" "2" "$(run_hook "$RU" "git reset --hard")"
|
|
61
|
+
expect_eq "guardrail/clean -fd blocked" "2" "$(run_hook "$RU" "git clean -fd")"
|
|
62
|
+
expect_eq "guardrail/branch -D blocked" "2" "$(run_hook "$RU" "git branch -D feature")"
|
|
63
|
+
expect_eq "guardrail/checkout dot blocked" "2" "$(run_hook "$RU" "git checkout .")"
|
|
64
|
+
expect_eq "guardrail/status allowed" "0" "$(run_hook "$RU" "git status")"
|
|
65
|
+
# Branch switching is NOT a guardrail pattern: allowed when no lock is active.
|
|
66
|
+
expect_eq "no-lock/switch other allowed" "0" "$(run_hook "$RU" "git switch feature")"
|
|
67
|
+
expect_eq "no-lock/checkout -b allowed" "0" "$(run_hook "$RU" "git checkout -b shiny")"
|
|
68
|
+
expect_eq "no-lock/stash allowed" "0" "$(run_hook "$RU" "git stash")"
|
|
69
|
+
|
|
70
|
+
# ===========================================================================
|
|
71
|
+
# Primary branch guard — config flag on, no branch-lock file required.
|
|
72
|
+
# ===========================================================================
|
|
73
|
+
mkdir -p "$RU/.red"
|
|
74
|
+
cat > "$RU/.red/config.yaml" <<'EOF'
|
|
75
|
+
dev:
|
|
76
|
+
lock:
|
|
77
|
+
primary-branch: true
|
|
78
|
+
EOF
|
|
79
|
+
expect_eq "primary-guard/switch other blocked" "2" "$(run_hook "$RU" "git switch feature")"
|
|
80
|
+
expect_eq "primary-guard/checkout other blocked" "2" "$(run_hook "$RU" "git checkout feature")"
|
|
81
|
+
expect_eq "primary-guard/switch -b blocked" "2" "$(run_hook "$RU" "git switch -b shiny")"
|
|
82
|
+
expect_eq "primary-guard/commit allowed" "0" "$(run_hook "$RU" "git commit -m wip")"
|
|
83
|
+
expect_eq "primary-guard/worktree add allowed" "0" "$(run_hook "$RU" "git worktree add ../wt feature")"
|
|
84
|
+
expect_eq "primary-guard/status allowed" "0" "$(run_hook "$RU" "git status")"
|
|
85
|
+
|
|
86
|
+
cat > "$RU/.red/config.yaml" <<'EOF'
|
|
87
|
+
dev:
|
|
88
|
+
lock:
|
|
89
|
+
primary-branch: false
|
|
90
|
+
EOF
|
|
91
|
+
expect_eq "primary-guard/flag off allows switch" "0" "$(run_hook "$RU" "git switch feature")"
|
|
92
|
+
|
|
93
|
+
# ===========================================================================
|
|
94
|
+
# AC1 — lock active in the primary checkout: branch-leaving / work-loss blocked.
|
|
95
|
+
# ===========================================================================
|
|
96
|
+
RL="$(mktemp -d)/red-skills"; mk_repo "$RL" main # locked to main
|
|
97
|
+
expect_eq "AC1/switch other blocked" "2" "$(run_hook "$RL" "git switch feature")"
|
|
98
|
+
expect_eq "AC1/checkout other blocked" "2" "$(run_hook "$RL" "git checkout feature")"
|
|
99
|
+
expect_eq "AC1/checkout -b new blocked" "2" "$(run_hook "$RL" "git checkout -b shiny")"
|
|
100
|
+
expect_eq "AC1/switch - blocked" "2" "$(run_hook "$RL" "git switch -")"
|
|
101
|
+
expect_eq "AC1/stash blocked" "2" "$(run_hook "$RL" "git stash")"
|
|
102
|
+
# Same-branch / legitimate ops still allowed while locked.
|
|
103
|
+
expect_eq "AC1/switch back to lock allowed" "0" "$(run_hook "$RL" "git switch main")"
|
|
104
|
+
expect_eq "AC1/checkout back to lock allowed" "0" "$(run_hook "$RL" "git checkout main")"
|
|
105
|
+
expect_eq "AC1/file restore allowed" "0" "$(run_hook "$RL" "git checkout -- src/app.ts")"
|
|
106
|
+
expect_eq "AC1/status allowed while locked" "0" "$(run_hook "$RL" "git status")"
|
|
107
|
+
expect_eq "AC1/worktree add allowed" "0" "$(run_hook "$RL" "git worktree add ../wt feature")"
|
|
108
|
+
# The guardrail patterns still fire while locked (idempotent with the work-loss family).
|
|
109
|
+
expect_eq "AC1/push blocked while locked" "2" "$(run_hook "$RL" "git push")"
|
|
110
|
+
expect_eq "AC1/reset --hard blocked while locked" "2" "$(run_hook "$RL" "git reset --hard")"
|
|
111
|
+
|
|
112
|
+
# ===========================================================================
|
|
113
|
+
# Scope — registered worktree lanes under .red/tmp/worktrees/ are exempt even with a lock.
|
|
114
|
+
# ===========================================================================
|
|
115
|
+
WT="$(mktemp -d)/red-skills/.red/tmp/worktrees/manual/fix-docs"
|
|
116
|
+
mkdir -p "$(dirname "$WT")"
|
|
117
|
+
mk_repo "$WT" main # lockfile present, but it's a worktree path
|
|
118
|
+
expect_eq "scope/worktree switch other allowed" "0" "$(run_hook "$WT" "git switch feature")"
|
|
119
|
+
mkdir -p "$WT/.red"
|
|
120
|
+
cat > "$WT/.red/config.yaml" <<'EOF'
|
|
121
|
+
dev:
|
|
122
|
+
lock:
|
|
123
|
+
primary-branch: true
|
|
124
|
+
EOF
|
|
125
|
+
expect_eq "scope/worktree primary guard allowed" "0" "$(run_hook "$WT" "git switch feature")"
|
|
126
|
+
# …but a worktree's own dangerous patterns still fire (guardrail is scope-independent).
|
|
127
|
+
expect_eq "scope/worktree push still blocked" "2" "$(run_hook "$WT" "git push")"
|
|
128
|
+
|
|
129
|
+
# ===========================================================================
|
|
130
|
+
# AC3 — no dependency on the branch-lock skill: the script must not source or
|
|
131
|
+
# require any of its files. (Naming the skill in a comment is fine — what would
|
|
132
|
+
# create a dependency is sourcing/dot-including its modules or reaching into its
|
|
133
|
+
# directory.)
|
|
134
|
+
# ===========================================================================
|
|
135
|
+
if grep -qE '(source|^\s*\.)[[:space:]].*(lock-store|scope-resolver|git-command-classifier|branch-lock/)' "$HOOK"; then
|
|
136
|
+
echo "FAIL AC3/no branch-lock dependency (sources a branch-lock module)"; fail=$((fail + 1))
|
|
137
|
+
else
|
|
138
|
+
echo "PASS AC3/no branch-lock dependency"; pass=$((pass + 1))
|
|
139
|
+
fi
|
|
140
|
+
|
|
141
|
+
echo
|
|
142
|
+
echo "summary: $pass passed, $fail failed"
|
|
143
|
+
[ "$fail" -eq 0 ]
|