@reddb-io/red-skills-dev 2.75.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +30 -0
  3. package/package.json +32 -0
  4. package/skills/engineering/README.md +36 -0
  5. package/skills/engineering/_report-runtime/WRAPPER.md +47 -0
  6. package/skills/engineering/afk/AGENT-PROMPT.md +295 -0
  7. package/skills/engineering/afk/SAFETY.md +103 -0
  8. package/skills/engineering/afk/SKILL.md +160 -0
  9. package/skills/engineering/afk/TROUBLESHOOTING.md +260 -0
  10. package/skills/engineering/afk/actions-lane.md +213 -0
  11. package/skills/engineering/afk/bin/README.md +29 -0
  12. package/skills/engineering/afk/bin/afk.mjs +51 -0
  13. package/skills/engineering/afk/defaults/cargo-pre-worktree.sh +37 -0
  14. package/skills/engineering/afk/defaults/envelope-post-attempt.sh +47 -0
  15. package/skills/engineering/afk/defaults/gradle-pre-worktree.sh +42 -0
  16. package/skills/engineering/afk/defaults/heartbeat-post-attempt.sh +43 -0
  17. package/skills/engineering/afk/defaults/validation-post-merge.sh +77 -0
  18. package/skills/engineering/afk/detectors/README.md +72 -0
  19. package/skills/engineering/afk/detectors/cargo.sh +27 -0
  20. package/skills/engineering/afk/detectors/gradle.sh +35 -0
  21. package/skills/engineering/afk/docs/BOOT-SWEEPS.md +81 -0
  22. package/skills/engineering/afk/docs/CONFIG.md +238 -0
  23. package/skills/engineering/afk/docs/ENVELOPE.md +177 -0
  24. package/skills/engineering/afk/docs/HANDOFF.md +70 -0
  25. package/skills/engineering/afk/docs/LIVENESS.md +56 -0
  26. package/skills/engineering/afk/docs/OPERATIONS.md +409 -0
  27. package/skills/engineering/afk/examples/only-mine.sh +32 -0
  28. package/skills/engineering/afk/examples/red-afk-attempt-action.yml +53 -0
  29. package/skills/engineering/afk/examples/rs-afk-attempt.yml +76 -0
  30. package/skills/engineering/afk/fleet.md +127 -0
  31. package/skills/engineering/afk/hooks/red-cargo +44 -0
  32. package/skills/engineering/afk/hooks/red-envelope +50 -0
  33. package/skills/engineering/afk/hooks/red-gradle +50 -0
  34. package/skills/engineering/afk/hooks/red-heartbeat +46 -0
  35. package/skills/engineering/afk/hooks/red-validation +79 -0
  36. package/skills/engineering/afk/monitor.md +170 -0
  37. package/skills/engineering/afk/runner-claude-minimax.md +71 -0
  38. package/skills/engineering/afk/runner-claude.md +67 -0
  39. package/skills/engineering/afk/runner-codex.md +138 -0
  40. package/skills/engineering/afk/runner-hermes.md +40 -0
  41. package/skills/engineering/afk/runner-opencode.md +142 -0
  42. package/skills/engineering/ask-red/SKILL.md +137 -0
  43. package/skills/engineering/audit-skills/SKILL.md +46 -0
  44. package/skills/engineering/code-review/SKILL.md +106 -0
  45. package/skills/engineering/context/SKILL.md +142 -0
  46. package/skills/engineering/curate/SKILL.md +133 -0
  47. package/skills/engineering/daily-review/SKILL.md +68 -0
  48. package/skills/engineering/dashboard/SKILL.md +44 -0
  49. package/skills/engineering/diagnose/SKILL.md +155 -0
  50. package/skills/engineering/diagnose/scripts/hitl-loop.template.sh +41 -0
  51. package/skills/engineering/go/SKILL.md +144 -0
  52. package/skills/engineering/go/TROUBLESHOOTING.md +81 -0
  53. package/skills/engineering/ground-truth/SKILL.md +84 -0
  54. package/skills/engineering/hitl/SKILL.md +171 -0
  55. package/skills/engineering/hitl/TROUBLESHOOTING.md +76 -0
  56. package/skills/engineering/implement/SKILL.md +69 -0
  57. package/skills/engineering/improve-codebase-architecture/DEEPENING.md +37 -0
  58. package/skills/engineering/improve-codebase-architecture/INTERFACE-DESIGN.md +44 -0
  59. package/skills/engineering/improve-codebase-architecture/LANGUAGE.md +53 -0
  60. package/skills/engineering/improve-codebase-architecture/SKILL.md +81 -0
  61. package/skills/engineering/model-tier-policy/SKILL.md +83 -0
  62. package/skills/engineering/prototype/LOGIC.md +79 -0
  63. package/skills/engineering/prototype/SKILL.md +30 -0
  64. package/skills/engineering/prototype/UI.md +112 -0
  65. package/skills/engineering/red-doctor/APPLY.md +35 -0
  66. package/skills/engineering/red-doctor/SKILL.md +172 -0
  67. package/skills/engineering/red-gains/SKILL.md +49 -0
  68. package/skills/engineering/red-setup/INTERVIEW.md +266 -0
  69. package/skills/engineering/red-setup/ISSUE-SWEEP.md +18 -0
  70. package/skills/engineering/red-setup/REFERENCE.md +35 -0
  71. package/skills/engineering/red-setup/SKILL.md +58 -0
  72. package/skills/engineering/red-setup/WORKFLOWS.md +74 -0
  73. package/skills/engineering/red-setup/WRITE-CONTRACT.md +151 -0
  74. package/skills/engineering/red-setup/config-template.yaml +174 -0
  75. package/skills/engineering/red-setup/domain.md +52 -0
  76. package/skills/engineering/red-setup/issue-tracker-github.md +88 -0
  77. package/skills/engineering/red-setup/rsp-retirement-runbook.md +35 -0
  78. package/skills/engineering/red-setup/scripts/install-runtime-shim.sh +189 -0
  79. package/skills/engineering/red-setup/triage-labels.md +176 -0
  80. package/skills/engineering/red-setup/workflows/red-issues-needs-triage.yml +36 -0
  81. package/skills/engineering/red-statusline/HOST-NOTES.md +180 -0
  82. package/skills/engineering/red-statusline/SKILL.md +58 -0
  83. package/skills/engineering/report-bug/SKILL.md +155 -0
  84. package/skills/engineering/resolving-merge-conflicts/SKILL.md +143 -0
  85. package/skills/engineering/retake/SKILL.md +184 -0
  86. package/skills/engineering/review-adrs/SKILL.md +125 -0
  87. package/skills/engineering/start/ADR-FORMAT.md +47 -0
  88. package/skills/engineering/start/CONTEXT-FORMAT.md +77 -0
  89. package/skills/engineering/start/DOC-LANDING-FINALIZER.md +9 -0
  90. package/skills/engineering/start/SKILL.md +179 -0
  91. package/skills/engineering/tdd/SKILL.md +96 -0
  92. package/skills/engineering/tdd/deep-modules.md +33 -0
  93. package/skills/engineering/tdd/interface-design.md +31 -0
  94. package/skills/engineering/tdd/mocking.md +59 -0
  95. package/skills/engineering/tdd/tests.md +85 -0
  96. package/skills/engineering/to-spec/SKILL.md +104 -0
  97. package/skills/engineering/to-tickets/SKILL.md +158 -0
  98. package/skills/engineering/triage/AGENT-BRIEF.md +207 -0
  99. package/skills/engineering/triage/OUT-OF-SCOPE.md +101 -0
  100. package/skills/engineering/triage/SKILL.md +176 -0
  101. package/skills/engineering/verify/SKILL.md +136 -0
  102. package/skills/engineering/wayfinder/SKILL.md +128 -0
  103. package/skills/engineering/zoom-out/SKILL.md +51 -0
  104. package/skills/engineering/zoom-out/scripts/graph-mode-plumbing.sh +36 -0
  105. package/skills/engineering/zoom-out/scripts/tests/contract.test.sh +168 -0
  106. package/skills/knowledge/README.md +5 -0
  107. package/skills/knowledge/research/SKILL.md +76 -0
  108. package/skills/misc/README.md +8 -0
  109. package/skills/misc/branch-lock/SKILL.md +187 -0
  110. package/skills/misc/branch-lock/scripts/branch-lock-hook.sh +113 -0
  111. package/skills/misc/branch-lock/scripts/branch-lock-session-start.sh +81 -0
  112. package/skills/misc/branch-lock/scripts/branch-lock.sh +68 -0
  113. package/skills/misc/branch-lock/scripts/lib/dev-config.sh +126 -0
  114. package/skills/misc/branch-lock/scripts/lib/git-command-classifier.sh +198 -0
  115. package/skills/misc/branch-lock/scripts/lib/lock-store.sh +67 -0
  116. package/skills/misc/branch-lock/scripts/lib/scope-resolver.sh +43 -0
  117. package/skills/misc/branch-lock/scripts/tests/branch-lock-cli.test.sh +114 -0
  118. package/skills/misc/branch-lock/scripts/tests/claude-plugin-hook.test.sh +117 -0
  119. package/skills/misc/branch-lock/scripts/tests/codex-hook.test.sh +229 -0
  120. package/skills/misc/branch-lock/scripts/tests/dev-config.test.sh +99 -0
  121. package/skills/misc/branch-lock/scripts/tests/git-command-classifier.test.sh +187 -0
  122. package/skills/misc/branch-lock/scripts/tests/lock-store.test.sh +90 -0
  123. package/skills/misc/branch-lock/scripts/tests/scope-resolver.test.sh +63 -0
  124. package/skills/misc/branch-lock/scripts/tests/session-start.test.sh +123 -0
  125. package/skills/misc/git-guardrails-claude-code/SKILL.md +105 -0
  126. package/skills/misc/git-guardrails-claude-code/scripts/block-dangerous-git.sh +231 -0
  127. package/skills/misc/git-guardrails-claude-code/scripts/tests/block-dangerous-git.test.sh +143 -0
  128. package/skills/misc/migrate-to-shoehorn/SKILL.md +96 -0
  129. package/skills/misc/setup-pre-commit/SKILL.md +94 -0
  130. package/skills/productivity/README.md +8 -0
  131. package/skills/productivity/ff/SKILL.md +116 -0
  132. package/skills/productivity/handoff/SKILL.md +37 -0
  133. package/skills/productivity/reflect/SKILL.md +18 -0
  134. package/skills/productivity/write-a-skill/SKILL.md +190 -0
  135. package/skills/productivity/write-a-skill/WRITING-STYLE.md +62 -0
@@ -0,0 +1,187 @@
1
+ ---
2
+ name: branch-lock
3
+ description: Lock the agent to a branch and block it from switching away. Sets up self-contained pre-tool hooks that block the agent's git checkout/switch to any other branch while a lock is active, and gives you a /branch-lock command to set, change, and clear the lock. Use when you want the agent pinned to one branch for a session.
4
+ disable-model-invocation: true
5
+ ---
6
+
7
+ # Branch Lock
8
+
9
+ Pin the agent to one branch in the primary checkout and block it from switching away.
10
+
11
+ <what-to-do>
12
+
13
+ **Never hand-edit `branch-lock.yaml` — route every lock change through `scripts/branch-lock.sh` so the lock-store stays single-writer.**
14
+
15
+ ## Parse the request
16
+
17
+ The user invokes `/branch-lock <branch>` or `/branch-lock clear` (or just asks
18
+ to lock/unlock). Map it to one CLI action and run the bundled CLI:
19
+
20
+ - `/branch-lock <branch>` → `scripts/branch-lock.sh set <branch>`
21
+ - `/branch-lock` (no arg) → `scripts/branch-lock.sh set` (lock to the current branch)
22
+ - `/branch-lock clear` → `scripts/branch-lock.sh clear`
23
+ - "what's locked?" → `scripts/branch-lock.sh status`
24
+
25
+ The CLI does the **atomic relock-then-switch**: it rewrites the lock target
26
+ first, so the switch to that branch is itself "return to the lock target" and
27
+ the hook never blocks the very change the user asked for.
28
+
29
+ ## Install the hooks (first time only)
30
+
31
+ If the hooks are not yet wired in this repo, install them before relying on the lock:
32
+
33
+ ### Codex
34
+
35
+ The `dev` plugin ships Codex wiring in
36
+ `plugins/dev/.codex-plugin/plugin.json` (`"hooks": "./hooks/codex.hooks.json"`).
37
+ That manifest calls `plugins/dev/hooks/branch-lock-codex.sh`, which reads Codex
38
+ `PreToolUse` payloads and reuses the same `lock-store`, `scope-resolver`, and
39
+ `git-command-classifier` modules as the Claude hook.
40
+
41
+ Codex must load the updated `dev` plugin, and plugin hooks must be enabled.
42
+ Current Codex builds list `plugin_hooks` as stable/enabled; older builds may
43
+ need `[features].plugin_hooks = true` in `~/.codex/config.toml`. Once that is
44
+ true, no per-repo `.codex/` copy is needed: set the lock with
45
+ `scripts/branch-lock.sh set` or the `/branch-lock` skill command and the Codex
46
+ hook enforces it for shell-command tool calls. If Codex has plugin hooks
47
+ disabled, the lock file may exist but Codex will not enforce it.
48
+
49
+ ### Claude Code
50
+
51
+ The `dev` plugin ships Claude wiring in
52
+ `plugins/dev/.claude-plugin/plugin.json` (`"hooks": "./hooks/claude.hooks.json"`).
53
+ That manifest registers `branch-lock-hook.sh` under `PreToolUse`/matcher `Bash`
54
+ at the plugin level, so no per-repo `.claude/settings.json` copy is needed for
55
+ the primary-branch guard. The hook reads `.red/config.yaml` at runtime; once
56
+ `plugins.dev.enabled: true` it enforces the untouchable-primary rule and, when a
57
+ branch-lock file is present, the work-loss family too.
58
+
59
+ Manual per-repo installation is only needed for older/pluginless Claude setups:
60
+ copy [scripts/branch-lock-hook.sh](scripts/branch-lock-hook.sh),
61
+ [scripts/branch-lock-session-start.sh](scripts/branch-lock-session-start.sh),
62
+ and `scripts/lib/` together into `.claude/hooks/branch-lock/`, make the scripts
63
+ executable, and register `branch-lock-hook.sh` under `PreToolUse`/matcher `Bash`.
64
+ The optional SessionStart hook only injects an instruction asking whether to
65
+ lock; it never writes the lock itself.
66
+
67
+ ## DOs / DON'Ts
68
+
69
+ - ✅ Always route lock changes through `scripts/branch-lock.sh`.
70
+ - ✅ Confirm the resulting `status` to the user after `set`/`clear`.
71
+ - ❌ Never hand-edit `branch-lock.yaml` — route via CLI so the lock-store stays single-writer.
72
+ - ❌ Don't try to make the lock block the human terminal — out of scope by design (ADR 0006).
73
+
74
+ </what-to-do>
75
+
76
+ <supporting-info>
77
+
78
+ ## Why the lock exists
79
+
80
+ The lock is a local fact — `.red/state/branch-lock.yaml` whose content is the
81
+ branch name — under the gitignored state tier, so every checkout and machine
82
+ locks independently and nothing is committed. Legacy readers may still recognize
83
+ the old `.red/tmp/branch-lock.yaml` path during migration, but the canonical home
84
+ is the durable state tier. Absence of the file means unlocked: the branch-lock
85
+ layer is strictly opt-in.
86
+
87
+ Layered under it, the dev plugin ships an **unconditional** primary-checkout
88
+ branch guard (ADR 0083 §2, untouchable primary). Once the dev plugin is enabled
89
+ (`plugins.dev.enabled: true`), the pre-tool hook blocks the agent from changing
90
+ the primary checkout's branch — always, with no lock file and no config toggle.
91
+ The answer to "may an agent switch the primary checkout's branch" is not
92
+ configurable: it is always **no**. The historical `dev.lock.primary-branch` key
93
+ stays **readable** for backward compatibility but can no longer enable or disable
94
+ switching; the guard fires regardless of its value, and `/red-doctor` may note it as
95
+ redundant.
96
+
97
+ ```yaml
98
+ dev: { lock: { primary-branch: true } } # retained for backward compatibility only — no longer toggles the guard
99
+ ```
100
+
101
+ The same unconditional guard also blocks the work-destroying commands the
102
+ standing maintainer rules forbid in the primary checkout, because parallel human
103
+ WIP lives there and these have destroyed in-progress work before (issue #1024).
104
+ The refusal points to the sanctioned alternative — do branch work in an isolated
105
+ worktree under `.red/tmp/worktrees/manual/<slug>`, and if the local trunk
106
+ diverged from origin, leave it alone and base on the fresh remote ref (ADR 0083).
107
+ Inside isolated worktrees these commands remain allowed — the guard is about the
108
+ primary checkout only, and `/afk` and `/go` worktrees under their registered
109
+ `.red/tmp/` lanes are always exempt.
110
+
111
+ Enforcement is **agent-only**, via runner pre-tool hooks — Claude Code
112
+ `PreToolUse(Bash)` and Codex plugin `PreToolUse` — that intercept the agent's own
113
+ tool calls, not the human's terminal (ADR 0006, unaffected). See
114
+ [ADR 0006](../../../../../.red/adr/0006-branch-lock-agent-only-enforcement.md).
115
+ The hook logic is self-contained: it depends on neither the
116
+ `git-guardrails-claude-code` skill nor anything else, and the two stack
117
+ harmlessly if both are installed.
118
+
119
+ ## Layout
120
+
121
+ ```
122
+ branch-lock/
123
+ ├── SKILL.md
124
+ ├── ../../../hooks/
125
+ │ ├── codex.hooks.json ← Codex plugin hook manifest
126
+ │ └── branch-lock-codex.sh ← Codex PreToolUse hook
127
+ └── scripts/
128
+ ├── branch-lock.sh ← /branch-lock CLI (set | clear | status)
129
+ ├── branch-lock-hook.sh ← Claude PreToolUse(Bash) hook
130
+ ├── branch-lock-session-start.sh ← SessionStart hook (offers to lock at start)
131
+ ├── lib/
132
+ │ ├── dev-config.sh ← read dev.lock.primary-branch from .red/config.yaml
133
+ │ ├── lock-store.sh ← read/write/clear branch-lock.yaml
134
+ │ ├── scope-resolver.sh ← primary enforces, registered .red/tmp worktrees exempt
135
+ │ └── git-command-classifier.sh ← branch-switch + work-loss family = block
136
+ └── tests/
137
+ ├── lock-store.test.sh
138
+ ├── scope-resolver.test.sh
139
+ ├── git-command-classifier.test.sh
140
+ ├── dev-config.test.sh
141
+ ├── session-start.test.sh
142
+ ├── claude-plugin-hook.test.sh
143
+ ├── codex-hook.test.sh
144
+ └── branch-lock-cli.test.sh ← /branch-lock set: atomic relock-then-switch
145
+ ```
146
+
147
+ ## What the hook blocks vs allows
148
+
149
+ In the primary checkout, once `plugins.dev.enabled: true`, the hook blocks (the
150
+ first three are the unconditional untouchable-primary rule; the rest are the
151
+ work-loss family, blocked only while a branch-lock file is present):
152
+
153
+ - `git checkout <any>` / `git switch <any>` — switching to any branch, including
154
+ the lock target (untouchable primary — the agent can never move the primary)
155
+ - `git checkout -b <new>` / `git switch -c <new>` — leaving via a new branch
156
+ - `git switch -` — switching to the previous branch
157
+ - `git stash` / `git stash push` / `git stash save` — shelving away the working tree
158
+ - `git clean -f` (any force flag: `-fd`, `-xfd`, `--force`) — deleting untracked files
159
+ - `git reset --hard` — discarding the working tree
160
+ - `git checkout .` / `git checkout -- .` / `git restore .` — whole-tree restore
161
+
162
+ It allows (exit 0, silent):
163
+
164
+ - `git checkout -- <path>` / `git restore <path>` — targeted file-level restore
165
+ - `git stash list` / `git stash show` — read-only stash
166
+ - `git clean -n` / `--dry-run` — non-destructive clean
167
+ - `git reset --soft` / mixed reset, `git restore --staged <path>` — no working-tree loss
168
+ - `git worktree add .red/tmp/worktrees/manual/<slug>` — manual worktrees stay in the registered lane
169
+ - `git commit`, `git status` / read-only git, and any other command
170
+
171
+ The dev command proxy has a stricter host-wide invariant when
172
+ `plugins.dev.enabled: true`: any agent-created `git worktree add` destination
173
+ must resolve under a registered lane in the repo's `.red/tmp/`. Branch-lock
174
+ allows the command class; the dev proxy decides whether the destination path is
175
+ inside the repo-owned runtime area.
176
+
177
+ ## Tests
178
+
179
+ Pure-module unit tests, run directly with bash:
180
+
181
+ ```bash
182
+ for t in scripts/tests/*.test.sh; do bash "$t"; done
183
+ ```
184
+
185
+ Each prints `summary: N passed, 0 failed` and exits non-zero on any failure.
186
+
187
+ </supporting-info>
@@ -0,0 +1,113 @@
1
+ #!/usr/bin/env bash
2
+ # branch-lock-hook.sh — PreToolUse(Bash) hook that blocks the agent from
3
+ # switching away from the locked branch. Self-contained: composes the three
4
+ # pure modules (lock-store, scope-resolver, git-command-classifier) into one
5
+ # allow/block verdict, with no dependency on the git-guardrails skill.
6
+ #
7
+ # Reads the Claude Code PreToolUse payload on stdin, extracts the Bash command,
8
+ # and:
9
+ # 1. resolves the project root (CLAUDE_PROJECT_DIR, else the git toplevel);
10
+ # 2. exits 0 (allow) when the checkout is an /afk worktree — scope exemption;
11
+ # 3. exits 0 (allow) when no lock file is present — unlocked is the default;
12
+ # 4. classifies the command; on "block" prints a clear message to stderr and
13
+ # exits 2 (the Claude Code convention for "deny this tool call").
14
+ #
15
+ # Any verdict other than block is a silent exit 0, so the hook never adds noise
16
+ # to allowed commands.
17
+
18
+ set -uo pipefail
19
+
20
+ allow() {
21
+ printf '{}'
22
+ exit 0
23
+ }
24
+
25
+ deny() {
26
+ local reason="$1"
27
+ printf '%s\n' "$reason" >&2
28
+ jq -nc --arg reason "$reason" '{
29
+ decision: "block",
30
+ reason: $reason,
31
+ hookSpecificOutput: {
32
+ hookEventName: "PreToolUse",
33
+ permissionDecision: "deny",
34
+ permissionDecisionReason: $reason
35
+ }
36
+ }'
37
+ exit 0
38
+ }
39
+
40
+ HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
41
+ # shellcheck source=lib/lock-store.sh
42
+ source "$HERE/lib/lock-store.sh"
43
+ # shellcheck source=lib/scope-resolver.sh
44
+ source "$HERE/lib/scope-resolver.sh"
45
+ # shellcheck source=lib/git-command-classifier.sh
46
+ source "$HERE/lib/git-command-classifier.sh"
47
+ # shellcheck source=lib/dev-config.sh
48
+ source "$HERE/lib/dev-config.sh"
49
+
50
+ INPUT="$(timeout "${RED_SKILLS_HOOK_STDIN_TIMEOUT_S:-5s}" cat 2>/dev/null || true)"
51
+ COMMAND="$(jq -r '.tool_input.command // empty' <<<"$INPUT" 2>/dev/null)"
52
+ [[ -z "$COMMAND" ]] && allow
53
+
54
+ # Project root: prefer the harness-provided dir, fall back to the git toplevel.
55
+ ROOT="${CLAUDE_PROJECT_DIR:-}"
56
+ if [[ -z "$ROOT" ]]; then
57
+ ROOT="$(git rev-parse --show-toplevel 2>/dev/null || true)"
58
+ fi
59
+ [[ -z "$ROOT" ]] && allow
60
+
61
+ # Per-directory plugin gate (ADR 0067): the dev plugin's hooks are installed
62
+ # globally but must stay fully inert in any repo that did not opt in. Exit before
63
+ # any lock/scope work unless `plugins.dev.enabled: true` is set here.
64
+ dev_plugin_enabled "$ROOT/.red/config.yaml" || allow
65
+
66
+ # Scope: /afk worktrees are exempt even when a lock is active.
67
+ scope_should_enforce "$ROOT" || allow
68
+
69
+ # Untouchable primary (ADR 0083 §2): an agent may never move the primary
70
+ # checkout's branch. This block is unconditional — it no longer arms with the
71
+ # `dev.lock.primary-branch` toggle (which stays readable for backward
72
+ # compatibility but can no longer *enable* switching; /red-doctor may flag it as
73
+ # redundant). Human terminals are unaffected: this is an agent-only pre-tool hook
74
+ # (ADR 0006).
75
+ if [[ "$(classify_primary_branch_switch_guard "$COMMAND")" == "block" ]]; then
76
+ deny "$(cat <<EOF
77
+ BLOCKED by the untouchable-primary rule (ADR 0083): an agent can never switch
78
+ the primary checkout's branch or destroy work in it, regardless of
79
+ configuration or lock state. The command '$COMMAND' would move the agent's
80
+ primary checkout to another branch or destroy work in it (branch switch,
81
+ 'git reset' in any form, 'git stash', or 'git rebase --autostash'). Parallel
82
+ human WIP lives in this primary checkout, and these commands have destroyed
83
+ in-progress work before.
84
+
85
+ Do branch work in an isolated worktree under .red/tmp/worktrees/manual/<slug> instead
86
+ ('git worktree add .red/tmp/worktrees/manual/<slug> -b <branch> origin/main'). If the local
87
+ trunk diverged from origin, leave it alone and base on the fresh remote ref
88
+ (ADR 0083) — never reset or stash the primary to reconcile it.
89
+
90
+ Allowed in the primary checkout: git commit, git worktree add, read-only git,
91
+ and other non-destructive commands. To change the primary branch, ask the user.
92
+ EOF
93
+ )"
94
+ fi
95
+
96
+ LOCKFILE="$ROOT/.red/tmp/branch-lock.yaml"
97
+ LOCK_BRANCH="$(lock_store_read "$LOCKFILE")" || allow # absent => unlocked
98
+
99
+ if [[ "$(classify_git_command "$LOCK_BRANCH" "$COMMAND")" == "block" ]]; then
100
+ deny "$(cat <<EOF
101
+ BLOCKED by branch lock: this session is locked to '$LOCK_BRANCH'.
102
+ The command '$COMMAND' would switch the agent away from the locked branch or
103
+ discard working-tree changes (stash, clean -f, reset --hard, whole-tree restore).
104
+
105
+ Allowed while locked: switching back to '$LOCK_BRANCH', targeted file restore
106
+ ('git checkout -- <path>', 'git restore <path>'), read-only stash, dry-run clean,
107
+ soft/mixed reset, and 'git worktree add'. To change or release the lock, ask the
108
+ user — they drive it with '/branch-lock <branch>' or '/branch-lock clear'.
109
+ EOF
110
+ )"
111
+ fi
112
+
113
+ allow
@@ -0,0 +1,81 @@
1
+ #!/usr/bin/env bash
2
+ # branch-lock-session-start.sh — SessionStart hook that offers, once at session
3
+ # start, to lock the agent to the current branch. Self-contained: reuses the
4
+ # same two pure modules as the PreToolUse hook (lock-store, scope-resolver) so
5
+ # the prompt obeys the exact same scope rule as enforcement.
6
+ #
7
+ # On SessionStart it:
8
+ # 1. resolves the project root (CLAUDE_PROJECT_DIR, else the git toplevel);
9
+ # 2. stays silent (exit 0, no output) inside an /afk worktree — the lock
10
+ # protects the interactive primary checkout, never the autonomous loop;
11
+ # 3. stays silent when a lock is already present — nothing to offer;
12
+ # 4. stays silent on a detached HEAD — there is no branch to lock;
13
+ # 5. otherwise emits a SessionStart additionalContext block instructing the
14
+ # agent to ask the user whether to lock to the current branch before doing
15
+ # anything else.
16
+ #
17
+ # The hook never writes the lock itself: it only injects the instruction. The
18
+ # agent runs `branch-lock.sh set <branch>` (or the user's /branch-lock command)
19
+ # on a "yes", and leaves the repo unlocked on a "no".
20
+
21
+ set -uo pipefail
22
+
23
+ HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
24
+ # shellcheck source=lib/lock-store.sh
25
+ source "$HERE/lib/lock-store.sh"
26
+ # shellcheck source=lib/scope-resolver.sh
27
+ source "$HERE/lib/scope-resolver.sh"
28
+
29
+ # session_start_should_prompt <root> <lockfile> <branch>
30
+ # Returns 0 (prompt) only in the primary checkout, while unlocked, with a real
31
+ # branch checked out. Returns 1 (stay silent) otherwise. No output.
32
+ session_start_should_prompt() {
33
+ local _root="$1" _lockfile="$2" _branch="$3"
34
+ scope_should_enforce "$_root" || return 1 # worktree => exempt
35
+ lock_store_is_locked "$_lockfile" && return 1 # already locked => nothing to offer
36
+ [[ -z "$_branch" || "$_branch" == "HEAD" ]] && return 1 # detached / no branch
37
+ return 0
38
+ }
39
+
40
+ # session_start_prompt_text <branch>
41
+ # The instruction injected into the agent's context.
42
+ session_start_prompt_text() {
43
+ local _branch="$1"
44
+ cat <<EOF
45
+ branch-lock: this session is on branch '$_branch' in the primary checkout, and the branch is not locked. Before doing anything else, ask the user whether to lock the agent to '$_branch' for this session, showing the branch name. If they say yes, lock it with the /branch-lock command (or run scripts/branch-lock.sh set '$_branch'). If they say no, leave the repo unlocked and proceed. Do not lock without an explicit yes.
46
+ EOF
47
+ }
48
+
49
+ main() {
50
+ # Drain stdin (SessionStart sends a JSON payload we don't need to inspect).
51
+ cat >/dev/null 2>&1 || true
52
+
53
+ local root="${CLAUDE_PROJECT_DIR:-}"
54
+ if [[ -z "$root" ]]; then
55
+ root="$(git rev-parse --show-toplevel 2>/dev/null || true)"
56
+ fi
57
+ [[ -z "$root" ]] && exit 0
58
+
59
+ local lockfile="$root/.red/tmp/branch-lock.yaml"
60
+ local branch
61
+ branch="$(git -C "$root" rev-parse --abbrev-ref HEAD 2>/dev/null || true)"
62
+
63
+ session_start_should_prompt "$root" "$lockfile" "$branch" || exit 0
64
+
65
+ local text
66
+ text="$(session_start_prompt_text "$branch")"
67
+
68
+ if command -v jq >/dev/null 2>&1; then
69
+ jq -nc --arg ctx "$text" \
70
+ '{hookSpecificOutput: {hookEventName: "SessionStart", additionalContext: $ctx}}'
71
+ else
72
+ # Fallback: bare stdout is added to the session context too.
73
+ printf '%s\n' "$text"
74
+ fi
75
+ exit 0
76
+ }
77
+
78
+ # Run main only when executed directly, not when sourced by tests.
79
+ if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
80
+ main "$@"
81
+ fi
@@ -0,0 +1,68 @@
1
+ #!/usr/bin/env bash
2
+ # branch-lock.sh — the CLI behind the /branch-lock command. Sets, clears, and
3
+ # reports the branch lock, keeping lib/lock-store.sh the single writer of the
4
+ # lock file. The lock always lives at <root>/.red/tmp/branch-lock.yaml, where
5
+ # <root> is CLAUDE_PROJECT_DIR (set by the harness) or the git toplevel.
6
+ #
7
+ # Usage:
8
+ # branch-lock.sh set [branch] lock to <branch> (default: current branch),
9
+ # atomic relock-then-switch — rewrite the lock
10
+ # target first so the move "back to the lock
11
+ # target" is itself allowed by the hook, then
12
+ # switch the working tree if not already there.
13
+ # branch-lock.sh clear release the lock.
14
+ # branch-lock.sh status print the locked branch, or "unlocked".
15
+
16
+ set -uo pipefail
17
+
18
+ HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
19
+ # shellcheck source=lib/lock-store.sh
20
+ source "$HERE/lib/lock-store.sh"
21
+
22
+ resolve_root() {
23
+ local root="${CLAUDE_PROJECT_DIR:-}"
24
+ [[ -z "$root" ]] && root="$(git rev-parse --show-toplevel 2>/dev/null || true)"
25
+ [[ -z "$root" ]] && { echo "branch-lock: not inside a git repo and CLAUDE_PROJECT_DIR unset" >&2; return 1; }
26
+ printf '%s' "$root"
27
+ }
28
+
29
+ main() {
30
+ local action="${1:-status}"
31
+ local root lockfile
32
+ root="$(resolve_root)" || return 1
33
+ lockfile="$root/.red/tmp/branch-lock.yaml"
34
+
35
+ case "$action" in
36
+ set)
37
+ local branch="${2:-}"
38
+ [[ -z "$branch" ]] && branch="$(git rev-parse --abbrev-ref HEAD 2>/dev/null)"
39
+ [[ -z "$branch" ]] && { echo "branch-lock: no branch given and HEAD is detached" >&2; return 1; }
40
+ # Atomic relock-then-switch: rewrite the target before moving, so the
41
+ # subsequent switch is "return to the lock target" and the hook allows it.
42
+ lock_store_write "$lockfile" "$branch" || return 1
43
+ local current; current="$(git rev-parse --abbrev-ref HEAD 2>/dev/null || true)"
44
+ if [[ -n "$current" && "$current" != "$branch" ]]; then
45
+ git switch "$branch" || { echo "branch-lock: locked to '$branch' but switch failed (resolve manually)" >&2; return 1; }
46
+ fi
47
+ echo "branch-lock: locked to '$branch'"
48
+ ;;
49
+ clear)
50
+ lock_store_clear "$lockfile"
51
+ echo "branch-lock: cleared (unlocked)"
52
+ ;;
53
+ status)
54
+ local b
55
+ if b="$(lock_store_read "$lockfile")"; then
56
+ echo "branch-lock: locked to '$b'"
57
+ else
58
+ echo "branch-lock: unlocked"
59
+ fi
60
+ ;;
61
+ *)
62
+ echo "usage: branch-lock.sh <set [branch]|clear|status>" >&2
63
+ return 2
64
+ ;;
65
+ esac
66
+ }
67
+
68
+ main "$@"
@@ -0,0 +1,126 @@
1
+ #!/usr/bin/env bash
2
+ # lib/dev-config.sh — tiny runtime reader for `.red/config.yaml` dev flags.
3
+ #
4
+ # The hook must be dormant by default: missing config file or missing key means
5
+ # disabled. This parser intentionally mirrors the constrained YAML shape used by
6
+ # the dev runtime: nested 2-space mappings with scalar leaves. It exposes the
7
+ # few boolean flags the dev hooks need (ADR 0043 lock guard, ADR 0067 gate).
8
+
9
+ # _dev_config_scalar_true <config.yaml> <dotted-key...>
10
+ # Returns 0 when ANY of the given dotted keys is the exact scalar `true`, 1
11
+ # otherwise (including a missing file). Shared parser for the public readers.
12
+ _dev_config_scalar_true() {
13
+ local _file="$1"
14
+ shift
15
+ local -a _keys=("$@")
16
+ [[ -f "$_file" ]] || return 1
17
+
18
+ local -a _stack=()
19
+ local -a _indents=()
20
+ local _raw _line _indent_str _indent _rest _key _value _full _i
21
+ local _inner _before_close _close_len _tail
22
+
23
+ while IFS= read -r _raw || [[ -n "$_raw" ]]; do
24
+ _raw="${_raw%$'\r'}"
25
+ _line="$_raw"
26
+
27
+ if [[ "$_line" != *\"* && "$_line" != *\'* ]]; then
28
+ _line="${_line%%#*}"
29
+ fi
30
+ _line="${_line%"${_line##*[![:space:]]}"}"
31
+ [[ -z "${_line//[[:space:]]/}" ]] && continue
32
+
33
+ _indent_str="${_line%%[![:space:]]*}"
34
+ _indent=${#_indent_str}
35
+ (( _indent % 2 == 0 )) || return 1
36
+ _rest="${_line:$_indent}"
37
+
38
+ # Skip block-sequence items (`- value`) — they never contain dev flags.
39
+ # A top-level sequence (empty stack) mirrors the TS parser: malformed → bail.
40
+ if [[ "$_rest" =~ ^-([[:space:]]|$) ]]; then
41
+ (( ${#_stack[@]} == 0 )) && return 1
42
+ continue
43
+ fi
44
+
45
+ [[ "$_rest" =~ ^[A-Za-z_][A-Za-z0-9_-]*: ]] || return 1
46
+ _key="${_rest%%:*}"
47
+ _value="${_rest#*:}"
48
+ _value="${_value#"${_value%%[![:space:]]*}"}"
49
+
50
+ while ((${#_indents[@]} > 0 && _indents[${#_indents[@]} - 1] >= _indent)); do
51
+ local _last_stack=$(( ${#_stack[@]} - 1 ))
52
+ local _last_indent=$(( ${#_indents[@]} - 1 ))
53
+ unset "_stack[$_last_stack]"
54
+ unset "_indents[$_last_indent]"
55
+ done
56
+
57
+ _full=""
58
+ for ((_i = 0; _i < ${#_stack[@]}; _i++)); do
59
+ [[ -n "$_full" ]] && _full+="."
60
+ _full+="${_stack[$_i]}"
61
+ done
62
+ [[ -n "$_full" ]] && _full+="."
63
+ _full+="$_key"
64
+
65
+ if [[ -z "$_value" ]]; then
66
+ _stack+=("$_key")
67
+ _indents+=("$_indent")
68
+ continue
69
+ fi
70
+
71
+ # Strip an inline comment that follows a closing quote (e.g. `key: "v" # note`).
72
+ if [[ "${_value:0:1}" == '"' ]]; then
73
+ _inner="${_value:1}"
74
+ _before_close="${_inner%%\"*}"
75
+ if [[ "$_inner" == *'"'* ]]; then
76
+ _close_len=$(( 1 + ${#_before_close} + 1 ))
77
+ _tail="${_value:$_close_len}"
78
+ if [[ "$_tail" =~ ^[[:space:]]*(#.*)?$ ]]; then
79
+ _value="${_value:0:$_close_len}"
80
+ fi
81
+ fi
82
+ elif [[ "${_value:0:1}" == "'" ]]; then
83
+ _inner="${_value:1}"
84
+ _before_close="${_inner%%\'*}"
85
+ if [[ "$_inner" == *"'"* ]]; then
86
+ _close_len=$(( 1 + ${#_before_close} + 1 ))
87
+ _tail="${_value:$_close_len}"
88
+ if [[ "$_tail" =~ ^[[:space:]]*(#.*)?$ ]]; then
89
+ _value="${_value:0:$_close_len}"
90
+ fi
91
+ fi
92
+ fi
93
+
94
+ if [[ "$_value" == \"*\" && "$_value" == *\" ]]; then
95
+ _value="${_value:1:${#_value}-2}"
96
+ elif [[ "$_value" == \'*\' && "$_value" == *\' ]]; then
97
+ _value="${_value:1:${#_value}-2}"
98
+ fi
99
+
100
+ local _k
101
+ for _k in "${_keys[@]}"; do
102
+ if [[ "$_full" == "$_k" ]]; then
103
+ [[ "$_value" == "true" ]] && return 0
104
+ fi
105
+ done
106
+ done < "$_file"
107
+
108
+ return 1
109
+ }
110
+
111
+ # dev_config_lock_primary_branch_enabled <config.yaml>
112
+ # Returns 0 when the primary-branch lock is on, 1 otherwise. The flag lives at
113
+ # `dev.lock.primary-branch` (top-level) or `plugins.dev.lock.primary-branch`
114
+ # (namespaced); either set to exactly `true` enables the guard.
115
+ dev_config_lock_primary_branch_enabled() {
116
+ _dev_config_scalar_true "$1" "dev.lock.primary-branch" "plugins.dev.lock.primary-branch"
117
+ }
118
+
119
+ # dev_plugin_enabled <config.yaml>
120
+ # The per-directory plugin activation gate (ADR 0067): returns 0 only when
121
+ # `plugins.dev.enabled` is the exact scalar `true`. A missing file or missing
122
+ # flag → 1 (dormant). Every dev shell hook must early-exit on a non-zero return
123
+ # so it stays fully inert in repos that did not opt into the dev plugin.
124
+ dev_plugin_enabled() {
125
+ _dev_config_scalar_true "$1" "plugins.dev.enabled"
126
+ }