@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.
Files changed (135) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +30 -0
  3. package/package.json +32 -0
  4. package/skills/engineering/README.md +36 -0
  5. package/skills/engineering/_report-runtime/WRAPPER.md +47 -0
  6. package/skills/engineering/afk/AGENT-PROMPT.md +295 -0
  7. package/skills/engineering/afk/SAFETY.md +103 -0
  8. package/skills/engineering/afk/SKILL.md +160 -0
  9. package/skills/engineering/afk/TROUBLESHOOTING.md +260 -0
  10. package/skills/engineering/afk/actions-lane.md +213 -0
  11. package/skills/engineering/afk/bin/README.md +29 -0
  12. package/skills/engineering/afk/bin/afk.mjs +51 -0
  13. package/skills/engineering/afk/defaults/cargo-pre-worktree.sh +37 -0
  14. package/skills/engineering/afk/defaults/envelope-post-attempt.sh +47 -0
  15. package/skills/engineering/afk/defaults/gradle-pre-worktree.sh +42 -0
  16. package/skills/engineering/afk/defaults/heartbeat-post-attempt.sh +43 -0
  17. package/skills/engineering/afk/defaults/validation-post-merge.sh +77 -0
  18. package/skills/engineering/afk/detectors/README.md +72 -0
  19. package/skills/engineering/afk/detectors/cargo.sh +27 -0
  20. package/skills/engineering/afk/detectors/gradle.sh +35 -0
  21. package/skills/engineering/afk/docs/BOOT-SWEEPS.md +81 -0
  22. package/skills/engineering/afk/docs/CONFIG.md +238 -0
  23. package/skills/engineering/afk/docs/ENVELOPE.md +177 -0
  24. package/skills/engineering/afk/docs/HANDOFF.md +70 -0
  25. package/skills/engineering/afk/docs/LIVENESS.md +56 -0
  26. package/skills/engineering/afk/docs/OPERATIONS.md +409 -0
  27. package/skills/engineering/afk/examples/only-mine.sh +32 -0
  28. package/skills/engineering/afk/examples/red-afk-attempt-action.yml +53 -0
  29. package/skills/engineering/afk/examples/rs-afk-attempt.yml +76 -0
  30. package/skills/engineering/afk/fleet.md +127 -0
  31. package/skills/engineering/afk/hooks/red-cargo +44 -0
  32. package/skills/engineering/afk/hooks/red-envelope +50 -0
  33. package/skills/engineering/afk/hooks/red-gradle +50 -0
  34. package/skills/engineering/afk/hooks/red-heartbeat +46 -0
  35. package/skills/engineering/afk/hooks/red-validation +79 -0
  36. package/skills/engineering/afk/monitor.md +170 -0
  37. package/skills/engineering/afk/runner-claude-minimax.md +71 -0
  38. package/skills/engineering/afk/runner-claude.md +67 -0
  39. package/skills/engineering/afk/runner-codex.md +138 -0
  40. package/skills/engineering/afk/runner-hermes.md +40 -0
  41. package/skills/engineering/afk/runner-opencode.md +142 -0
  42. package/skills/engineering/ask-red/SKILL.md +137 -0
  43. package/skills/engineering/audit-skills/SKILL.md +46 -0
  44. package/skills/engineering/code-review/SKILL.md +106 -0
  45. package/skills/engineering/context/SKILL.md +142 -0
  46. package/skills/engineering/curate/SKILL.md +133 -0
  47. package/skills/engineering/daily-review/SKILL.md +68 -0
  48. package/skills/engineering/dashboard/SKILL.md +44 -0
  49. package/skills/engineering/diagnose/SKILL.md +155 -0
  50. package/skills/engineering/diagnose/scripts/hitl-loop.template.sh +41 -0
  51. package/skills/engineering/go/SKILL.md +144 -0
  52. package/skills/engineering/go/TROUBLESHOOTING.md +81 -0
  53. package/skills/engineering/ground-truth/SKILL.md +84 -0
  54. package/skills/engineering/hitl/SKILL.md +171 -0
  55. package/skills/engineering/hitl/TROUBLESHOOTING.md +76 -0
  56. package/skills/engineering/implement/SKILL.md +69 -0
  57. package/skills/engineering/improve-codebase-architecture/DEEPENING.md +37 -0
  58. package/skills/engineering/improve-codebase-architecture/INTERFACE-DESIGN.md +44 -0
  59. package/skills/engineering/improve-codebase-architecture/LANGUAGE.md +53 -0
  60. package/skills/engineering/improve-codebase-architecture/SKILL.md +81 -0
  61. package/skills/engineering/model-tier-policy/SKILL.md +83 -0
  62. package/skills/engineering/prototype/LOGIC.md +79 -0
  63. package/skills/engineering/prototype/SKILL.md +30 -0
  64. package/skills/engineering/prototype/UI.md +112 -0
  65. package/skills/engineering/red-doctor/APPLY.md +35 -0
  66. package/skills/engineering/red-doctor/SKILL.md +172 -0
  67. package/skills/engineering/red-gains/SKILL.md +49 -0
  68. package/skills/engineering/red-setup/INTERVIEW.md +266 -0
  69. package/skills/engineering/red-setup/ISSUE-SWEEP.md +18 -0
  70. package/skills/engineering/red-setup/REFERENCE.md +35 -0
  71. package/skills/engineering/red-setup/SKILL.md +58 -0
  72. package/skills/engineering/red-setup/WORKFLOWS.md +74 -0
  73. package/skills/engineering/red-setup/WRITE-CONTRACT.md +151 -0
  74. package/skills/engineering/red-setup/config-template.yaml +174 -0
  75. package/skills/engineering/red-setup/domain.md +52 -0
  76. package/skills/engineering/red-setup/issue-tracker-github.md +88 -0
  77. package/skills/engineering/red-setup/rsp-retirement-runbook.md +35 -0
  78. package/skills/engineering/red-setup/scripts/install-runtime-shim.sh +189 -0
  79. package/skills/engineering/red-setup/triage-labels.md +176 -0
  80. package/skills/engineering/red-setup/workflows/red-issues-needs-triage.yml +36 -0
  81. package/skills/engineering/red-statusline/HOST-NOTES.md +180 -0
  82. package/skills/engineering/red-statusline/SKILL.md +58 -0
  83. package/skills/engineering/report-bug/SKILL.md +155 -0
  84. package/skills/engineering/resolving-merge-conflicts/SKILL.md +143 -0
  85. package/skills/engineering/retake/SKILL.md +184 -0
  86. package/skills/engineering/review-adrs/SKILL.md +125 -0
  87. package/skills/engineering/start/ADR-FORMAT.md +47 -0
  88. package/skills/engineering/start/CONTEXT-FORMAT.md +77 -0
  89. package/skills/engineering/start/DOC-LANDING-FINALIZER.md +9 -0
  90. package/skills/engineering/start/SKILL.md +179 -0
  91. package/skills/engineering/tdd/SKILL.md +96 -0
  92. package/skills/engineering/tdd/deep-modules.md +33 -0
  93. package/skills/engineering/tdd/interface-design.md +31 -0
  94. package/skills/engineering/tdd/mocking.md +59 -0
  95. package/skills/engineering/tdd/tests.md +85 -0
  96. package/skills/engineering/to-spec/SKILL.md +104 -0
  97. package/skills/engineering/to-tickets/SKILL.md +158 -0
  98. package/skills/engineering/triage/AGENT-BRIEF.md +207 -0
  99. package/skills/engineering/triage/OUT-OF-SCOPE.md +101 -0
  100. package/skills/engineering/triage/SKILL.md +176 -0
  101. package/skills/engineering/verify/SKILL.md +136 -0
  102. package/skills/engineering/wayfinder/SKILL.md +128 -0
  103. package/skills/engineering/zoom-out/SKILL.md +51 -0
  104. package/skills/engineering/zoom-out/scripts/graph-mode-plumbing.sh +36 -0
  105. package/skills/engineering/zoom-out/scripts/tests/contract.test.sh +168 -0
  106. package/skills/knowledge/README.md +5 -0
  107. package/skills/knowledge/research/SKILL.md +76 -0
  108. package/skills/misc/README.md +8 -0
  109. package/skills/misc/branch-lock/SKILL.md +187 -0
  110. package/skills/misc/branch-lock/scripts/branch-lock-hook.sh +113 -0
  111. package/skills/misc/branch-lock/scripts/branch-lock-session-start.sh +81 -0
  112. package/skills/misc/branch-lock/scripts/branch-lock.sh +68 -0
  113. package/skills/misc/branch-lock/scripts/lib/dev-config.sh +126 -0
  114. package/skills/misc/branch-lock/scripts/lib/git-command-classifier.sh +198 -0
  115. package/skills/misc/branch-lock/scripts/lib/lock-store.sh +67 -0
  116. package/skills/misc/branch-lock/scripts/lib/scope-resolver.sh +43 -0
  117. package/skills/misc/branch-lock/scripts/tests/branch-lock-cli.test.sh +114 -0
  118. package/skills/misc/branch-lock/scripts/tests/claude-plugin-hook.test.sh +117 -0
  119. package/skills/misc/branch-lock/scripts/tests/codex-hook.test.sh +229 -0
  120. package/skills/misc/branch-lock/scripts/tests/dev-config.test.sh +99 -0
  121. package/skills/misc/branch-lock/scripts/tests/git-command-classifier.test.sh +187 -0
  122. package/skills/misc/branch-lock/scripts/tests/lock-store.test.sh +90 -0
  123. package/skills/misc/branch-lock/scripts/tests/scope-resolver.test.sh +63 -0
  124. package/skills/misc/branch-lock/scripts/tests/session-start.test.sh +123 -0
  125. package/skills/misc/git-guardrails-claude-code/SKILL.md +105 -0
  126. package/skills/misc/git-guardrails-claude-code/scripts/block-dangerous-git.sh +231 -0
  127. package/skills/misc/git-guardrails-claude-code/scripts/tests/block-dangerous-git.test.sh +143 -0
  128. package/skills/misc/migrate-to-shoehorn/SKILL.md +96 -0
  129. package/skills/misc/setup-pre-commit/SKILL.md +94 -0
  130. package/skills/productivity/README.md +8 -0
  131. package/skills/productivity/ff/SKILL.md +116 -0
  132. package/skills/productivity/handoff/SKILL.md +37 -0
  133. package/skills/productivity/reflect/SKILL.md +18 -0
  134. package/skills/productivity/write-a-skill/SKILL.md +190 -0
  135. package/skills/productivity/write-a-skill/WRITING-STYLE.md +62 -0
@@ -0,0 +1,229 @@
1
+ #!/usr/bin/env bash
2
+ # Unit test for the Codex plugin PreToolUse branch-lock hook.
3
+
4
+ set -uo pipefail
5
+
6
+ HERE="$(cd "$(dirname "$0")" && pwd)"
7
+ PLUGIN_ROOT="$(cd "$HERE/../../../../.." && pwd)"
8
+ HOOK="$PLUGIN_ROOT/hooks/branch-lock-codex.sh"
9
+ MANIFEST="$PLUGIN_ROOT/hooks/codex.hooks.json"
10
+
11
+ pass=0
12
+ fail=0
13
+
14
+ ok() { echo "PASS $1"; pass=$((pass + 1)); }
15
+ bad() { echo "FAIL $1"; fail=$((fail + 1)); }
16
+
17
+ expect_eq() {
18
+ local label="$1" expected="$2" actual="$3"
19
+ if [[ "$expected" == "$actual" ]]; then
20
+ ok "$label"
21
+ else
22
+ bad "$label"
23
+ printf ' expected: %q\n actual: %q\n' "$expected" "$actual"
24
+ fi
25
+ }
26
+
27
+ expect_contains() {
28
+ local label="$1" needle="$2" haystack="$3"
29
+ if [[ "$haystack" == *"$needle"* ]]; then
30
+ ok "$label"
31
+ else
32
+ bad "$label"
33
+ printf ' missing: %q\n in: %q\n' "$needle" "$haystack"
34
+ fi
35
+ }
36
+
37
+ tmp="$(mktemp -d -t branch-lock-codex.XXXXXX)"
38
+ trap 'rm -rf "$tmp"' EXIT
39
+
40
+ primary="$tmp/red-skills"
41
+ mkdir -p "$primary/.red/tmp"
42
+ cat > "$primary/.red/config.yaml" <<'EOF'
43
+ plugins:
44
+ dev:
45
+ enabled: true
46
+ EOF
47
+ printf 'main\n' > "$primary/.red/tmp/branch-lock.yaml"
48
+
49
+ run_hook() {
50
+ local root="$1" payload="$2" out err rc
51
+ out="$tmp/out"
52
+ err="$tmp/err"
53
+ CODEX_PLUGIN_ROOT="$PLUGIN_ROOT" "$HOOK" >"$out" 2>"$err" <<<"$payload"
54
+ rc=$?
55
+ printf '%s\n---stdout---\n%s\n---stderr---\n%s\n' "$rc" "$(<"$out")" "$(<"$err")"
56
+ }
57
+
58
+ payload() {
59
+ local root="$1" cmd="$2"
60
+ jq -nc --arg cwd "$root" --arg cmd "$cmd" \
61
+ '{hook_event_name:"PreToolUse", cwd:$cwd, tool_input:{cmd:$cmd}}'
62
+ }
63
+
64
+ manifest_hook="$(jq -r '.hooks.PreToolUse[0].hooks[] | select(.command | contains("branch-lock-codex.sh")) | .command' "$MANIFEST")"
65
+ expect_contains "manifest: wires branch-lock-codex.sh" "branch-lock-codex.sh" "$manifest_hook"
66
+ expect_contains "manifest: wrapper drains stdin before hook" 'cat >"$tmp"' "$manifest_hook"
67
+
68
+ out="$tmp/manifest-out"
69
+ err="$tmp/manifest-err"
70
+ CODEX_PLUGIN_ROOT="$PLUGIN_ROOT" bash -lc "$manifest_hook" >"$out" 2>"$err" \
71
+ <<<"$(payload "$primary" "git status")"
72
+ rc=$?
73
+ expect_eq "manifest: command executes through shell" "0" "$rc"
74
+ expect_eq "manifest: command prints empty JSON" "{}" "$(<"$out")"
75
+
76
+ missing_root="$tmp/missing-plugin-root"
77
+ mkdir -p "$missing_root"
78
+ CODEX_PLUGIN_ROOT="$missing_root" bash -lc "$manifest_hook" >"$out" 2>"$err" \
79
+ <<<"$(payload "$primary" "git switch main")"
80
+ rc=$?
81
+ expect_eq "manifest: missing hook fails open" "0" "$rc"
82
+ expect_eq "manifest: missing hook prints empty JSON" "{}" "$(<"$out")"
83
+
84
+ early_root="$tmp/early-plugin-root"
85
+ mkdir -p "$early_root/hooks"
86
+ cat > "$early_root/hooks/branch-lock-codex.sh" <<'EOF'
87
+ #!/usr/bin/env sh
88
+ printf '{}'
89
+ exit 0
90
+ EOF
91
+ chmod +x "$early_root/hooks/branch-lock-codex.sh"
92
+ set -o pipefail
93
+ (head -c 1048576 /dev/zero | tr '\0' x) \
94
+ | CODEX_PLUGIN_ROOT="$early_root" bash -lc "$manifest_hook" >"$out" 2>"$err"
95
+ rc=$?
96
+ set +o pipefail
97
+ expect_eq "manifest: early hook still drains Codex stdin" "0" "$rc"
98
+ expect_eq "manifest: early hook prints empty JSON" "{}" "$(<"$out")"
99
+
100
+ CODEX_PLUGIN_ROOT="$missing_root" "$HOOK" >"$out" 2>"$err" \
101
+ <<<"$(payload "$primary" "git switch main")"
102
+ rc=$?
103
+ expect_eq "hook: incomplete plugin root fails open" "0" "$rc"
104
+ expect_eq "hook: incomplete plugin root prints empty JSON" "{}" "$(<"$out")"
105
+
106
+ # Branch movement in the primary is now blocked unconditionally by the
107
+ # untouchable-primary rule (ADR 0083) — it fires before the branch-lock path, so
108
+ # switching away, and even switching *back* to the locked branch, is refused.
109
+ result="$(run_hook "$primary" "$(payload "$primary" "git switch feature")")"
110
+ rc="$(sed -n '1p' <<<"$result")"
111
+ stderr="$(sed -n '/---stderr---/,$p' <<<"$result")"
112
+ expect_eq "locked: switch away is blocked" "0" "$rc"
113
+ expect_contains "locked: error names ADR 0083" "ADR 0083" "$stderr"
114
+
115
+ result="$(run_hook "$primary" "$(payload "$primary" "git switch main")")"
116
+ rc="$(sed -n '1p' <<<"$result")"
117
+ stderr="$(sed -n '/---stderr---/,$p' <<<"$result")"
118
+ expect_eq "locked: switch back is also blocked (untouchable primary)" "0" "$rc"
119
+ expect_contains "locked: switch-back error names ADR 0083" "ADR 0083" "$stderr"
120
+
121
+ # Work-loss commands are caught by the unconditional untouchable-primary guard
122
+ # (#1024 reset/stash family + #1025 unconditional structure) even while a lock
123
+ # is active — the guard runs before the lock read, so the refusal names
124
+ # ADR 0083, not the branch lock.
125
+ result="$(run_hook "$primary" "$(payload "$primary" "git reset --hard HEAD~1")")"
126
+ rc="$(sed -n '1p' <<<"$result")"
127
+ stderr="$(sed -n '/---stderr---/,$p' <<<"$result")"
128
+ expect_eq "locked: work-loss reset --hard is blocked" "0" "$rc"
129
+ expect_contains "locked: work-loss error names untouchable primary" "ADR 0083" "$stderr"
130
+
131
+ rm -f "$primary/.red/tmp/branch-lock.yaml"
132
+ result="$(run_hook "$primary" "$(payload "$primary" "git switch feature")")"
133
+ rc="$(sed -n '1p' <<<"$result")"
134
+ stderr="$(sed -n '/---stderr---/,$p' <<<"$result")"
135
+ expect_eq "unlocked: switch is still blocked (untouchable primary)" "0" "$rc"
136
+ expect_contains "unlocked: error names ADR 0083" "ADR 0083" "$stderr"
137
+
138
+ # With no lock file the untouchable-primary guard still blocks every branch
139
+ # move, regardless of the legacy `dev.lock.primary-branch` toggle value.
140
+ mkdir -p "$primary/.red"
141
+ cat > "$primary/.red/config.yaml" <<'EOF'
142
+ plugins:
143
+ dev:
144
+ enabled: true
145
+ EOF
146
+
147
+ result="$(run_hook "$primary" "$(payload "$primary" "git switch feature")")"
148
+ rc="$(sed -n '1p' <<<"$result")"
149
+ stderr="$(sed -n '/---stderr---/,$p' <<<"$result")"
150
+ expect_eq "primary guard: switch blocked with no lock and no config" "0" "$rc"
151
+ expect_contains "primary guard: error names ADR 0083" "ADR 0083" "$stderr"
152
+
153
+ result="$(run_hook "$primary" "$(payload "$primary" "git checkout feature")")"
154
+ rc="$(sed -n '1p' <<<"$result")"
155
+ expect_eq "primary guard: checkout branch is blocked" "0" "$rc"
156
+
157
+ # issue #1024 — git reset (any form) and git stash (all subcommands) blocked,
158
+ # and the refusal explains the reason + the sanctioned worktree alternative.
159
+ result="$(run_hook "$primary" "$(payload "$primary" "git reset --hard")")"
160
+ rc="$(sed -n '1p' <<<"$result")"
161
+ stderr="$(sed -n '/---stderr---/,$p' <<<"$result")"
162
+ expect_eq "primary guard: reset --hard is blocked when flag is on" "0" "$rc"
163
+ expect_contains "primary guard: reset refusal points to worktree" ".red/tmp/work-" "$stderr"
164
+
165
+ result="$(run_hook "$primary" "$(payload "$primary" "git reset --soft HEAD~1")")"
166
+ rc="$(sed -n '1p' <<<"$result")"
167
+ expect_eq "primary guard: reset --soft is blocked when flag is on" "0" "$rc"
168
+
169
+ result="$(run_hook "$primary" "$(payload "$primary" "git stash")")"
170
+ rc="$(sed -n '1p' <<<"$result")"
171
+ expect_eq "primary guard: stash is blocked when flag is on" "0" "$rc"
172
+
173
+ result="$(run_hook "$primary" "$(payload "$primary" "git rebase --autostash main")")"
174
+ rc="$(sed -n '1p' <<<"$result")"
175
+ expect_eq "primary guard: rebase --autostash is blocked when flag is on" "0" "$rc"
176
+
177
+ result="$(run_hook "$primary" "$(payload "$primary" "git switch -b new")")"
178
+ rc="$(sed -n '1p' <<<"$result")"
179
+ expect_eq "primary guard: switch -b is blocked" "0" "$rc"
180
+
181
+ result="$(run_hook "$primary" "$(payload "$primary" "git commit -m wip")")"
182
+ rc="$(sed -n '1p' <<<"$result")"
183
+ expect_eq "primary guard: commit is allowed" "0" "$rc"
184
+
185
+ result="$(run_hook "$primary" "$(payload "$primary" "git worktree add ../wt feature")")"
186
+ rc="$(sed -n '1p' <<<"$result")"
187
+ expect_eq "primary guard: worktree add is allowed" "0" "$rc"
188
+
189
+ result="$(run_hook "$primary" "$(payload "$primary" "git status")")"
190
+ rc="$(sed -n '1p' <<<"$result")"
191
+ expect_eq "primary guard: read-only git is allowed" "0" "$rc"
192
+
193
+ # The legacy toggle is still readable but can no longer *enable* switching:
194
+ # explicitly false must not re-open primary branch movement (ADR 0083 §2).
195
+ cat > "$primary/.red/config.yaml" <<'EOF'
196
+ plugins:
197
+ dev:
198
+ enabled: true
199
+ lock:
200
+ primary-branch: false
201
+ EOF
202
+ result="$(run_hook "$primary" "$(payload "$primary" "git switch feature")")"
203
+ rc="$(sed -n '1p' <<<"$result")"
204
+ expect_eq "primary guard: legacy toggle off still blocks switch" "0" "$rc"
205
+
206
+ worktree="$primary/.red/tmp/work-wAAAA-i1/worktree"
207
+ mkdir -p "$worktree" "$primary/.red/tmp"
208
+ printf 'main\n' > "$primary/.red/tmp/branch-lock.yaml"
209
+ result="$(run_hook "$worktree" "$(payload "$worktree" "git switch feature")")"
210
+ rc="$(sed -n '1p' <<<"$result")"
211
+ expect_eq "worktree: branch lock is exempt" "0" "$rc"
212
+
213
+ mkdir -p "$worktree/.red"
214
+ cat > "$worktree/.red/config.yaml" <<'EOF'
215
+ dev:
216
+ lock:
217
+ primary-branch: true
218
+ EOF
219
+ result="$(run_hook "$worktree" "$(payload "$worktree" "git switch feature")")"
220
+ rc="$(sed -n '1p' <<<"$result")"
221
+ expect_eq "worktree: primary guard is exempt" "0" "$rc"
222
+
223
+ result="$(run_hook "$primary" "$(jq -nc --arg cwd "$primary" '{hook_event_name:"PreToolUse", cwd:$cwd, tool_input:{file:"x"}}')")"
224
+ rc="$(sed -n '1p' <<<"$result")"
225
+ expect_eq "non-shell payload: no-op allowed" "0" "$rc"
226
+
227
+ echo
228
+ echo "summary: $pass passed, $fail failed"
229
+ [[ "$fail" -eq 0 ]]
@@ -0,0 +1,99 @@
1
+ #!/usr/bin/env bash
2
+ # Unit test for lib/dev-config.sh — runtime reader for dev.lock.primary-branch.
3
+
4
+ set -uo pipefail
5
+
6
+ HERE="$(cd "$(dirname "$0")" && pwd)"
7
+ LIB="$(dirname "$HERE")/lib/dev-config.sh"
8
+
9
+ # shellcheck source=../lib/dev-config.sh
10
+ source "$LIB"
11
+
12
+ pass=0
13
+ fail=0
14
+
15
+ expect_rc() {
16
+ local label="$1" expected="$2" file="$3"
17
+ dev_config_lock_primary_branch_enabled "$file"
18
+ local actual=$?
19
+ if [[ "$actual" == "$expected" ]]; then
20
+ echo "PASS $label"; pass=$((pass + 1))
21
+ else
22
+ printf 'FAIL %s\n expected rc: %q\n actual rc: %q\n' "$label" "$expected" "$actual"
23
+ fail=$((fail + 1))
24
+ fi
25
+ }
26
+
27
+ tmp="$(mktemp -d)"
28
+ trap 'rm -rf "$tmp"' EXIT
29
+
30
+ missing="$tmp/missing.yaml"
31
+ expect_rc "missing file: disabled" 1 "$missing"
32
+
33
+ cfg="$tmp/config.yaml"
34
+
35
+ cat > "$cfg" <<'EOF'
36
+ dev:
37
+ lock:
38
+ primary-branch: true
39
+ EOF
40
+ expect_rc "nested dev flag true: enabled" 0 "$cfg"
41
+
42
+ cat > "$cfg" <<'EOF'
43
+ dev:
44
+ lock:
45
+ primary-branch: false
46
+ EOF
47
+ expect_rc "nested dev flag false: disabled" 1 "$cfg"
48
+
49
+ cat > "$cfg" <<'EOF'
50
+ dev:
51
+ lock:
52
+ primary-branch: "true"
53
+ EOF
54
+ expect_rc "quoted true: enabled" 0 "$cfg"
55
+
56
+ cat > "$cfg" <<'EOF'
57
+ afk:
58
+ default_runner: codex
59
+ EOF
60
+ expect_rc "absent key: disabled" 1 "$cfg"
61
+
62
+ cat > "$cfg" <<'EOF'
63
+ dev:
64
+ lock:
65
+ primary-branch: true
66
+ EOF
67
+ expect_rc "malformed indentation: disabled" 1 "$cfg"
68
+
69
+ cat > "$cfg" <<'EOF'
70
+ dev:
71
+ lock:
72
+ primary-branch: "true" # branch protection on
73
+ EOF
74
+ expect_rc "inline comment after quoted true: enabled" 0 "$cfg"
75
+
76
+ cat > "$cfg" <<'EOF'
77
+ dev:
78
+ lock:
79
+ primary-branch: true
80
+ afk:
81
+ backpressure:
82
+ - npm run test
83
+ - npm run lint
84
+ EOF
85
+ expect_rc "block-sequence sibling does not disable guard: enabled" 0 "$cfg"
86
+
87
+ cat > "$cfg" <<'EOF'
88
+ afk:
89
+ backpressure:
90
+ - npm run test
91
+ dev:
92
+ lock:
93
+ primary-branch: true
94
+ EOF
95
+ expect_rc "block-sequence before dev key: enabled" 0 "$cfg"
96
+
97
+ echo
98
+ echo "summary: $pass passed, $fail failed"
99
+ [[ "$fail" -eq 0 ]]
@@ -0,0 +1,187 @@
1
+ #!/usr/bin/env bash
2
+ # Unit test for lib/git-command-classifier.sh (issue #60).
3
+ #
4
+ # Locked to "main", verifies the minimal classifier:
5
+ # block — checkout/switch to a branch other than the lock target
6
+ # allow — switching back to the lock target, file-level `checkout -- <path>`,
7
+ # bare checkout, `git worktree add`, and non-git/non-checkout commands
8
+
9
+ set -uo pipefail
10
+
11
+ HERE="$(cd "$(dirname "$0")" && pwd)"
12
+ LIB="$(dirname "$HERE")/lib/git-command-classifier.sh"
13
+
14
+ # shellcheck source=../lib/git-command-classifier.sh
15
+ source "$LIB"
16
+
17
+ pass=0
18
+ fail=0
19
+
20
+ expect_verdict() {
21
+ local label="$1" lock="$2" cmd="$3" expected="$4"
22
+ local actual; actual="$(classify_git_command "$lock" "$cmd")"
23
+ if [[ "$actual" == "$expected" ]]; then
24
+ echo "PASS $label"; pass=$((pass + 1))
25
+ else
26
+ printf 'FAIL %s\n cmd: %q\n expected: %q\n actual: %q\n' \
27
+ "$label" "$cmd" "$expected" "$actual"
28
+ fail=$((fail + 1))
29
+ fi
30
+ }
31
+
32
+ LOCK="main"
33
+
34
+ # ===========================================================================
35
+ # BLOCK — leaving the locked branch
36
+ # ===========================================================================
37
+ expect_verdict "block/checkout other" "$LOCK" "git checkout feature" "block"
38
+ expect_verdict "block/switch other" "$LOCK" "git switch feature" "block"
39
+ expect_verdict "block/checkout slash branch" "$LOCK" "git checkout feature/x" "block"
40
+ expect_verdict "block/checkout -b new" "$LOCK" "git checkout -b shiny" "block"
41
+ expect_verdict "block/switch -c new" "$LOCK" "git switch -c shiny" "block"
42
+ expect_verdict "block/switch dash previous" "$LOCK" "git switch -" "block"
43
+ expect_verdict "block/compound command" "$LOCK" "cd repo && git checkout other" "block"
44
+ expect_verdict "block/checkout --track" "$LOCK" "git checkout --track origin/x" "block"
45
+
46
+ # global options before the subcommand must not bypass the lock
47
+ expect_verdict "block/-C path checkout" "$LOCK" "git -C /repo checkout other" "block"
48
+ expect_verdict "block/-C path switch" "$LOCK" "git -C /repo switch other" "block"
49
+ expect_verdict "block/-C path checkout -b" "$LOCK" "git -C /repo checkout -b shiny" "block"
50
+ expect_verdict "block/-c config checkout" "$LOCK" "git -c core.x=1 checkout other" "block"
51
+ expect_verdict "block/--git-dir eq switch" "$LOCK" "git --git-dir=/r/.git switch o" "block"
52
+ expect_verdict "block/--git-dir arg switch" "$LOCK" "git --git-dir /r/.git switch o" "block"
53
+ expect_verdict "block/--no-pager checkout" "$LOCK" "git --no-pager checkout other" "block"
54
+ expect_verdict "block/-C compound" "$LOCK" "cd x && git -C /r checkout other" "block"
55
+ expect_verdict "block/-C reset --hard" "$LOCK" "git -C /repo reset --hard" "block"
56
+ expect_verdict "block/-C stash" "$LOCK" "git -C /repo stash" "block"
57
+
58
+ # ===========================================================================
59
+ # ALLOW — same-branch / legitimate operations
60
+ # ===========================================================================
61
+ expect_verdict "allow/checkout lock target" "$LOCK" "git checkout main" "allow"
62
+ expect_verdict "allow/switch lock target" "$LOCK" "git switch main" "allow"
63
+ expect_verdict "allow/file restore --" "$LOCK" "git checkout -- src/app.ts" "allow"
64
+ expect_verdict "allow/file restore -- multi" "$LOCK" "git checkout -- a.txt b.txt" "allow"
65
+ expect_verdict "allow/bare checkout" "$LOCK" "git checkout" "allow"
66
+ expect_verdict "allow/worktree add" "$LOCK" "git worktree add ../wt branch" "allow"
67
+ expect_verdict "allow/non-checkout git" "$LOCK" "git status" "allow"
68
+ expect_verdict "allow/non-git command" "$LOCK" "ls -la" "allow"
69
+ expect_verdict "allow/commit" "$LOCK" "git commit -m wip" "allow"
70
+
71
+ # global options before the subcommand: legitimate operations still allowed
72
+ expect_verdict "allow/-C checkout lock" "$LOCK" "git -C /repo checkout main" "allow"
73
+ expect_verdict "allow/-C status" "$LOCK" "git -C /repo status" "allow"
74
+ expect_verdict "allow/-C file restore" "$LOCK" "git -C /repo checkout -- a.ts" "allow"
75
+ expect_verdict "allow/-C worktree add" "$LOCK" "git -C /repo worktree add ../w" "allow"
76
+
77
+ # slash lock target: switching back to it is allowed, elsewhere blocked
78
+ expect_verdict "allow/slash lock back" "feature/x" "git switch feature/x" "allow"
79
+ expect_verdict "block/slash other" "feature/x" "git switch feature/y" "block"
80
+
81
+ # ===========================================================================
82
+ # BLOCK — the work-loss family (issue #61)
83
+ # ===========================================================================
84
+ expect_verdict "block/stash bare" "$LOCK" "git stash" "block"
85
+ expect_verdict "block/stash push" "$LOCK" "git stash push" "block"
86
+ expect_verdict "block/stash push paths" "$LOCK" "git stash push -m x src/" "block"
87
+ expect_verdict "block/stash save" "$LOCK" "git stash save wip" "block"
88
+ expect_verdict "block/clean -f" "$LOCK" "git clean -f" "block"
89
+ expect_verdict "block/clean -fd" "$LOCK" "git clean -fd" "block"
90
+ expect_verdict "block/clean -xfd" "$LOCK" "git clean -xfd" "block"
91
+ expect_verdict "block/clean --force" "$LOCK" "git clean --force" "block"
92
+ expect_verdict "block/reset --hard" "$LOCK" "git reset --hard" "block"
93
+ expect_verdict "block/reset --hard HEAD~1" "$LOCK" "git reset --hard HEAD~1" "block"
94
+ expect_verdict "block/checkout dot" "$LOCK" "git checkout ." "block"
95
+ expect_verdict "block/restore dot" "$LOCK" "git restore ." "block"
96
+ expect_verdict "block/checkout dashdash dot" "$LOCK" "git checkout -- ." "block"
97
+ expect_verdict "block/family compound" "$LOCK" "cd repo && git reset --hard" "block"
98
+
99
+ # ===========================================================================
100
+ # ALLOW — non-destructive members of the same command families
101
+ # ===========================================================================
102
+ expect_verdict "allow/stash list" "$LOCK" "git stash list" "allow"
103
+ expect_verdict "allow/stash show" "$LOCK" "git stash show" "allow"
104
+ expect_verdict "allow/clean dry-run" "$LOCK" "git clean -n" "allow"
105
+ expect_verdict "allow/clean --dry-run" "$LOCK" "git clean --dry-run" "allow"
106
+ expect_verdict "allow/reset soft" "$LOCK" "git reset --soft HEAD~1" "allow"
107
+ expect_verdict "allow/reset mixed default" "$LOCK" "git reset HEAD file.txt" "allow"
108
+ expect_verdict "allow/restore staged" "$LOCK" "git restore --staged f.txt" "allow"
109
+ expect_verdict "allow/restore single file" "$LOCK" "git restore src/app.ts" "allow"
110
+
111
+ # ===========================================================================
112
+ # PRIMARY BRANCH GUARD — dev.lock.primary-branch (issue #396 / ADR 0043)
113
+ # Table: command, cwd-is-primary, flag-state => allow/block.
114
+ # ===========================================================================
115
+ expect_primary_guard() {
116
+ local label="$1" cmd="$2" primary="$3" flag="$4" expected="$5"
117
+ local actual="allow"
118
+ if [[ "$primary" == "primary" && "$flag" == "on" ]]; then
119
+ actual="$(classify_primary_branch_switch_guard "$cmd")"
120
+ fi
121
+ if [[ "$actual" == "$expected" ]]; then
122
+ echo "PASS $label"; pass=$((pass + 1))
123
+ else
124
+ printf 'FAIL %s\n cmd: %q\n primary: %q\n flag: %q\n expected: %q\n actual: %q\n' \
125
+ "$label" "$cmd" "$primary" "$flag" "$expected" "$actual"
126
+ fail=$((fail + 1))
127
+ fi
128
+ }
129
+
130
+ expect_primary_guard "primary/on/switch other blocked" "git switch other" primary on block
131
+ expect_primary_guard "primary/on/checkout other blocked" "git checkout other" primary on block
132
+ expect_primary_guard "primary/on/switch -b new blocked" "git switch -b new" primary on block
133
+ expect_primary_guard "primary/on/checkout -b new blocked" "git checkout -b new" primary on block
134
+ expect_primary_guard "primary/on/compound switch blocked" "cd repo && git switch other" primary on block
135
+ expect_primary_guard "primary/on/-C switch blocked" "git -C /repo switch other" primary on block
136
+ expect_primary_guard "primary/on/-C checkout -b blocked" "git -C /repo checkout -b new" primary on block
137
+ expect_primary_guard "primary/on/switch dash blocked" "git switch -" primary on block
138
+ expect_primary_guard "primary/on/commit allowed" "git commit -m wip" primary on allow
139
+ expect_primary_guard "primary/on/worktree add allowed" "git worktree add ../wt other" primary on allow
140
+ expect_primary_guard "primary/on/status allowed" "git status" primary on allow
141
+ expect_primary_guard "primary/on/checkout path allowed" "git checkout -- src/app.ts" primary on allow
142
+ expect_primary_guard "primary/on/checkout dot allowed" "git checkout ." primary on allow
143
+ expect_primary_guard "primary/off/switch allowed" "git switch other" primary off allow
144
+ expect_primary_guard "worktree/on/switch allowed" "git switch other" worktree on allow
145
+ expect_primary_guard "worktree/off/switch allowed" "git switch other" worktree off allow
146
+
147
+ # ---------------------------------------------------------------------------
148
+ # issue #1024: block git reset (any form) and git stash (all subcommands),
149
+ # plus git rebase --autostash, in the primary checkout. Parallel human WIP
150
+ # lives there; these commands have destroyed in-progress work before.
151
+ # ---------------------------------------------------------------------------
152
+ expect_primary_guard "primary/on/reset bare blocked" "git reset" primary on block
153
+ expect_primary_guard "primary/on/reset --hard blocked" "git reset --hard" primary on block
154
+ expect_primary_guard "primary/on/reset --hard HEAD~1 blocked" "git reset --hard HEAD~1" primary on block
155
+ expect_primary_guard "primary/on/reset --soft blocked" "git reset --soft HEAD~1" primary on block
156
+ expect_primary_guard "primary/on/reset --mixed blocked" "git reset --mixed" primary on block
157
+ expect_primary_guard "primary/on/reset pathspec blocked" "git reset HEAD file.txt" primary on block
158
+ expect_primary_guard "primary/on/-C reset blocked" "git -C /repo reset --hard" primary on block
159
+ expect_primary_guard "primary/on/compound reset blocked" "cd repo && git reset --hard" primary on block
160
+
161
+ expect_primary_guard "primary/on/stash bare blocked" "git stash" primary on block
162
+ expect_primary_guard "primary/on/stash push blocked" "git stash push" primary on block
163
+ expect_primary_guard "primary/on/stash push paths blocked" "git stash push -m x src/" primary on block
164
+ expect_primary_guard "primary/on/stash save blocked" "git stash save wip" primary on block
165
+ expect_primary_guard "primary/on/stash pop blocked" "git stash pop" primary on block
166
+ expect_primary_guard "primary/on/stash apply blocked" "git stash apply" primary on block
167
+ expect_primary_guard "primary/on/stash list blocked" "git stash list" primary on block
168
+ expect_primary_guard "primary/on/stash show blocked" "git stash show" primary on block
169
+ expect_primary_guard "primary/on/-C stash blocked" "git -C /repo stash" primary on block
170
+
171
+ expect_primary_guard "primary/on/rebase autostash blocked" "git rebase --autostash" primary on block
172
+ expect_primary_guard "primary/on/rebase autostash arg blocked" "git rebase --autostash origin/main" primary on block
173
+ expect_primary_guard "primary/on/rebase main autostash blocked" "git rebase main --autostash" primary on block
174
+
175
+ # rebase without --autostash is not the work-loss vector this rule targets
176
+ expect_primary_guard "primary/on/rebase plain allowed" "git rebase main" primary on allow
177
+ expect_primary_guard "primary/on/rebase --no-autostash allowed" "git rebase --no-autostash main" primary on allow
178
+
179
+ # worktrees stay exempt (scope), flag-off stays inert — for reset/stash too
180
+ expect_primary_guard "worktree/on/reset allowed" "git reset --hard" worktree on allow
181
+ expect_primary_guard "worktree/on/stash allowed" "git stash" worktree on allow
182
+ expect_primary_guard "primary/off/reset allowed" "git reset --hard" primary off allow
183
+ expect_primary_guard "primary/off/stash allowed" "git stash" primary off allow
184
+
185
+ echo
186
+ echo "summary: $pass passed, $fail failed"
187
+ [ "$fail" -eq 0 ]
@@ -0,0 +1,90 @@
1
+ #!/usr/bin/env bash
2
+ # Unit test for lib/lock-store.sh (issue #60).
3
+ #
4
+ # Sources the module directly (no globals) and exercises all four entry points
5
+ # against temp lock files:
6
+ # - write/read round-trip, including a branch name with a slash
7
+ # - absent file => unlocked (read rc 1, no output; is_locked rc 1)
8
+ # - clear removes the file and is idempotent
9
+ # - write is atomic and overwrites (relock to a new target)
10
+
11
+ set -uo pipefail
12
+
13
+ HERE="$(cd "$(dirname "$0")" && pwd)"
14
+ LIB="$(dirname "$HERE")/lib/lock-store.sh"
15
+
16
+ # shellcheck source=../lib/lock-store.sh
17
+ source "$LIB"
18
+
19
+ pass=0
20
+ fail=0
21
+
22
+ expect_eq() {
23
+ local label="$1" expected="$2" actual="$3"
24
+ if [[ "$expected" == "$actual" ]]; then
25
+ echo "PASS $label"; pass=$((pass + 1))
26
+ else
27
+ printf 'FAIL %s\n expected: %q\n actual: %q\n' "$label" "$expected" "$actual"
28
+ fail=$((fail + 1))
29
+ fi
30
+ }
31
+
32
+ # ===========================================================================
33
+ # absent file => unlocked
34
+ # ===========================================================================
35
+ tmpdir="$(mktemp -d)"
36
+ lock="$tmpdir/.red/tmp/branch-lock.yaml"
37
+
38
+ out="$(lock_store_read "$lock")"; rc=$?
39
+ expect_eq "absent/read rc 1" "1" "$rc"
40
+ expect_eq "absent/read silent" "" "$out"
41
+ lock_store_is_locked "$lock"; rc=$?
42
+ expect_eq "absent/is_locked rc 1" "1" "$rc"
43
+
44
+ # ===========================================================================
45
+ # write / read round-trip (parent dir created, slash branch preserved)
46
+ # ===========================================================================
47
+ lock_store_write "$lock" "feature/branch-lock"; rc=$?
48
+ expect_eq "write/rc 0" "0" "$rc"
49
+ expect_eq "write/file exists" "1" "$([[ -f "$lock" ]] && echo 1 || echo 0)"
50
+ expect_eq "write/read back" "feature/branch-lock" "$(lock_store_read "$lock")"
51
+ lock_store_is_locked "$lock"; rc=$?
52
+ expect_eq "write/is_locked rc 0" "0" "$rc"
53
+ # stored content is a single line (no trailing whitespace in the value)
54
+ expect_eq "write/single line" "1" "$(wc -l < "$lock" | tr -d ' ')"
55
+
56
+ # ===========================================================================
57
+ # write overwrites (relock to a new target)
58
+ # ===========================================================================
59
+ lock_store_write "$lock" "main"
60
+ expect_eq "relock/read new target" "main" "$(lock_store_read "$lock")"
61
+
62
+ # missing arg => rc 2, file untouched
63
+ lock_store_write "$lock" "" 2>/dev/null; rc=$?
64
+ expect_eq "write/missing branch rc 2" "2" "$rc"
65
+ expect_eq "write/untouched after bad" "main" "$(lock_store_read "$lock")"
66
+
67
+ # ===========================================================================
68
+ # clear removes the file and is idempotent
69
+ # ===========================================================================
70
+ lock_store_clear "$lock"; rc=$?
71
+ expect_eq "clear/rc 0" "0" "$rc"
72
+ expect_eq "clear/file gone" "0" "$([[ -f "$lock" ]] && echo 1 || echo 0)"
73
+ lock_store_clear "$lock"; rc=$?
74
+ expect_eq "clear/idempotent rc 0" "0" "$rc"
75
+ lock_store_is_locked "$lock"; rc=$?
76
+ expect_eq "clear/is_locked rc 1" "1" "$rc"
77
+
78
+ # ===========================================================================
79
+ # an empty lock file reads as unlocked (defensive: half-written / truncated)
80
+ # ===========================================================================
81
+ mkdir -p "$(dirname "$lock")"; : > "$lock"
82
+ out="$(lock_store_read "$lock")"; rc=$?
83
+ expect_eq "empty/read rc 1" "1" "$rc"
84
+ expect_eq "empty/read silent" "" "$out"
85
+
86
+ rm -rf "$tmpdir"
87
+
88
+ echo
89
+ echo "summary: $pass passed, $fail failed"
90
+ [ "$fail" -eq 0 ]