@reddb-io/red-skills 4.4.1 → 4.6.0

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 (78) hide show
  1. package/bin/build-info.mjs +87 -0
  2. package/bin/cli-frontdoor.mjs +14 -0
  3. package/bin/red-skills-brain.mjs +17 -22
  4. package/bin/red-skills-content.mjs +17 -0
  5. package/bin/red-skills-hook.mjs +45 -0
  6. package/bin/red-skills-memory.mjs +17 -22
  7. package/bin/red-skills-redskilled-mcp.mjs +2 -0
  8. package/bin/red-skills-resource.mjs +21 -0
  9. package/bin/runtime-compatibility.mjs +17 -0
  10. package/bin/runtime-paths.mjs +13 -0
  11. package/dist/code-nav.bundle.min.mjs +1 -1
  12. package/dist/opencode-host.bundle.min.mjs +1 -1
  13. package/dist/redskilled-mcp.bundle.min.mjs +36 -36
  14. package/dist/redskilled-web.bundle.min.mjs +311 -0
  15. package/dist/redskilled.bundle.min.mjs +306 -292
  16. package/dist/release.bundle.min.mjs +1 -1
  17. package/dist/rsp-core.bundle.min.mjs +1 -1
  18. package/dist/rsp.bundle.min.mjs +1 -1
  19. package/dist/statusline.bundle.min.mjs +1 -1
  20. package/package.json +12 -4
  21. package/runtime/hook-routes.json +125 -0
  22. package/runtime/plugins/brain/scripts/bootstrap.mjs +656 -0
  23. package/runtime/plugins/brain/scripts/structural-smoke.sh +7 -0
  24. package/runtime/plugins/dev/hooks/branch-lock-codex.sh +105 -0
  25. package/runtime/plugins/dev/hooks/code-nav-mcp.sh +63 -0
  26. package/runtime/plugins/dev/hooks/command-guard.sh +893 -0
  27. package/runtime/plugins/dev/hooks/ensure-codex-statusline.mjs +144 -0
  28. package/runtime/plugins/dev/hooks/red-fetch.mjs +38 -0
  29. package/runtime/plugins/dev/hooks/redskilled-mcp.sh +64 -0
  30. package/runtime/plugins/dev/hooks/rsp-hook.sh +343 -0
  31. package/runtime/plugins/dev/hooks/tests/command-guard.test.sh +378 -0
  32. package/runtime/plugins/dev/hooks/tests/rsp-hook.test.sh +229 -0
  33. package/runtime/plugins/dev/scripts/memory-bridge.sh +230 -0
  34. package/runtime/plugins/dev/scripts/structural-smoke.sh +7 -0
  35. package/runtime/plugins/dev/scripts/tests/memory-bridge.test.sh +555 -0
  36. package/runtime/plugins/dev/skills/engineering/afk/defaults/cargo-pre-worktree.sh +37 -0
  37. package/runtime/plugins/dev/skills/engineering/afk/defaults/envelope-post-attempt.sh +47 -0
  38. package/runtime/plugins/dev/skills/engineering/afk/defaults/gradle-pre-worktree.sh +45 -0
  39. package/runtime/plugins/dev/skills/engineering/afk/defaults/heartbeat-post-attempt.sh +43 -0
  40. package/runtime/plugins/dev/skills/engineering/afk/defaults/validation-post-merge.sh +77 -0
  41. package/runtime/plugins/dev/skills/engineering/afk/detectors/cargo.sh +27 -0
  42. package/runtime/plugins/dev/skills/engineering/afk/detectors/gradle.sh +38 -0
  43. package/runtime/plugins/dev/skills/engineering/afk/examples/only-mine.sh +32 -0
  44. package/runtime/plugins/dev/skills/engineering/afk/examples/red-afk-attempt-action.yml +53 -0
  45. package/runtime/plugins/dev/skills/engineering/afk/examples/rs-afk-attempt.yml +76 -0
  46. package/runtime/plugins/dev/skills/engineering/afk/hooks/red-cargo +38 -0
  47. package/runtime/plugins/dev/skills/engineering/afk/hooks/red-envelope +50 -0
  48. package/runtime/plugins/dev/skills/engineering/afk/hooks/red-gradle +42 -0
  49. package/runtime/plugins/dev/skills/engineering/afk/hooks/red-heartbeat +46 -0
  50. package/runtime/plugins/dev/skills/engineering/afk/hooks/red-validation +79 -0
  51. package/runtime/plugins/dev/skills/engineering/diagnose/scripts/hitl-loop.template.sh +41 -0
  52. package/runtime/plugins/dev/skills/engineering/red-setup/scripts/install-runtime-shim.sh +181 -0
  53. package/runtime/plugins/dev/skills/engineering/red-setup/workflows/red-issues-needs-triage.yml +36 -0
  54. package/runtime/plugins/dev/skills/engineering/wizard/template.sh +211 -0
  55. package/runtime/plugins/dev/skills/engineering/zoom-out/scripts/graph-mode-plumbing.sh +36 -0
  56. package/runtime/plugins/dev/skills/engineering/zoom-out/scripts/tests/contract.test.sh +168 -0
  57. package/runtime/plugins/dev/skills/misc/branch-lock/scripts/branch-lock-hook.sh +113 -0
  58. package/runtime/plugins/dev/skills/misc/branch-lock/scripts/branch-lock-session-start.sh +81 -0
  59. package/runtime/plugins/dev/skills/misc/branch-lock/scripts/branch-lock.sh +68 -0
  60. package/runtime/plugins/dev/skills/misc/branch-lock/scripts/lib/dev-config.sh +126 -0
  61. package/runtime/plugins/dev/skills/misc/branch-lock/scripts/lib/git-command-classifier.sh +198 -0
  62. package/runtime/plugins/dev/skills/misc/branch-lock/scripts/lib/lock-store.sh +67 -0
  63. package/runtime/plugins/dev/skills/misc/branch-lock/scripts/lib/scope-resolver.sh +43 -0
  64. package/runtime/plugins/dev/skills/misc/branch-lock/scripts/tests/branch-lock-cli.test.sh +114 -0
  65. package/runtime/plugins/dev/skills/misc/branch-lock/scripts/tests/claude-plugin-hook.test.sh +117 -0
  66. package/runtime/plugins/dev/skills/misc/branch-lock/scripts/tests/codex-hook.test.sh +230 -0
  67. package/runtime/plugins/dev/skills/misc/branch-lock/scripts/tests/dev-config.test.sh +99 -0
  68. package/runtime/plugins/dev/skills/misc/branch-lock/scripts/tests/git-command-classifier.test.sh +187 -0
  69. package/runtime/plugins/dev/skills/misc/branch-lock/scripts/tests/lock-store.test.sh +90 -0
  70. package/runtime/plugins/dev/skills/misc/branch-lock/scripts/tests/scope-resolver.test.sh +63 -0
  71. package/runtime/plugins/dev/skills/misc/branch-lock/scripts/tests/session-start.test.sh +123 -0
  72. package/runtime/plugins/dev/skills/misc/git-guardrails-claude-code/scripts/block-dangerous-git.sh +231 -0
  73. package/runtime/plugins/dev/skills/misc/git-guardrails-claude-code/scripts/tests/block-dangerous-git.test.sh +143 -0
  74. package/runtime/plugins/internal/scripts/structural-smoke.sh +7 -0
  75. package/runtime/plugins/internal/skills/maintainer/create-plugin/scripts/create-plugin.sh +308 -0
  76. package/runtime/plugins/memory/scripts/bootstrap.mjs +659 -0
  77. package/runtime/plugins/memory/scripts/structural-smoke.sh +7 -0
  78. package/scripts/check-skills-boundary.mjs +32 -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 ]
@@ -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 ]