@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.
- package/bin/build-info.mjs +87 -0
- package/bin/cli-frontdoor.mjs +14 -0
- package/bin/red-skills-brain.mjs +17 -22
- package/bin/red-skills-content.mjs +17 -0
- package/bin/red-skills-hook.mjs +45 -0
- package/bin/red-skills-memory.mjs +17 -22
- package/bin/red-skills-redskilled-mcp.mjs +2 -0
- package/bin/red-skills-resource.mjs +21 -0
- package/bin/runtime-compatibility.mjs +17 -0
- package/bin/runtime-paths.mjs +13 -0
- package/dist/code-nav.bundle.min.mjs +1 -1
- package/dist/opencode-host.bundle.min.mjs +1 -1
- package/dist/redskilled-mcp.bundle.min.mjs +36 -36
- package/dist/redskilled-web.bundle.min.mjs +311 -0
- package/dist/redskilled.bundle.min.mjs +306 -292
- package/dist/release.bundle.min.mjs +1 -1
- package/dist/rsp-core.bundle.min.mjs +1 -1
- package/dist/rsp.bundle.min.mjs +1 -1
- package/dist/statusline.bundle.min.mjs +1 -1
- package/package.json +12 -4
- package/runtime/hook-routes.json +125 -0
- package/runtime/plugins/brain/scripts/bootstrap.mjs +656 -0
- package/runtime/plugins/brain/scripts/structural-smoke.sh +7 -0
- package/runtime/plugins/dev/hooks/branch-lock-codex.sh +105 -0
- package/runtime/plugins/dev/hooks/code-nav-mcp.sh +63 -0
- package/runtime/plugins/dev/hooks/command-guard.sh +893 -0
- package/runtime/plugins/dev/hooks/ensure-codex-statusline.mjs +144 -0
- package/runtime/plugins/dev/hooks/red-fetch.mjs +38 -0
- package/runtime/plugins/dev/hooks/redskilled-mcp.sh +64 -0
- package/runtime/plugins/dev/hooks/rsp-hook.sh +343 -0
- package/runtime/plugins/dev/hooks/tests/command-guard.test.sh +378 -0
- package/runtime/plugins/dev/hooks/tests/rsp-hook.test.sh +229 -0
- package/runtime/plugins/dev/scripts/memory-bridge.sh +230 -0
- package/runtime/plugins/dev/scripts/structural-smoke.sh +7 -0
- package/runtime/plugins/dev/scripts/tests/memory-bridge.test.sh +555 -0
- package/runtime/plugins/dev/skills/engineering/afk/defaults/cargo-pre-worktree.sh +37 -0
- package/runtime/plugins/dev/skills/engineering/afk/defaults/envelope-post-attempt.sh +47 -0
- package/runtime/plugins/dev/skills/engineering/afk/defaults/gradle-pre-worktree.sh +45 -0
- package/runtime/plugins/dev/skills/engineering/afk/defaults/heartbeat-post-attempt.sh +43 -0
- package/runtime/plugins/dev/skills/engineering/afk/defaults/validation-post-merge.sh +77 -0
- package/runtime/plugins/dev/skills/engineering/afk/detectors/cargo.sh +27 -0
- package/runtime/plugins/dev/skills/engineering/afk/detectors/gradle.sh +38 -0
- package/runtime/plugins/dev/skills/engineering/afk/examples/only-mine.sh +32 -0
- package/runtime/plugins/dev/skills/engineering/afk/examples/red-afk-attempt-action.yml +53 -0
- package/runtime/plugins/dev/skills/engineering/afk/examples/rs-afk-attempt.yml +76 -0
- package/runtime/plugins/dev/skills/engineering/afk/hooks/red-cargo +38 -0
- package/runtime/plugins/dev/skills/engineering/afk/hooks/red-envelope +50 -0
- package/runtime/plugins/dev/skills/engineering/afk/hooks/red-gradle +42 -0
- package/runtime/plugins/dev/skills/engineering/afk/hooks/red-heartbeat +46 -0
- package/runtime/plugins/dev/skills/engineering/afk/hooks/red-validation +79 -0
- package/runtime/plugins/dev/skills/engineering/diagnose/scripts/hitl-loop.template.sh +41 -0
- package/runtime/plugins/dev/skills/engineering/red-setup/scripts/install-runtime-shim.sh +181 -0
- package/runtime/plugins/dev/skills/engineering/red-setup/workflows/red-issues-needs-triage.yml +36 -0
- package/runtime/plugins/dev/skills/engineering/wizard/template.sh +211 -0
- package/runtime/plugins/dev/skills/engineering/zoom-out/scripts/graph-mode-plumbing.sh +36 -0
- package/runtime/plugins/dev/skills/engineering/zoom-out/scripts/tests/contract.test.sh +168 -0
- package/runtime/plugins/dev/skills/misc/branch-lock/scripts/branch-lock-hook.sh +113 -0
- package/runtime/plugins/dev/skills/misc/branch-lock/scripts/branch-lock-session-start.sh +81 -0
- package/runtime/plugins/dev/skills/misc/branch-lock/scripts/branch-lock.sh +68 -0
- package/runtime/plugins/dev/skills/misc/branch-lock/scripts/lib/dev-config.sh +126 -0
- package/runtime/plugins/dev/skills/misc/branch-lock/scripts/lib/git-command-classifier.sh +198 -0
- package/runtime/plugins/dev/skills/misc/branch-lock/scripts/lib/lock-store.sh +67 -0
- package/runtime/plugins/dev/skills/misc/branch-lock/scripts/lib/scope-resolver.sh +43 -0
- package/runtime/plugins/dev/skills/misc/branch-lock/scripts/tests/branch-lock-cli.test.sh +114 -0
- package/runtime/plugins/dev/skills/misc/branch-lock/scripts/tests/claude-plugin-hook.test.sh +117 -0
- package/runtime/plugins/dev/skills/misc/branch-lock/scripts/tests/codex-hook.test.sh +230 -0
- package/runtime/plugins/dev/skills/misc/branch-lock/scripts/tests/dev-config.test.sh +99 -0
- package/runtime/plugins/dev/skills/misc/branch-lock/scripts/tests/git-command-classifier.test.sh +187 -0
- package/runtime/plugins/dev/skills/misc/branch-lock/scripts/tests/lock-store.test.sh +90 -0
- package/runtime/plugins/dev/skills/misc/branch-lock/scripts/tests/scope-resolver.test.sh +63 -0
- package/runtime/plugins/dev/skills/misc/branch-lock/scripts/tests/session-start.test.sh +123 -0
- package/runtime/plugins/dev/skills/misc/git-guardrails-claude-code/scripts/block-dangerous-git.sh +231 -0
- package/runtime/plugins/dev/skills/misc/git-guardrails-claude-code/scripts/tests/block-dangerous-git.test.sh +143 -0
- package/runtime/plugins/internal/scripts/structural-smoke.sh +7 -0
- package/runtime/plugins/internal/skills/maintainer/create-plugin/scripts/create-plugin.sh +308 -0
- package/runtime/plugins/memory/scripts/bootstrap.mjs +659 -0
- package/runtime/plugins/memory/scripts/structural-smoke.sh +7 -0
- package/scripts/check-skills-boundary.mjs +32 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# lib/scope-resolver.sh — the Scope Resolver: decides whether the branch lock is
|
|
3
|
+
# enforced for a given git checkout, by toplevel location alone. Pure /
|
|
4
|
+
# explicit-args, like lib/lock-store.sh.
|
|
5
|
+
#
|
|
6
|
+
# The lock is defined as "the primary checkout's branch". /afk does its work in
|
|
7
|
+
# isolated worktrees under registered .red/tmp worktree lanes, which must never
|
|
8
|
+
# be strangled by an interactive session's lock. The robust discriminator is
|
|
9
|
+
# *where the checkout lives*, not its branch name or an env flag (a branch can be
|
|
10
|
+
# named anything; an env flag relies on every caller setting it). So: a toplevel
|
|
11
|
+
# under .red/tmp/worktrees/<lane>/<slug> or the legacy .red/tmp/work-*/ shape is
|
|
12
|
+
# a worktree (exempt); anything else is the primary checkout (enforced).
|
|
13
|
+
#
|
|
14
|
+
# Public surface:
|
|
15
|
+
# scope_is_worktree <toplevel>
|
|
16
|
+
# Returns 0 when <toplevel> is (or is nested under) a registered .red/tmp
|
|
17
|
+
# worktree lane or legacy .red/tmp/work-* directory. Returns 1 otherwise.
|
|
18
|
+
# No output.
|
|
19
|
+
#
|
|
20
|
+
# scope_should_enforce <toplevel>
|
|
21
|
+
# The verdict the hook wants: returns 0 (enforce the lock) for the primary
|
|
22
|
+
# checkout, 1 (exempt) for a worktree. The exact inverse of
|
|
23
|
+
# scope_is_worktree. No output.
|
|
24
|
+
|
|
25
|
+
# scope_is_worktree <toplevel>
|
|
26
|
+
scope_is_worktree() {
|
|
27
|
+
local _top="$1"
|
|
28
|
+
case "$_top" in
|
|
29
|
+
*/.red/tmp/worktrees/*/*) return 0 ;; # registered worktree lane root
|
|
30
|
+
*/.red/tmp/worktrees/*/*/*) return 0 ;; # nested below registered worktree lane root
|
|
31
|
+
*/.red/tmp/work-*) return 0 ;; # the worktree root itself
|
|
32
|
+
*/.red/tmp/work-*/*) return 0 ;; # nested below a worktree root
|
|
33
|
+
*) return 1 ;;
|
|
34
|
+
esac
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
# scope_should_enforce <toplevel>
|
|
38
|
+
scope_should_enforce() {
|
|
39
|
+
if scope_is_worktree "$1"; then
|
|
40
|
+
return 1
|
|
41
|
+
fi
|
|
42
|
+
return 0
|
|
43
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Unit test for branch-lock.sh — the /branch-lock CLI (issue #63).
|
|
3
|
+
#
|
|
4
|
+
# Covers the atomic relock-then-switch contract of `set`:
|
|
5
|
+
# AC1: `set <new>` from a different branch updates the lock AND lands the
|
|
6
|
+
# working tree on <new> in one step.
|
|
7
|
+
# AC2: the CLI still lands the branch even though the untouchable-primary rule
|
|
8
|
+
# (ADR 0083) blocks every raw agent `git switch` — proven by showing
|
|
9
|
+
# (a) the PreToolUse hook BLOCKS a raw `git switch <new>`, yet (b)
|
|
10
|
+
# `set <new>` succeeds anyway, because the CLI performs the switch itself
|
|
11
|
+
# (a subprocess, not an agent Bash-tool call the hook can intercept).
|
|
12
|
+
# AC3: locking to the branch already checked out just rewrites the target
|
|
13
|
+
# (no switch needed, exit 0).
|
|
14
|
+
#
|
|
15
|
+
# End-to-end against throwaway git repos: branch-lock.sh runs main() on every
|
|
16
|
+
# invocation, so it cannot be sourced for pure helpers — drive it as a subprocess.
|
|
17
|
+
|
|
18
|
+
set -uo pipefail
|
|
19
|
+
|
|
20
|
+
HERE="$(cd "$(dirname "$0")" && pwd)"
|
|
21
|
+
SCRIPTS="$(dirname "$HERE")"
|
|
22
|
+
CLI="$SCRIPTS/branch-lock.sh"
|
|
23
|
+
HOOK="$SCRIPTS/branch-lock-hook.sh"
|
|
24
|
+
|
|
25
|
+
pass=0
|
|
26
|
+
fail=0
|
|
27
|
+
|
|
28
|
+
expect_eq() {
|
|
29
|
+
local label="$1" expected="$2" actual="$3"
|
|
30
|
+
if [[ "$expected" == "$actual" ]]; then
|
|
31
|
+
echo "PASS $label"; pass=$((pass + 1))
|
|
32
|
+
else
|
|
33
|
+
printf 'FAIL %s\n expected: %q\n actual: %q\n' "$label" "$expected" "$actual"
|
|
34
|
+
fail=$((fail + 1))
|
|
35
|
+
fi
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
expect_contains() {
|
|
39
|
+
local label="$1" needle="$2" haystack="$3"
|
|
40
|
+
if [[ "$haystack" == *"$needle"* ]]; then
|
|
41
|
+
echo "PASS $label"; pass=$((pass + 1))
|
|
42
|
+
else
|
|
43
|
+
printf 'FAIL %s\n missing: %q\n in: %q\n' "$label" "$needle" "$haystack"
|
|
44
|
+
fail=$((fail + 1))
|
|
45
|
+
fi
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
# A throwaway repo with two branches, left checked out on `main`. The dev plugin
|
|
49
|
+
# is opted in (ADR 0067 gate) so the PreToolUse hook is armed for run_hook.
|
|
50
|
+
mk_repo() {
|
|
51
|
+
local dir="$1"
|
|
52
|
+
git init -q -b main "$dir"
|
|
53
|
+
git -C "$dir" config user.email t@t.t
|
|
54
|
+
git -C "$dir" config user.name t
|
|
55
|
+
git -C "$dir" commit -q --allow-empty -m init
|
|
56
|
+
git -C "$dir" branch feature
|
|
57
|
+
mkdir -p "$dir/.red"
|
|
58
|
+
printf 'plugins:\n dev:\n enabled: true\n' > "$dir/.red/config.yaml"
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
current_branch() { git -C "$1" rev-parse --abbrev-ref HEAD; }
|
|
62
|
+
locked_branch() { cat "$1/.red/tmp/branch-lock.yaml" 2>/dev/null | tr -d '\n'; }
|
|
63
|
+
|
|
64
|
+
# Run the CLI from inside the repo (git operates on cwd) with the root pinned.
|
|
65
|
+
run_cli() { local root="$1"; shift; ( cd "$root" && CLAUDE_PROJECT_DIR="$root" bash "$CLI" "$@" ); }
|
|
66
|
+
# Run the PreToolUse hook against a Bash command; echoes the exit code.
|
|
67
|
+
run_hook() {
|
|
68
|
+
local root="$1" cmd="$2"
|
|
69
|
+
( cd "$root" && CLAUDE_PROJECT_DIR="$root" bash "$HOOK" \
|
|
70
|
+
<<<"{\"tool_input\":{\"command\":\"$cmd\"}}" ) >/dev/null 2>&1
|
|
71
|
+
echo $?
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
# ===========================================================================
|
|
75
|
+
# AC1 — set <new> from a different branch: lock moves AND tree lands on <new>.
|
|
76
|
+
# ===========================================================================
|
|
77
|
+
R1="$(mktemp -d)/red-skills"; mk_repo "$R1"
|
|
78
|
+
OUT1="$(run_cli "$R1" set feature)"; RC1=$?
|
|
79
|
+
expect_eq "AC1 set <new>: exit 0" "0" "$RC1"
|
|
80
|
+
expect_eq "AC1 set <new>: lock target rewritten" "feature" "$(locked_branch "$R1")"
|
|
81
|
+
expect_eq "AC1 set <new>: working tree on new branch" "feature" "$(current_branch "$R1")"
|
|
82
|
+
expect_contains "AC1 set <new>: reports the lock" "locked to 'feature'" "$OUT1"
|
|
83
|
+
|
|
84
|
+
# ===========================================================================
|
|
85
|
+
# AC2 — the intended switch is never blocked by the hook.
|
|
86
|
+
# Lock to main first; a raw `git switch feature` is then hook-blocked, but the
|
|
87
|
+
# CLI's `set feature` still succeeds (it relocks first, then switches).
|
|
88
|
+
# ===========================================================================
|
|
89
|
+
R2="$(mktemp -d)/red-skills"; mk_repo "$R2"
|
|
90
|
+
run_cli "$R2" set main >/dev/null
|
|
91
|
+
expect_eq "AC2 raw switch to <new> is hook-blocked while locked to old" \
|
|
92
|
+
"2" "$(run_hook "$R2" "git switch feature")"
|
|
93
|
+
OUT2="$(run_cli "$R2" set feature)"; RC2=$?
|
|
94
|
+
expect_eq "AC2 CLI set <new>: exit 0 despite the old lock" "0" "$RC2"
|
|
95
|
+
expect_eq "AC2 CLI set <new>: lock now on new branch" "feature" "$(locked_branch "$R2")"
|
|
96
|
+
expect_eq "AC2 CLI set <new>: tree landed on new branch" "feature" "$(current_branch "$R2")"
|
|
97
|
+
# Untouchable primary (ADR 0083): a raw agent switch to the new target is still
|
|
98
|
+
# refused — the primary is never movable by the agent, even back to the lock
|
|
99
|
+
# target. The CLI succeeds because it switches itself, outside the hook's reach.
|
|
100
|
+
expect_eq "AC2 post-relock: hook still blocks raw switch to the new target" \
|
|
101
|
+
"2" "$(run_hook "$R2" "git switch feature")"
|
|
102
|
+
|
|
103
|
+
# ===========================================================================
|
|
104
|
+
# AC3 — locking to the branch already checked out just rewrites the target.
|
|
105
|
+
# ===========================================================================
|
|
106
|
+
R3="$(mktemp -d)/red-skills"; mk_repo "$R3" # on main, unlocked
|
|
107
|
+
OUT3="$(run_cli "$R3" set main)"; RC3=$?
|
|
108
|
+
expect_eq "AC3 set <current>: exit 0" "0" "$RC3"
|
|
109
|
+
expect_eq "AC3 set <current>: lock target written" "main" "$(locked_branch "$R3")"
|
|
110
|
+
expect_eq "AC3 set <current>: still on the same branch" "main" "$(current_branch "$R3")"
|
|
111
|
+
|
|
112
|
+
echo
|
|
113
|
+
echo "summary: $pass passed, $fail failed"
|
|
114
|
+
[ "$fail" -eq 0 ]
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Unit test for the Claude plugin PreToolUse(Bash) branch guard wiring.
|
|
3
|
+
|
|
4
|
+
set -uo pipefail
|
|
5
|
+
|
|
6
|
+
HERE="$(cd "$(dirname "$0")" && pwd)"
|
|
7
|
+
PLUGIN_ROOT="$(cd "$HERE/../../../../.." && pwd)"
|
|
8
|
+
MANIFEST="$PLUGIN_ROOT/hooks/claude.hooks.json"
|
|
9
|
+
|
|
10
|
+
pass=0
|
|
11
|
+
fail=0
|
|
12
|
+
|
|
13
|
+
ok() { echo "PASS $1"; pass=$((pass + 1)); }
|
|
14
|
+
bad() { echo "FAIL $1"; fail=$((fail + 1)); }
|
|
15
|
+
|
|
16
|
+
expect_eq() {
|
|
17
|
+
local label="$1" expected="$2" actual="$3"
|
|
18
|
+
if [[ "$expected" == "$actual" ]]; then
|
|
19
|
+
ok "$label"
|
|
20
|
+
else
|
|
21
|
+
bad "$label"
|
|
22
|
+
printf ' expected: %q\n actual: %q\n' "$expected" "$actual"
|
|
23
|
+
fi
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
expect_contains() {
|
|
27
|
+
local label="$1" needle="$2" haystack="$3"
|
|
28
|
+
if [[ "$haystack" == *"$needle"* ]]; then
|
|
29
|
+
ok "$label"
|
|
30
|
+
else
|
|
31
|
+
bad "$label"
|
|
32
|
+
printf ' missing: %q\n in: %q\n' "$needle" "$haystack"
|
|
33
|
+
fi
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
tmp="$(mktemp -d -t branch-lock-claude.XXXXXX)"
|
|
37
|
+
trap 'rm -rf "$tmp"' EXIT
|
|
38
|
+
|
|
39
|
+
primary="$tmp/red-skills"
|
|
40
|
+
mkdir -p "$primary/.red"
|
|
41
|
+
# No lock file and no `dev.lock.primary-branch` toggle: the untouchable-primary
|
|
42
|
+
# rule (ADR 0083) must block branch movement anyway.
|
|
43
|
+
cat > "$primary/.red/config.yaml" <<'EOF'
|
|
44
|
+
plugins:
|
|
45
|
+
dev:
|
|
46
|
+
enabled: true
|
|
47
|
+
EOF
|
|
48
|
+
|
|
49
|
+
payload() {
|
|
50
|
+
local cmd="$1"
|
|
51
|
+
jq -nc --arg cmd "$cmd" '{hook_event_name:"PreToolUse", tool_input:{command:$cmd}}'
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
manifest_matcher="$(jq -r '.hooks.PreToolUse[0].matcher' "$MANIFEST")"
|
|
55
|
+
manifest_hook="$(jq -r '.hooks.PreToolUse[0].hooks[0].command' "$MANIFEST")"
|
|
56
|
+
expect_eq "manifest: matcher is Bash" "Bash" "$manifest_matcher"
|
|
57
|
+
expect_contains "manifest: wires branch-lock-hook.sh" "branch-lock-hook.sh" "$manifest_hook"
|
|
58
|
+
expect_contains "manifest: wrapper drains stdin before hook" 'cat >"$tmp"' "$manifest_hook"
|
|
59
|
+
|
|
60
|
+
out="$tmp/out"
|
|
61
|
+
err="$tmp/err"
|
|
62
|
+
CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT" CLAUDE_PROJECT_DIR="$primary" bash -lc "$manifest_hook" \
|
|
63
|
+
>"$out" 2>"$err" <<<"$(payload "git switch feature")"
|
|
64
|
+
rc=$?
|
|
65
|
+
expect_eq "primary guard: switch blocked with no lock and no config" "0" "$rc"
|
|
66
|
+
expect_contains "primary guard: error names ADR 0083" "ADR 0083" "$(<"$err")"
|
|
67
|
+
|
|
68
|
+
# issue #1024 — git reset (any form) and git stash (all subcommands) blocked,
|
|
69
|
+
# and the refusal explains the reason + the sanctioned worktree alternative.
|
|
70
|
+
CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT" CLAUDE_PROJECT_DIR="$primary" bash -lc "$manifest_hook" \
|
|
71
|
+
>"$out" 2>"$err" <<<"$(payload "git reset --hard")"
|
|
72
|
+
rc=$?
|
|
73
|
+
expect_eq "primary guard: reset --hard is blocked when flag is on" "0" "$rc"
|
|
74
|
+
expect_contains "primary guard: reset refusal points to worktree" ".red/tmp/work-" "$(<"$err")"
|
|
75
|
+
|
|
76
|
+
CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT" CLAUDE_PROJECT_DIR="$primary" bash -lc "$manifest_hook" \
|
|
77
|
+
>"$out" 2>"$err" <<<"$(payload "git stash")"
|
|
78
|
+
rc=$?
|
|
79
|
+
expect_eq "primary guard: stash is blocked when flag is on" "0" "$rc"
|
|
80
|
+
|
|
81
|
+
CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT" CLAUDE_PROJECT_DIR="$primary" bash -lc "$manifest_hook" \
|
|
82
|
+
>"$out" 2>"$err" <<<"$(payload "git rebase --autostash main")"
|
|
83
|
+
rc=$?
|
|
84
|
+
expect_eq "primary guard: rebase --autostash is blocked when flag is on" "0" "$rc"
|
|
85
|
+
|
|
86
|
+
CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT" CLAUDE_PROJECT_DIR="$primary" bash -lc "$manifest_hook" \
|
|
87
|
+
>"$out" 2>"$err" <<<"$(payload "git commit -m wip")"
|
|
88
|
+
rc=$?
|
|
89
|
+
expect_eq "primary guard: commit is allowed" "0" "$rc"
|
|
90
|
+
|
|
91
|
+
# The legacy toggle stays readable but can no longer *enable* switching: with it
|
|
92
|
+
# explicitly false the guard still blocks, because primary movement is
|
|
93
|
+
# unconditional (ADR 0083 §2).
|
|
94
|
+
cat > "$primary/.red/config.yaml" <<'EOF'
|
|
95
|
+
plugins:
|
|
96
|
+
dev:
|
|
97
|
+
enabled: true
|
|
98
|
+
lock:
|
|
99
|
+
primary-branch: false
|
|
100
|
+
EOF
|
|
101
|
+
CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT" CLAUDE_PROJECT_DIR="$primary" bash -lc "$manifest_hook" \
|
|
102
|
+
>"$out" 2>"$err" <<<"$(payload "git switch feature")"
|
|
103
|
+
rc=$?
|
|
104
|
+
expect_eq "primary guard: legacy toggle off still blocks switch" "0" "$rc"
|
|
105
|
+
expect_contains "primary guard: legacy-off error names ADR 0083" "ADR 0083" "$(<"$err")"
|
|
106
|
+
|
|
107
|
+
missing_root="$tmp/missing-plugin-root"
|
|
108
|
+
mkdir -p "$missing_root"
|
|
109
|
+
CLAUDE_PLUGIN_ROOT="$missing_root" CLAUDE_PROJECT_DIR="$primary" bash -lc "$manifest_hook" \
|
|
110
|
+
>"$out" 2>"$err" <<<"$(payload "git switch feature")"
|
|
111
|
+
rc=$?
|
|
112
|
+
expect_eq "manifest: missing hook fails open" "0" "$rc"
|
|
113
|
+
expect_eq "manifest: missing hook prints empty JSON" "{}" "$(<"$out")"
|
|
114
|
+
|
|
115
|
+
echo
|
|
116
|
+
echo "summary: $pass passed, $fail failed"
|
|
117
|
+
[[ "$fail" -eq 0 ]]
|
|
@@ -0,0 +1,230 @@
|
|
|
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
|
+
expect_contains "manifest: invokes bash-dependent hook explicitly" 'bash "$hook"' "$manifest_hook"
|
|
68
|
+
|
|
69
|
+
out="$tmp/manifest-out"
|
|
70
|
+
err="$tmp/manifest-err"
|
|
71
|
+
CODEX_PLUGIN_ROOT="$PLUGIN_ROOT" bash -lc "$manifest_hook" >"$out" 2>"$err" \
|
|
72
|
+
<<<"$(payload "$primary" "git status")"
|
|
73
|
+
rc=$?
|
|
74
|
+
expect_eq "manifest: command executes through shell" "0" "$rc"
|
|
75
|
+
expect_eq "manifest: command prints empty JSON" "{}" "$(<"$out")"
|
|
76
|
+
|
|
77
|
+
missing_root="$tmp/missing-plugin-root"
|
|
78
|
+
mkdir -p "$missing_root"
|
|
79
|
+
CODEX_PLUGIN_ROOT="$missing_root" bash -lc "$manifest_hook" >"$out" 2>"$err" \
|
|
80
|
+
<<<"$(payload "$primary" "git switch main")"
|
|
81
|
+
rc=$?
|
|
82
|
+
expect_eq "manifest: missing hook fails open" "0" "$rc"
|
|
83
|
+
expect_eq "manifest: missing hook prints empty JSON" "{}" "$(<"$out")"
|
|
84
|
+
|
|
85
|
+
early_root="$tmp/early-plugin-root"
|
|
86
|
+
mkdir -p "$early_root/hooks"
|
|
87
|
+
cat > "$early_root/hooks/branch-lock-codex.sh" <<'EOF'
|
|
88
|
+
#!/usr/bin/env sh
|
|
89
|
+
printf '{}'
|
|
90
|
+
exit 0
|
|
91
|
+
EOF
|
|
92
|
+
chmod +x "$early_root/hooks/branch-lock-codex.sh"
|
|
93
|
+
set -o pipefail
|
|
94
|
+
(head -c 1048576 /dev/zero | tr '\0' x) \
|
|
95
|
+
| CODEX_PLUGIN_ROOT="$early_root" bash -lc "$manifest_hook" >"$out" 2>"$err"
|
|
96
|
+
rc=$?
|
|
97
|
+
set +o pipefail
|
|
98
|
+
expect_eq "manifest: early hook still drains Codex stdin" "0" "$rc"
|
|
99
|
+
expect_eq "manifest: early hook prints empty JSON" "{}" "$(<"$out")"
|
|
100
|
+
|
|
101
|
+
CODEX_PLUGIN_ROOT="$missing_root" "$HOOK" >"$out" 2>"$err" \
|
|
102
|
+
<<<"$(payload "$primary" "git switch main")"
|
|
103
|
+
rc=$?
|
|
104
|
+
expect_eq "hook: incomplete plugin root fails open" "0" "$rc"
|
|
105
|
+
expect_eq "hook: incomplete plugin root prints empty JSON" "{}" "$(<"$out")"
|
|
106
|
+
|
|
107
|
+
# Branch movement in the primary is now blocked unconditionally by the
|
|
108
|
+
# untouchable-primary rule (ADR 0083) — it fires before the branch-lock path, so
|
|
109
|
+
# switching away, and even switching *back* to the locked branch, is refused.
|
|
110
|
+
result="$(run_hook "$primary" "$(payload "$primary" "git switch feature")")"
|
|
111
|
+
rc="$(sed -n '1p' <<<"$result")"
|
|
112
|
+
stderr="$(sed -n '/---stderr---/,$p' <<<"$result")"
|
|
113
|
+
expect_eq "locked: switch away is blocked" "0" "$rc"
|
|
114
|
+
expect_contains "locked: error names ADR 0083" "ADR 0083" "$stderr"
|
|
115
|
+
|
|
116
|
+
result="$(run_hook "$primary" "$(payload "$primary" "git switch main")")"
|
|
117
|
+
rc="$(sed -n '1p' <<<"$result")"
|
|
118
|
+
stderr="$(sed -n '/---stderr---/,$p' <<<"$result")"
|
|
119
|
+
expect_eq "locked: switch back is also blocked (untouchable primary)" "0" "$rc"
|
|
120
|
+
expect_contains "locked: switch-back error names ADR 0083" "ADR 0083" "$stderr"
|
|
121
|
+
|
|
122
|
+
# Work-loss commands are caught by the unconditional untouchable-primary guard
|
|
123
|
+
# (#1024 reset/stash family + #1025 unconditional structure) even while a lock
|
|
124
|
+
# is active — the guard runs before the lock read, so the refusal names
|
|
125
|
+
# ADR 0083, not the branch lock.
|
|
126
|
+
result="$(run_hook "$primary" "$(payload "$primary" "git reset --hard HEAD~1")")"
|
|
127
|
+
rc="$(sed -n '1p' <<<"$result")"
|
|
128
|
+
stderr="$(sed -n '/---stderr---/,$p' <<<"$result")"
|
|
129
|
+
expect_eq "locked: work-loss reset --hard is blocked" "0" "$rc"
|
|
130
|
+
expect_contains "locked: work-loss error names untouchable primary" "ADR 0083" "$stderr"
|
|
131
|
+
|
|
132
|
+
rm -f "$primary/.red/tmp/branch-lock.yaml"
|
|
133
|
+
result="$(run_hook "$primary" "$(payload "$primary" "git switch feature")")"
|
|
134
|
+
rc="$(sed -n '1p' <<<"$result")"
|
|
135
|
+
stderr="$(sed -n '/---stderr---/,$p' <<<"$result")"
|
|
136
|
+
expect_eq "unlocked: switch is still blocked (untouchable primary)" "0" "$rc"
|
|
137
|
+
expect_contains "unlocked: error names ADR 0083" "ADR 0083" "$stderr"
|
|
138
|
+
|
|
139
|
+
# With no lock file the untouchable-primary guard still blocks every branch
|
|
140
|
+
# move, regardless of the legacy `dev.lock.primary-branch` toggle value.
|
|
141
|
+
mkdir -p "$primary/.red"
|
|
142
|
+
cat > "$primary/.red/config.yaml" <<'EOF'
|
|
143
|
+
plugins:
|
|
144
|
+
dev:
|
|
145
|
+
enabled: true
|
|
146
|
+
EOF
|
|
147
|
+
|
|
148
|
+
result="$(run_hook "$primary" "$(payload "$primary" "git switch feature")")"
|
|
149
|
+
rc="$(sed -n '1p' <<<"$result")"
|
|
150
|
+
stderr="$(sed -n '/---stderr---/,$p' <<<"$result")"
|
|
151
|
+
expect_eq "primary guard: switch blocked with no lock and no config" "0" "$rc"
|
|
152
|
+
expect_contains "primary guard: error names ADR 0083" "ADR 0083" "$stderr"
|
|
153
|
+
|
|
154
|
+
result="$(run_hook "$primary" "$(payload "$primary" "git checkout feature")")"
|
|
155
|
+
rc="$(sed -n '1p' <<<"$result")"
|
|
156
|
+
expect_eq "primary guard: checkout branch is blocked" "0" "$rc"
|
|
157
|
+
|
|
158
|
+
# issue #1024 — git reset (any form) and git stash (all subcommands) blocked,
|
|
159
|
+
# and the refusal explains the reason + the sanctioned worktree alternative.
|
|
160
|
+
result="$(run_hook "$primary" "$(payload "$primary" "git reset --hard")")"
|
|
161
|
+
rc="$(sed -n '1p' <<<"$result")"
|
|
162
|
+
stderr="$(sed -n '/---stderr---/,$p' <<<"$result")"
|
|
163
|
+
expect_eq "primary guard: reset --hard is blocked when flag is on" "0" "$rc"
|
|
164
|
+
expect_contains "primary guard: reset refusal points to worktree" ".red/tmp/work-" "$stderr"
|
|
165
|
+
|
|
166
|
+
result="$(run_hook "$primary" "$(payload "$primary" "git reset --soft HEAD~1")")"
|
|
167
|
+
rc="$(sed -n '1p' <<<"$result")"
|
|
168
|
+
expect_eq "primary guard: reset --soft is blocked when flag is on" "0" "$rc"
|
|
169
|
+
|
|
170
|
+
result="$(run_hook "$primary" "$(payload "$primary" "git stash")")"
|
|
171
|
+
rc="$(sed -n '1p' <<<"$result")"
|
|
172
|
+
expect_eq "primary guard: stash is blocked when flag is on" "0" "$rc"
|
|
173
|
+
|
|
174
|
+
result="$(run_hook "$primary" "$(payload "$primary" "git rebase --autostash main")")"
|
|
175
|
+
rc="$(sed -n '1p' <<<"$result")"
|
|
176
|
+
expect_eq "primary guard: rebase --autostash is blocked when flag is on" "0" "$rc"
|
|
177
|
+
|
|
178
|
+
result="$(run_hook "$primary" "$(payload "$primary" "git switch -b new")")"
|
|
179
|
+
rc="$(sed -n '1p' <<<"$result")"
|
|
180
|
+
expect_eq "primary guard: switch -b is blocked" "0" "$rc"
|
|
181
|
+
|
|
182
|
+
result="$(run_hook "$primary" "$(payload "$primary" "git commit -m wip")")"
|
|
183
|
+
rc="$(sed -n '1p' <<<"$result")"
|
|
184
|
+
expect_eq "primary guard: commit is allowed" "0" "$rc"
|
|
185
|
+
|
|
186
|
+
result="$(run_hook "$primary" "$(payload "$primary" "git worktree add ../wt feature")")"
|
|
187
|
+
rc="$(sed -n '1p' <<<"$result")"
|
|
188
|
+
expect_eq "primary guard: worktree add is allowed" "0" "$rc"
|
|
189
|
+
|
|
190
|
+
result="$(run_hook "$primary" "$(payload "$primary" "git status")")"
|
|
191
|
+
rc="$(sed -n '1p' <<<"$result")"
|
|
192
|
+
expect_eq "primary guard: read-only git is allowed" "0" "$rc"
|
|
193
|
+
|
|
194
|
+
# The legacy toggle is still readable but can no longer *enable* switching:
|
|
195
|
+
# explicitly false must not re-open primary branch movement (ADR 0083 §2).
|
|
196
|
+
cat > "$primary/.red/config.yaml" <<'EOF'
|
|
197
|
+
plugins:
|
|
198
|
+
dev:
|
|
199
|
+
enabled: true
|
|
200
|
+
lock:
|
|
201
|
+
primary-branch: false
|
|
202
|
+
EOF
|
|
203
|
+
result="$(run_hook "$primary" "$(payload "$primary" "git switch feature")")"
|
|
204
|
+
rc="$(sed -n '1p' <<<"$result")"
|
|
205
|
+
expect_eq "primary guard: legacy toggle off still blocks switch" "0" "$rc"
|
|
206
|
+
|
|
207
|
+
worktree="$primary/.red/tmp/work-wAAAA-i1/worktree"
|
|
208
|
+
mkdir -p "$worktree" "$primary/.red/tmp"
|
|
209
|
+
printf 'main\n' > "$primary/.red/tmp/branch-lock.yaml"
|
|
210
|
+
result="$(run_hook "$worktree" "$(payload "$worktree" "git switch feature")")"
|
|
211
|
+
rc="$(sed -n '1p' <<<"$result")"
|
|
212
|
+
expect_eq "worktree: branch lock is exempt" "0" "$rc"
|
|
213
|
+
|
|
214
|
+
mkdir -p "$worktree/.red"
|
|
215
|
+
cat > "$worktree/.red/config.yaml" <<'EOF'
|
|
216
|
+
dev:
|
|
217
|
+
lock:
|
|
218
|
+
primary-branch: true
|
|
219
|
+
EOF
|
|
220
|
+
result="$(run_hook "$worktree" "$(payload "$worktree" "git switch feature")")"
|
|
221
|
+
rc="$(sed -n '1p' <<<"$result")"
|
|
222
|
+
expect_eq "worktree: primary guard is exempt" "0" "$rc"
|
|
223
|
+
|
|
224
|
+
result="$(run_hook "$primary" "$(jq -nc --arg cwd "$primary" '{hook_event_name:"PreToolUse", cwd:$cwd, tool_input:{file:"x"}}')")"
|
|
225
|
+
rc="$(sed -n '1p' <<<"$result")"
|
|
226
|
+
expect_eq "non-shell payload: no-op allowed" "0" "$rc"
|
|
227
|
+
|
|
228
|
+
echo
|
|
229
|
+
echo "summary: $pass passed, $fail failed"
|
|
230
|
+
[[ "$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 ]]
|