@reddb-io/red-skills 4.4.1 → 4.5.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 (77) 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.bundle.min.mjs +1 -1
  15. package/dist/release.bundle.min.mjs +1 -1
  16. package/dist/rsp-core.bundle.min.mjs +1 -1
  17. package/dist/rsp.bundle.min.mjs +1 -1
  18. package/dist/statusline.bundle.min.mjs +1 -1
  19. package/package.json +9 -4
  20. package/runtime/hook-routes.json +125 -0
  21. package/runtime/plugins/brain/scripts/bootstrap.mjs +656 -0
  22. package/runtime/plugins/brain/scripts/structural-smoke.sh +7 -0
  23. package/runtime/plugins/dev/hooks/branch-lock-codex.sh +105 -0
  24. package/runtime/plugins/dev/hooks/code-nav-mcp.sh +63 -0
  25. package/runtime/plugins/dev/hooks/command-guard.sh +893 -0
  26. package/runtime/plugins/dev/hooks/ensure-codex-statusline.mjs +144 -0
  27. package/runtime/plugins/dev/hooks/red-fetch.mjs +38 -0
  28. package/runtime/plugins/dev/hooks/redskilled-mcp.sh +64 -0
  29. package/runtime/plugins/dev/hooks/rsp-hook.sh +343 -0
  30. package/runtime/plugins/dev/hooks/tests/command-guard.test.sh +378 -0
  31. package/runtime/plugins/dev/hooks/tests/rsp-hook.test.sh +229 -0
  32. package/runtime/plugins/dev/scripts/memory-bridge.sh +230 -0
  33. package/runtime/plugins/dev/scripts/structural-smoke.sh +7 -0
  34. package/runtime/plugins/dev/scripts/tests/memory-bridge.test.sh +555 -0
  35. package/runtime/plugins/dev/skills/engineering/afk/defaults/cargo-pre-worktree.sh +37 -0
  36. package/runtime/plugins/dev/skills/engineering/afk/defaults/envelope-post-attempt.sh +47 -0
  37. package/runtime/plugins/dev/skills/engineering/afk/defaults/gradle-pre-worktree.sh +45 -0
  38. package/runtime/plugins/dev/skills/engineering/afk/defaults/heartbeat-post-attempt.sh +43 -0
  39. package/runtime/plugins/dev/skills/engineering/afk/defaults/validation-post-merge.sh +77 -0
  40. package/runtime/plugins/dev/skills/engineering/afk/detectors/cargo.sh +27 -0
  41. package/runtime/plugins/dev/skills/engineering/afk/detectors/gradle.sh +38 -0
  42. package/runtime/plugins/dev/skills/engineering/afk/examples/only-mine.sh +32 -0
  43. package/runtime/plugins/dev/skills/engineering/afk/examples/red-afk-attempt-action.yml +53 -0
  44. package/runtime/plugins/dev/skills/engineering/afk/examples/rs-afk-attempt.yml +76 -0
  45. package/runtime/plugins/dev/skills/engineering/afk/hooks/red-cargo +38 -0
  46. package/runtime/plugins/dev/skills/engineering/afk/hooks/red-envelope +50 -0
  47. package/runtime/plugins/dev/skills/engineering/afk/hooks/red-gradle +42 -0
  48. package/runtime/plugins/dev/skills/engineering/afk/hooks/red-heartbeat +46 -0
  49. package/runtime/plugins/dev/skills/engineering/afk/hooks/red-validation +79 -0
  50. package/runtime/plugins/dev/skills/engineering/diagnose/scripts/hitl-loop.template.sh +41 -0
  51. package/runtime/plugins/dev/skills/engineering/red-setup/scripts/install-runtime-shim.sh +181 -0
  52. package/runtime/plugins/dev/skills/engineering/red-setup/workflows/red-issues-needs-triage.yml +36 -0
  53. package/runtime/plugins/dev/skills/engineering/wizard/template.sh +211 -0
  54. package/runtime/plugins/dev/skills/engineering/zoom-out/scripts/graph-mode-plumbing.sh +36 -0
  55. package/runtime/plugins/dev/skills/engineering/zoom-out/scripts/tests/contract.test.sh +168 -0
  56. package/runtime/plugins/dev/skills/misc/branch-lock/scripts/branch-lock-hook.sh +113 -0
  57. package/runtime/plugins/dev/skills/misc/branch-lock/scripts/branch-lock-session-start.sh +81 -0
  58. package/runtime/plugins/dev/skills/misc/branch-lock/scripts/branch-lock.sh +68 -0
  59. package/runtime/plugins/dev/skills/misc/branch-lock/scripts/lib/dev-config.sh +126 -0
  60. package/runtime/plugins/dev/skills/misc/branch-lock/scripts/lib/git-command-classifier.sh +198 -0
  61. package/runtime/plugins/dev/skills/misc/branch-lock/scripts/lib/lock-store.sh +67 -0
  62. package/runtime/plugins/dev/skills/misc/branch-lock/scripts/lib/scope-resolver.sh +43 -0
  63. package/runtime/plugins/dev/skills/misc/branch-lock/scripts/tests/branch-lock-cli.test.sh +114 -0
  64. package/runtime/plugins/dev/skills/misc/branch-lock/scripts/tests/claude-plugin-hook.test.sh +117 -0
  65. package/runtime/plugins/dev/skills/misc/branch-lock/scripts/tests/codex-hook.test.sh +230 -0
  66. package/runtime/plugins/dev/skills/misc/branch-lock/scripts/tests/dev-config.test.sh +99 -0
  67. package/runtime/plugins/dev/skills/misc/branch-lock/scripts/tests/git-command-classifier.test.sh +187 -0
  68. package/runtime/plugins/dev/skills/misc/branch-lock/scripts/tests/lock-store.test.sh +90 -0
  69. package/runtime/plugins/dev/skills/misc/branch-lock/scripts/tests/scope-resolver.test.sh +63 -0
  70. package/runtime/plugins/dev/skills/misc/branch-lock/scripts/tests/session-start.test.sh +123 -0
  71. package/runtime/plugins/dev/skills/misc/git-guardrails-claude-code/scripts/block-dangerous-git.sh +231 -0
  72. package/runtime/plugins/dev/skills/misc/git-guardrails-claude-code/scripts/tests/block-dangerous-git.test.sh +143 -0
  73. package/runtime/plugins/internal/scripts/structural-smoke.sh +7 -0
  74. package/runtime/plugins/internal/skills/maintainer/create-plugin/scripts/create-plugin.sh +308 -0
  75. package/runtime/plugins/memory/scripts/bootstrap.mjs +659 -0
  76. package/runtime/plugins/memory/scripts/structural-smoke.sh +7 -0
  77. package/scripts/check-skills-boundary.mjs +32 -0
@@ -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 ]
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ REPO="$(cd "$(dirname "$0")/../../.." && pwd)"
5
+ cd "$REPO"
6
+
7
+ node scripts/plugin-structural-smoke.mjs plugins/internal
@@ -0,0 +1,308 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ usage() {
5
+ cat <<'EOF'
6
+ usage: create-plugin.sh [--root REPO_ROOT] <plugin-name>
7
+
8
+ Scaffolds a RedSkills plugin and appends it to both marketplace manifests.
9
+ EOF
10
+ }
11
+
12
+ fail() {
13
+ printf 'error: %s\n' "$*" >&2
14
+ exit 1
15
+ }
16
+
17
+ ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
18
+
19
+ while [ "$#" -gt 0 ]; do
20
+ case "$1" in
21
+ --root)
22
+ [ "$#" -ge 2 ] || fail "--root requires a path"
23
+ ROOT="$2"
24
+ shift 2
25
+ ;;
26
+ -h|--help)
27
+ usage
28
+ exit 0
29
+ ;;
30
+ --*)
31
+ fail "unknown option: $1"
32
+ ;;
33
+ *)
34
+ break
35
+ ;;
36
+ esac
37
+ done
38
+
39
+ [ "$#" -eq 1 ] || {
40
+ usage >&2
41
+ exit 2
42
+ }
43
+
44
+ command -v jq >/dev/null || fail "jq is required"
45
+
46
+ PLUGIN="$1"
47
+ PLUGIN="$(printf '%s' "$PLUGIN" \
48
+ | tr '[:upper:]' '[:lower:]' \
49
+ | sed -E 's/[^a-z0-9]+/-/g; s/^-+//; s/-+$//; s/-+/-/g')"
50
+
51
+ [ -n "$PLUGIN" ] || fail "plugin name normalizes to empty"
52
+ [ "${#PLUGIN}" -le 64 ] || fail "plugin name must be 64 characters or fewer after normalization"
53
+
54
+ ROOT="$(cd "$ROOT" && pwd)"
55
+ PLUGIN_DIR="$ROOT/plugins/$PLUGIN"
56
+ SKILL_NAME="${PLUGIN}-demo"
57
+ SKILL_DIR="$PLUGIN_DIR/skills/core/$SKILL_NAME"
58
+
59
+ [ ! -e "$PLUGIN_DIR" ] || fail "plugin already exists: plugins/$PLUGIN"
60
+ [ -f "$ROOT/.claude-plugin/marketplace.json" ] || fail "missing .claude-plugin/marketplace.json"
61
+ [ -f "$ROOT/.agents/plugins/marketplace.json" ] || fail "missing .agents/plugins/marketplace.json"
62
+ [ -f "$ROOT/.gemini-plugin/marketplace.json" ] || fail "missing .gemini-plugin/marketplace.json"
63
+
64
+ mkdir -p \
65
+ "$PLUGIN_DIR/.claude-plugin" \
66
+ "$PLUGIN_DIR/.codex-plugin" \
67
+ "$PLUGIN_DIR/.gemini-plugin" \
68
+ "$SKILL_DIR"
69
+
70
+ cat > "$PLUGIN_DIR/.claude-plugin/plugin.json" <<EOF
71
+ {
72
+ "name": "$PLUGIN",
73
+ "version": "0.1.0",
74
+ "description": "reddb.io $PLUGIN plugin - repository-local RedSkills extension.",
75
+ "skills": [
76
+ "./skills/core/$SKILL_NAME"
77
+ ]
78
+ }
79
+ EOF
80
+
81
+ cat > "$PLUGIN_DIR/.codex-plugin/plugin.json" <<EOF
82
+ {
83
+ "name": "$PLUGIN",
84
+ "version": "0.1.0",
85
+ "description": "reddb.io $PLUGIN plugin - repository-local RedSkills extension.",
86
+ "author": {
87
+ "name": "reddb.io",
88
+ "url": "https://github.com/reddb-io"
89
+ },
90
+ "homepage": "https://github.com/reddb-io/red-skills",
91
+ "repository": "https://github.com/reddb-io/red-skills",
92
+ "license": "Apache-2.0",
93
+ "keywords": [
94
+ "codex",
95
+ "claude-code",
96
+ "skills",
97
+ "agents"
98
+ ],
99
+ "skills": "./skills/",
100
+ "interface": {
101
+ "displayName": "$PLUGIN",
102
+ "shortDescription": "Repository-local RedSkills extension.",
103
+ "longDescription": "The $PLUGIN plugin is a repository-local RedSkills extension scaffolded with born-compliant marketplace metadata, skill structure, documentation, changelog, and structural smoke checks.",
104
+ "developerName": "reddb.io",
105
+ "category": "Developer Tools",
106
+ "capabilities": [
107
+ "Interactive",
108
+ "Read",
109
+ "Shell"
110
+ ],
111
+ "websiteURL": "https://github.com/reddb-io/red-skills",
112
+ "defaultPrompt": [
113
+ "Run \$$SKILL_NAME to verify the $PLUGIN plugin is available."
114
+ ],
115
+ "brandColor": "#2563EB"
116
+ }
117
+ }
118
+ EOF
119
+
120
+ cat > "$PLUGIN_DIR/.gemini-plugin/plugin.json" <<EOF
121
+ {
122
+ "name": "$PLUGIN",
123
+ "version": "0.1.0",
124
+ "description": "reddb.io $PLUGIN plugin - repository-local RedSkills extension.",
125
+ "author": {
126
+ "name": "reddb.io",
127
+ "url": "https://github.com/reddb-io"
128
+ },
129
+ "homepage": "https://github.com/reddb-io/red-skills",
130
+ "repository": "https://github.com/reddb-io/red-skills",
131
+ "license": "Apache-2.0",
132
+ "keywords": [
133
+ "gemini-cli",
134
+ "skills",
135
+ "agents"
136
+ ],
137
+ "skills": [
138
+ "./skills/core/$SKILL_NAME/"
139
+ ],
140
+ "interface": {
141
+ "displayName": "$PLUGIN",
142
+ "shortDescription": "Repository-local RedSkills extension.",
143
+ "longDescription": "The $PLUGIN plugin is a repository-local RedSkills extension scaffolded with born-compliant marketplace metadata, skill structure, documentation, changelog, and structural smoke checks.",
144
+ "developerName": "reddb.io",
145
+ "category": "Developer Tools",
146
+ "capabilities": [
147
+ "Interactive",
148
+ "Read",
149
+ "Shell"
150
+ ],
151
+ "websiteURL": "https://github.com/reddb-io/red-skills",
152
+ "defaultPrompt": [
153
+ "Run \$$SKILL_NAME to verify the $PLUGIN plugin is available."
154
+ ],
155
+ "brandColor": "#2563EB"
156
+ }
157
+ }
158
+ EOF
159
+
160
+ cat > "$SKILL_DIR/SKILL.md" <<EOF
161
+ ---
162
+ name: $SKILL_NAME
163
+ working-mode: interactive
164
+ description: Use when verifying that the scaffolded $PLUGIN plugin is installed, discoverable, and ready for repository-specific implementation.
165
+ ---
166
+
167
+ # $SKILL_NAME
168
+
169
+ **Verify the scaffold before adding domain behavior.** Keep this seed skill
170
+ small until the plugin has a real maintainer workflow.
171
+
172
+ <what-to-do>
173
+
174
+ Run the plugin smoke script from the RedSkills repository root:
175
+
176
+ \`\`\`bash
177
+ red-skills-content --check "$ROOT"
178
+ \`\`\`
179
+
180
+ Report the printed result.
181
+
182
+ </what-to-do>
183
+
184
+ <supporting-info>
185
+
186
+ This seed exists so new plugins ship with a valid skill tree on day one. Replace
187
+ it only after the replacement skill keeps the same frontmatter discipline,
188
+ two-section body structure, and marketplace-safe tool grants.
189
+
190
+ </supporting-info>
191
+ EOF
192
+
193
+
194
+ cat > "$PLUGIN_DIR/README.md" <<EOF
195
+ # $PLUGIN
196
+
197
+ Repository-local RedSkills plugin scaffolded by the internal create-plugin
198
+ maintainer skill.
199
+
200
+ ## Install
201
+
202
+ Install through the RedSkills marketplace after this plugin has been committed
203
+ to the repository and the marketplace manifests have been published.
204
+
205
+ ## Skills
206
+
207
+ - \`$SKILL_NAME\` - verifies the scaffolded plugin structure before real
208
+ maintainer workflows are added.
209
+
210
+ ## Validation
211
+
212
+ \`\`\`bash
213
+ bash scripts/validate-marketplace-manifests.sh
214
+ bash scripts/lint-skill-frontmatter.sh
215
+ red-skills-content --check "$ROOT"
216
+ \`\`\`
217
+
218
+ ## Maintenance
219
+
220
+ Change the internal create-plugin scaffolder before changing this plugin
221
+ contract. Generated plugins are the contract fixture for future plugin work.
222
+ EOF
223
+
224
+ cat > "$PLUGIN_DIR/CHANGES.md" <<EOF
225
+ # CHANGES - $PLUGIN
226
+
227
+ ## $PLUGIN
228
+
229
+ - **status**: added
230
+ - **upstream**: none
231
+ - **why**: new repository-local plugin scaffold.
232
+ - **what changed**: created born-compliant plugin manifests, seed skill,
233
+ structural smoke script, README, and marketplace entries.
234
+ EOF
235
+
236
+ tmp="$(mktemp)"
237
+ trap 'rm -f "$tmp"' EXIT
238
+
239
+ jq --arg name "$PLUGIN" --arg source "./plugins/$PLUGIN" --arg description "reddb.io $PLUGIN plugin - repository-local RedSkills extension." '
240
+ if any(.plugins[]?; .name == $name) then
241
+ error("Claude marketplace already contains plugin " + $name)
242
+ else
243
+ .plugins += [{
244
+ "name": $name,
245
+ "source": $source,
246
+ "description": $description
247
+ }]
248
+ end
249
+ ' "$ROOT/.claude-plugin/marketplace.json" > "$tmp"
250
+ mv "$tmp" "$ROOT/.claude-plugin/marketplace.json"
251
+
252
+ jq --arg name "$PLUGIN" --arg path "./plugins/$PLUGIN" --arg description "Repository-local RedSkills extension." '
253
+ if any(.plugins[]?; .name == $name) then
254
+ error("Codex marketplace already contains plugin " + $name)
255
+ else
256
+ .plugins += [{
257
+ "name": $name,
258
+ "description": $description,
259
+ "source": {
260
+ "source": "local",
261
+ "path": $path
262
+ },
263
+ "policy": {
264
+ "installation": "AVAILABLE",
265
+ "authentication": "ON_USE"
266
+ },
267
+ "category": "Developer Tools"
268
+ }]
269
+ end
270
+ ' "$ROOT/.agents/plugins/marketplace.json" > "$tmp"
271
+ mv "$tmp" "$ROOT/.agents/plugins/marketplace.json"
272
+
273
+ jq --arg name "$PLUGIN" --arg path "./plugins/$PLUGIN" --arg description "Repository-local RedSkills extension." '
274
+ if any(.plugins[]?; .name == $name) then
275
+ error("Gemini marketplace already contains plugin " + $name)
276
+ else
277
+ .plugins += [{
278
+ "name": $name,
279
+ "description": $description,
280
+ "source": {
281
+ "source": "local",
282
+ "path": $path
283
+ },
284
+ "policy": {
285
+ "installation": "AVAILABLE",
286
+ "authentication": "ON_USE"
287
+ },
288
+ "category": "Developer Tools"
289
+ }]
290
+ end
291
+ ' "$ROOT/.gemini-plugin/marketplace.json" > "$tmp"
292
+ mv "$tmp" "$ROOT/.gemini-plugin/marketplace.json"
293
+
294
+ if ! grep -Fq "./plugins/$PLUGIN/README.md" "$ROOT/README.md"; then
295
+ cat >> "$ROOT/README.md" <<EOF
296
+
297
+ - [\`$PLUGIN\`](./plugins/$PLUGIN/README.md) - Repository-local RedSkills extension.
298
+ EOF
299
+ fi
300
+
301
+ cat <<EOF
302
+ created plugins/$PLUGIN
303
+
304
+ validate with:
305
+ red-skills-content --check "$ROOT"
306
+
307
+ red-skills-content --check "$ROOT"
308
+ EOF