@hivehub/rulebook 5.3.3 → 5.4.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 (74) hide show
  1. package/README.md +393 -354
  2. package/dist/cli/commands/compress.d.ts +18 -0
  3. package/dist/cli/commands/compress.d.ts.map +1 -0
  4. package/dist/cli/commands/compress.js +100 -0
  5. package/dist/cli/commands/compress.js.map +1 -0
  6. package/dist/cli/commands/index.d.ts +1 -0
  7. package/dist/cli/commands/index.d.ts.map +1 -1
  8. package/dist/cli/commands/index.js +1 -0
  9. package/dist/cli/commands/index.js.map +1 -1
  10. package/dist/cli/commands/init.d.ts.map +1 -1
  11. package/dist/cli/commands/init.js +2 -0
  12. package/dist/cli/commands/init.js.map +1 -1
  13. package/dist/cli/commands/update.d.ts.map +1 -1
  14. package/dist/cli/commands/update.js +2 -0
  15. package/dist/cli/commands/update.js.map +1 -1
  16. package/dist/core/claude-settings-manager.d.ts +7 -0
  17. package/dist/core/claude-settings-manager.d.ts.map +1 -1
  18. package/dist/core/claude-settings-manager.js +31 -14
  19. package/dist/core/claude-settings-manager.js.map +1 -1
  20. package/dist/core/compress/compressor.d.ts +60 -0
  21. package/dist/core/compress/compressor.d.ts.map +1 -0
  22. package/dist/core/compress/compressor.js +232 -0
  23. package/dist/core/compress/compressor.js.map +1 -0
  24. package/dist/core/compress/discover.d.ts +19 -0
  25. package/dist/core/compress/discover.d.ts.map +1 -0
  26. package/dist/core/compress/discover.js +100 -0
  27. package/dist/core/compress/discover.js.map +1 -0
  28. package/dist/core/compress/validator.d.ts +47 -0
  29. package/dist/core/compress/validator.d.ts.map +1 -0
  30. package/dist/core/compress/validator.js +131 -0
  31. package/dist/core/compress/validator.js.map +1 -0
  32. package/dist/core/doctor.d.ts.map +1 -1
  33. package/dist/core/doctor.js +66 -0
  34. package/dist/core/doctor.js.map +1 -1
  35. package/dist/core/generator.d.ts +16 -0
  36. package/dist/core/generator.d.ts.map +1 -1
  37. package/dist/core/generator.js +36 -11
  38. package/dist/core/generator.js.map +1 -1
  39. package/dist/hooks/safe-flag-io.d.ts +77 -0
  40. package/dist/hooks/safe-flag-io.d.ts.map +1 -0
  41. package/dist/hooks/safe-flag-io.js +169 -0
  42. package/dist/hooks/safe-flag-io.js.map +1 -0
  43. package/dist/hooks/terse-activate.d.ts +59 -0
  44. package/dist/hooks/terse-activate.d.ts.map +1 -0
  45. package/dist/hooks/terse-activate.js +149 -0
  46. package/dist/hooks/terse-activate.js.map +1 -0
  47. package/dist/hooks/terse-config.d.ts +51 -0
  48. package/dist/hooks/terse-config.d.ts.map +1 -0
  49. package/dist/hooks/terse-config.js +130 -0
  50. package/dist/hooks/terse-config.js.map +1 -0
  51. package/dist/hooks/terse-mode-tracker.d.ts +78 -0
  52. package/dist/hooks/terse-mode-tracker.d.ts.map +1 -0
  53. package/dist/hooks/terse-mode-tracker.js +213 -0
  54. package/dist/hooks/terse-mode-tracker.js.map +1 -0
  55. package/dist/index.js +11 -1
  56. package/dist/index.js.map +1 -1
  57. package/dist/mcp/rulebook-server.d.ts.map +1 -1
  58. package/dist/mcp/rulebook-server.js +236 -0
  59. package/dist/mcp/rulebook-server.js.map +1 -1
  60. package/dist/types.d.ts +4 -0
  61. package/dist/types.d.ts.map +1 -1
  62. package/package.json +2 -1
  63. package/templates/hooks/terse-activate.ps1 +143 -0
  64. package/templates/hooks/terse-activate.sh +197 -0
  65. package/templates/hooks/terse-mode-tracker.ps1 +153 -0
  66. package/templates/hooks/terse-mode-tracker.sh +187 -0
  67. package/templates/modules/RULEBOOK_MCP.md +52 -0
  68. package/templates/skills/core/rulebook-terse/SKILL.md +116 -0
  69. package/templates/skills/core/rulebook-terse-commit/SKILL.md +96 -0
  70. package/templates/skills/core/rulebook-terse-review/SKILL.md +112 -0
  71. package/dist/cli/commands.d.ts +0 -225
  72. package/dist/cli/commands.d.ts.map +0 -1
  73. package/dist/cli/commands.js +0 -3984
  74. package/dist/cli/commands.js.map +0 -1
@@ -0,0 +1,143 @@
1
+ # Claude Code SessionStart hook for rulebook-terse (v5.4.0) — Windows.
2
+ #
3
+ # Mirrors templates/hooks/terse-activate.sh. Resolves mode, writes
4
+ # flag file, reads SKILL.md, filters intensity table + examples to
5
+ # the active level only, emits the filtered body to stdout.
6
+
7
+ $ErrorActionPreference = 'SilentlyContinue'
8
+
9
+ $validModes = @('off','brief','terse','ultra','commit','review')
10
+
11
+ # Read optional JSON input from stdin (SessionStart may pass metadata).
12
+ $input = $null
13
+ if (-not [Console]::IsInputRedirected) { $input = $null }
14
+ else { try { $input = [Console]::In.ReadToEnd() } catch { } }
15
+
16
+ $projectRoot = $null
17
+ if ($input) {
18
+ try { $projectRoot = (ConvertFrom-Json $input).cwd } catch { }
19
+ }
20
+ if (-not $projectRoot) {
21
+ if ($env:CLAUDE_PROJECT_DIR) { $projectRoot = $env:CLAUDE_PROJECT_DIR }
22
+ else { $projectRoot = (Get-Location).Path }
23
+ }
24
+
25
+ $flagPath = Join-Path $projectRoot '.rulebook/.terse-mode'
26
+ $projectCfg = Join-Path $projectRoot '.rulebook/rulebook.json'
27
+ $userCfgBase = if ($env:XDG_CONFIG_HOME) { $env:XDG_CONFIG_HOME } else { Join-Path $env:APPDATA 'rulebook' }
28
+ $userCfg = Join-Path $userCfgBase 'config.json'
29
+
30
+ function Resolve-Mode {
31
+ foreach ($candidate in @(
32
+ $env:RULEBOOK_TERSE_MODE,
33
+ (Get-ConfigMode $projectCfg),
34
+ (Get-ConfigMode $userCfg)
35
+ )) {
36
+ if ($candidate) {
37
+ $m = $candidate.Trim().ToLower()
38
+ if ($validModes -contains $m) { return $m }
39
+ }
40
+ }
41
+ return 'terse'
42
+ }
43
+
44
+ function Get-ConfigMode([string]$path) {
45
+ if (-not (Test-Path $path)) { return $null }
46
+ try {
47
+ $cfg = Get-Content -Raw -ErrorAction Stop $path | ConvertFrom-Json
48
+ if ($cfg -and $cfg.terse -and $cfg.terse.defaultMode) {
49
+ return $cfg.terse.defaultMode
50
+ }
51
+ } catch { }
52
+ return $null
53
+ }
54
+
55
+ function Write-SafeFlag([string]$content) {
56
+ $dir = Split-Path -Parent $flagPath
57
+ if (-not (Test-Path $dir)) { New-Item -ItemType Directory -Force $dir | Out-Null }
58
+
59
+ # Refuse if target or parent is a symlink.
60
+ $parentAttr = (Get-Item $dir -Force).Attributes
61
+ if ($parentAttr -band [IO.FileAttributes]::ReparsePoint) { return }
62
+ if (Test-Path $flagPath) {
63
+ $attr = (Get-Item $flagPath -Force).Attributes
64
+ if ($attr -band [IO.FileAttributes]::ReparsePoint) { return }
65
+ }
66
+
67
+ $tmp = Join-Path $dir (".terse-mode." + [Guid]::NewGuid().ToString('N').Substring(0,8))
68
+ try {
69
+ [IO.File]::WriteAllText($tmp, $content, (New-Object Text.UTF8Encoding $false))
70
+ Move-Item -Force $tmp $flagPath
71
+ } catch {
72
+ if (Test-Path $tmp) { Remove-Item -Force $tmp }
73
+ }
74
+ }
75
+
76
+ $mode = Resolve-Mode
77
+
78
+ if ($mode -eq 'off') {
79
+ if (Test-Path $flagPath) { Remove-Item -Force $flagPath }
80
+ exit 0
81
+ }
82
+
83
+ Write-SafeFlag $mode
84
+
85
+ # Locate SKILL.md.
86
+ $skillCandidates = @(
87
+ (Join-Path $projectRoot '.claude/skills/rulebook-terse/SKILL.md'),
88
+ (Join-Path $projectRoot 'templates/skills/core/rulebook-terse/SKILL.md')
89
+ )
90
+ $skillBody = $null
91
+ foreach ($p in $skillCandidates) {
92
+ if (Test-Path $p) {
93
+ $skillBody = Get-Content -Raw $p
94
+ break
95
+ }
96
+ }
97
+
98
+ Write-Output "RULEBOOK-TERSE MODE ACTIVE — level: $mode"
99
+ Write-Output ""
100
+
101
+ if (-not $skillBody) {
102
+ Write-Output @"
103
+ ## Persistence
104
+ ACTIVE EVERY RESPONSE once set. Off only via "/rulebook-terse off", "normal mode", or session end.
105
+
106
+ ## Rules
107
+ Drop filler (just, really, basically), pleasantries, hedging. Keep technical terms exact. Code blocks byte-for-byte unchanged.
108
+
109
+ ## Auto-Clarity
110
+ Full prose for: security warnings, destructive-op confirmations, quality-gate failures, multi-step sequences, user confusion.
111
+
112
+ ## Boundaries
113
+ Code/tests/commits/specs: unchanged.
114
+ "@
115
+ exit 0
116
+ }
117
+
118
+ # Strip YAML frontmatter + filter intensity table + example rows.
119
+ $lines = $skillBody -split "`n"
120
+ $inFm = $false
121
+ $pastFm = $false
122
+ $tableRow = '^\s*\|\s*\*\*([^*]+)\*\*\s*\|'
123
+ $exampleLine = '^\s*-\s*\*\*([^*]+)\*\*\s*:'
124
+
125
+ foreach ($line in $lines) {
126
+ if (-not $pastFm) {
127
+ if ($line -match '^---\s*$') {
128
+ if (-not $inFm) { $inFm = $true; continue }
129
+ else { $inFm = $false; $pastFm = $true; continue }
130
+ }
131
+ if ($inFm) { continue }
132
+ }
133
+
134
+ if ($line -match $tableRow) {
135
+ if ($Matches[1] -eq $mode) { Write-Output $line }
136
+ continue
137
+ }
138
+ if ($line -match $exampleLine) {
139
+ if ($Matches[1] -eq $mode) { Write-Output $line }
140
+ continue
141
+ }
142
+ Write-Output $line
143
+ }
@@ -0,0 +1,197 @@
1
+ #!/usr/bin/env bash
2
+ # Claude Code SessionStart hook for rulebook-terse (v5.4.0).
3
+ #
4
+ # Resolves the active intensity mode, writes it to the project-local
5
+ # flag file via a symlink-safe path, reads the installed SKILL.md,
6
+ # filters the intensity table + example rows down to the active
7
+ # level only, and emits the filtered body to stdout — Claude Code
8
+ # injects SessionStart stdout as hidden `additionalContext`.
9
+ #
10
+ # Contract matches `.rulebook/specs/RULEBOOK_TERSE.md`. Silent-fails
11
+ # on every filesystem error so a broken hook never blocks session
12
+ # start.
13
+ #
14
+ # Configuration resolution (first match wins):
15
+ # 1. RULEBOOK_TERSE_MODE env var
16
+ # 2. $PROJECT_ROOT/.rulebook/rulebook.json → terse.defaultMode
17
+ # 3. $XDG_CONFIG_HOME/rulebook/config.json → terse.defaultMode
18
+ # 4. ~/.config/rulebook/config.json → terse.defaultMode
19
+ # 5. "terse"
20
+
21
+ set -u
22
+
23
+ # Hook input may arrive on stdin as JSON — read it if present so we
24
+ # can resolve PROJECT_ROOT from the `cwd` field (Claude Code may
25
+ # invoke the hook from a sub-directory of the project).
26
+ input=""
27
+ if [ ! -t 0 ]; then
28
+ input="$(cat)"
29
+ fi
30
+
31
+ PROJECT_ROOT=""
32
+ if [ -n "$input" ]; then
33
+ PROJECT_ROOT="$(printf '%s' "$input" | node -e "
34
+ try {
35
+ const data = JSON.parse(require('fs').readFileSync(0, 'utf8'));
36
+ process.stdout.write(data.cwd || '');
37
+ } catch { }
38
+ " 2>/dev/null || true)"
39
+ fi
40
+ [ -z "$PROJECT_ROOT" ] && PROJECT_ROOT="${CLAUDE_PROJECT_DIR:-$(pwd)}"
41
+
42
+ FLAG_PATH="${PROJECT_ROOT}/.rulebook/.terse-mode"
43
+ CONFIG_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/rulebook"
44
+ USER_CONFIG="${CONFIG_DIR}/config.json"
45
+ PROJECT_CONFIG="${PROJECT_ROOT}/.rulebook/rulebook.json"
46
+
47
+ VALID_MODES_RE='^(off|brief|terse|ultra|commit|review)$'
48
+
49
+ resolve_mode() {
50
+ # 1. Env var
51
+ if [ -n "${RULEBOOK_TERSE_MODE:-}" ]; then
52
+ local m="$(printf '%s' "$RULEBOOK_TERSE_MODE" | tr '[:upper:]' '[:lower:]' | tr -d '[:space:]')"
53
+ if [[ "$m" =~ $VALID_MODES_RE ]]; then
54
+ printf '%s' "$m"
55
+ return
56
+ fi
57
+ fi
58
+
59
+ # 2. Project config
60
+ if [ -f "$PROJECT_CONFIG" ]; then
61
+ local m
62
+ m="$(node -e "
63
+ try {
64
+ const cfg = JSON.parse(require('fs').readFileSync(process.argv[1], 'utf8'));
65
+ if (cfg.terse && cfg.terse.defaultMode) process.stdout.write(String(cfg.terse.defaultMode));
66
+ } catch { }
67
+ " "$PROJECT_CONFIG" 2>/dev/null | tr '[:upper:]' '[:lower:]' | tr -d '[:space:]' || true)"
68
+ if [ -n "$m" ] && [[ "$m" =~ $VALID_MODES_RE ]]; then
69
+ printf '%s' "$m"
70
+ return
71
+ fi
72
+ fi
73
+
74
+ # 3. User config
75
+ if [ -f "$USER_CONFIG" ]; then
76
+ local m
77
+ m="$(node -e "
78
+ try {
79
+ const cfg = JSON.parse(require('fs').readFileSync(process.argv[1], 'utf8'));
80
+ if (cfg.terse && cfg.terse.defaultMode) process.stdout.write(String(cfg.terse.defaultMode));
81
+ } catch { }
82
+ " "$USER_CONFIG" 2>/dev/null | tr '[:upper:]' '[:lower:]' | tr -d '[:space:]' || true)"
83
+ if [ -n "$m" ] && [[ "$m" =~ $VALID_MODES_RE ]]; then
84
+ printf '%s' "$m"
85
+ return
86
+ fi
87
+ fi
88
+
89
+ printf 'terse'
90
+ }
91
+
92
+ # Symlink-safe flag-file write. Refuses if target or parent is a
93
+ # symlink; creates with 0600 via umask + atomic temp+rename.
94
+ safe_write_flag() {
95
+ local content="$1"
96
+ local dir
97
+ dir="$(dirname "$FLAG_PATH")"
98
+
99
+ mkdir -p "$dir" 2>/dev/null || return 0
100
+
101
+ # Refuse if parent is itself a symlink.
102
+ [ -L "$dir" ] && return 0
103
+ # Refuse if target already exists as a symlink.
104
+ [ -L "$FLAG_PATH" ] && return 0
105
+
106
+ local tmp
107
+ tmp="$(mktemp "$dir/.terse-mode.XXXXXX" 2>/dev/null)" || return 0
108
+ {
109
+ umask 077
110
+ printf '%s' "$content" > "$tmp" 2>/dev/null || { rm -f "$tmp"; return 0; }
111
+ }
112
+ chmod 600 "$tmp" 2>/dev/null || true
113
+ mv -f "$tmp" "$FLAG_PATH" 2>/dev/null || rm -f "$tmp"
114
+ }
115
+
116
+ mode="$(resolve_mode)"
117
+
118
+ # "off" → unlink the flag and exit cleanly (no hidden-context emission).
119
+ if [ "$mode" = "off" ]; then
120
+ rm -f "$FLAG_PATH" 2>/dev/null || true
121
+ exit 0
122
+ fi
123
+
124
+ safe_write_flag "$mode"
125
+
126
+ # Locate the SKILL.md. Prefer the installed copy; fall back to the
127
+ # repo-local template when running inside the Rulebook source tree.
128
+ SKILL_PATHS=(
129
+ "${PROJECT_ROOT}/.claude/skills/rulebook-terse/SKILL.md"
130
+ "${PROJECT_ROOT}/templates/skills/core/rulebook-terse/SKILL.md"
131
+ )
132
+
133
+ skill_body=""
134
+ for p in "${SKILL_PATHS[@]}"; do
135
+ if [ -f "$p" ]; then
136
+ skill_body="$(cat "$p" 2>/dev/null || true)"
137
+ break
138
+ fi
139
+ done
140
+
141
+ # Emit the payload. When no SKILL.md is found, fall back to a
142
+ # minimal hardcoded ruleset — matches the Caveman pattern for
143
+ # standalone installs without templates.
144
+ if [ -z "$skill_body" ]; then
145
+ cat <<EOF
146
+ RULEBOOK-TERSE MODE ACTIVE — level: ${mode}
147
+
148
+ ## Persistence
149
+ ACTIVE EVERY RESPONSE once set. Off only via "/rulebook-terse off", "normal mode", or session end.
150
+
151
+ ## Rules
152
+ Drop filler (just, really, basically), pleasantries, hedging. Keep technical terms exact. Code blocks byte-for-byte unchanged.
153
+
154
+ ## Auto-Clarity
155
+ Full prose for: security warnings, destructive-op confirmations, quality-gate failures, multi-step sequences, user confusion.
156
+
157
+ ## Boundaries
158
+ Code/tests/commits/specs: unchanged.
159
+ EOF
160
+ exit 0
161
+ fi
162
+
163
+ # Header + filtered body. Filtering:
164
+ # - Strip YAML frontmatter (everything up through the second `---`).
165
+ # - Intensity-table rows `| **<level>** | ...` — keep only the active one.
166
+ # - Example lines `- **<level>**: "..."` — keep only the active one.
167
+ # - All other lines pass through unchanged.
168
+ #
169
+ # Uses `node -e` for portability — BSD awk (macOS default) does not
170
+ # support the 3-arg `match(str, re, array)` form that gawk ships with.
171
+ printf 'RULEBOOK-TERSE MODE ACTIVE — level: %s\n\n' "$mode"
172
+
173
+ printf '%s' "$skill_body" | node -e "
174
+ let body = '';
175
+ process.stdin.setEncoding('utf8');
176
+ process.stdin.on('data', (c) => { body += c; });
177
+ process.stdin.on('end', () => {
178
+ const active = process.argv[1];
179
+ // Strip YAML frontmatter.
180
+ const stripped = body.replace(/^---\s*\n[\s\S]*?\n---\s*\n/, '');
181
+ const out = [];
182
+ for (const line of stripped.split('\n')) {
183
+ const tableRow = line.match(/^\s*\|\s*\*\*([^*]+)\*\*\s*\|/);
184
+ if (tableRow) {
185
+ if (tableRow[1] === active) out.push(line);
186
+ continue;
187
+ }
188
+ const exampleLine = line.match(/^\s*-\s*\*\*([^*]+)\*\*\s*:/);
189
+ if (exampleLine) {
190
+ if (exampleLine[1] === active) out.push(line);
191
+ continue;
192
+ }
193
+ out.push(line);
194
+ }
195
+ process.stdout.write(out.join('\n'));
196
+ });
197
+ " "$mode"
@@ -0,0 +1,153 @@
1
+ # Claude Code UserPromptSubmit hook for rulebook-terse (v5.4.0) — Windows.
2
+ #
3
+ # Mirrors templates/hooks/terse-mode-tracker.sh. Parses slash commands
4
+ # + natural-language activation/deactivation, updates the flag file,
5
+ # and emits an attention anchor JSON for persistent modes.
6
+
7
+ $ErrorActionPreference = 'SilentlyContinue'
8
+
9
+ $validModes = @('off','brief','terse','ultra','commit','review')
10
+ $maxFlagBytes = 32
11
+
12
+ $input = $null
13
+ try { $input = [Console]::In.ReadToEnd() } catch { }
14
+
15
+ $prompt = ''
16
+ $cwd = $null
17
+ if ($input) {
18
+ try {
19
+ $parsed = $input | ConvertFrom-Json
20
+ if ($parsed.prompt) { $prompt = $parsed.prompt }
21
+ if ($parsed.cwd) { $cwd = $parsed.cwd }
22
+ } catch { }
23
+ }
24
+
25
+ $projectRoot = if ($cwd) { $cwd } elseif ($env:CLAUDE_PROJECT_DIR) { $env:CLAUDE_PROJECT_DIR } else { (Get-Location).Path }
26
+ $flagPath = Join-Path $projectRoot '.rulebook/.terse-mode'
27
+ $projectCfg = Join-Path $projectRoot '.rulebook/rulebook.json'
28
+ $userCfgBase = if ($env:XDG_CONFIG_HOME) { $env:XDG_CONFIG_HOME } else { Join-Path $env:APPDATA 'rulebook' }
29
+ $userCfg = Join-Path $userCfgBase 'config.json'
30
+
31
+ function Get-ConfigMode([string]$path) {
32
+ if (-not (Test-Path $path)) { return $null }
33
+ try {
34
+ $cfg = Get-Content -Raw -ErrorAction Stop $path | ConvertFrom-Json
35
+ if ($cfg -and $cfg.terse -and $cfg.terse.defaultMode) { return $cfg.terse.defaultMode }
36
+ } catch { }
37
+ return $null
38
+ }
39
+
40
+ function Resolve-DefaultMode {
41
+ foreach ($candidate in @(
42
+ $env:RULEBOOK_TERSE_MODE,
43
+ (Get-ConfigMode $projectCfg),
44
+ (Get-ConfigMode $userCfg)
45
+ )) {
46
+ if ($candidate) {
47
+ $m = $candidate.Trim().ToLower()
48
+ if ($validModes -contains $m) { return $m }
49
+ }
50
+ }
51
+ return 'terse'
52
+ }
53
+
54
+ function Write-SafeFlag([string]$content) {
55
+ $dir = Split-Path -Parent $flagPath
56
+ if (-not (Test-Path $dir)) { New-Item -ItemType Directory -Force $dir | Out-Null }
57
+ $parentAttr = (Get-Item $dir -Force).Attributes
58
+ if ($parentAttr -band [IO.FileAttributes]::ReparsePoint) { return }
59
+ if (Test-Path $flagPath) {
60
+ $attr = (Get-Item $flagPath -Force).Attributes
61
+ if ($attr -band [IO.FileAttributes]::ReparsePoint) { return }
62
+ }
63
+ $tmp = Join-Path $dir (".terse-mode." + [Guid]::NewGuid().ToString('N').Substring(0,8))
64
+ try {
65
+ [IO.File]::WriteAllText($tmp, $content, (New-Object Text.UTF8Encoding $false))
66
+ Move-Item -Force $tmp $flagPath
67
+ } catch {
68
+ if (Test-Path $tmp) { Remove-Item -Force $tmp }
69
+ }
70
+ }
71
+
72
+ function Read-Flag {
73
+ if (-not (Test-Path $flagPath)) { return $null }
74
+ $attr = (Get-Item $flagPath -Force).Attributes
75
+ if ($attr -band [IO.FileAttributes]::ReparsePoint) { return $null }
76
+ $size = (Get-Item $flagPath).Length
77
+ if ($size -gt $maxFlagBytes) { return $null }
78
+ $raw = (Get-Content -Raw $flagPath).Trim().ToLower()
79
+ if ($validModes -contains $raw) { return $raw }
80
+ return $null
81
+ }
82
+
83
+ $defaultMode = Resolve-DefaultMode
84
+ $lowerPrompt = $prompt.ToLower()
85
+
86
+ $newMode = $null
87
+ $deactivate = $false
88
+
89
+ if ($lowerPrompt -match '^\s*/rulebook-terse-commit\b') {
90
+ $newMode = 'commit'
91
+ }
92
+ elseif ($lowerPrompt -match '^\s*/rulebook-terse-review\b') {
93
+ $newMode = 'review'
94
+ }
95
+ elseif ($lowerPrompt -match '^\s*/rulebook-terse\s*$') {
96
+ $newMode = $defaultMode
97
+ }
98
+ elseif ($lowerPrompt -match '^\s*/rulebook-terse\s+(\S+)') {
99
+ $arg = $Matches[1]
100
+ switch ($arg) {
101
+ 'off' { $deactivate = $true }
102
+ { @('brief','terse','ultra','commit','review') -contains $_ } { $newMode = $arg }
103
+ }
104
+ }
105
+
106
+ if (-not $newMode -and -not $deactivate) {
107
+ $deactRegexes = @(
108
+ '\b(stop|disable|turn off|deactivate)\b.*\brulebook[- ]?terse\b',
109
+ '\brulebook[- ]?terse\b.*\b(stop|disable|turn off|deactivate)\b',
110
+ '\b(stop|disable) terse\b',
111
+ '\bnormal mode\b'
112
+ )
113
+ foreach ($re in $deactRegexes) {
114
+ if ($prompt -match $re) { $deactivate = $true; break }
115
+ }
116
+ }
117
+
118
+ if (-not $newMode -and -not $deactivate) {
119
+ $actRegexes = @(
120
+ '\b(activate|enable|turn on|start)\b.*\brulebook[- ]?terse\b',
121
+ '\brulebook[- ]?terse\b.*\b(mode|activate|enable|turn on|start)\b',
122
+ '\bbe terse\b',
123
+ '\bterse mode\b',
124
+ '\bless tokens?\b'
125
+ )
126
+ foreach ($re in $actRegexes) {
127
+ if ($prompt -match $re) { $newMode = $defaultMode; break }
128
+ }
129
+ }
130
+
131
+ if ($deactivate) {
132
+ if (Test-Path $flagPath) { Remove-Item -Force $flagPath }
133
+ }
134
+ elseif ($newMode) {
135
+ if ($newMode -eq 'off') {
136
+ if (Test-Path $flagPath) { Remove-Item -Force $flagPath }
137
+ } else {
138
+ Write-SafeFlag $newMode
139
+ }
140
+ }
141
+
142
+ $active = Read-Flag
143
+ if ($active -and $active -ne 'off' -and $active -ne 'commit' -and $active -ne 'review') {
144
+ $keep = if ($active -eq 'brief') { 'Keep articles and full sentences.' } else { 'Fragments OK.' }
145
+ $text = "RULEBOOK-TERSE ACTIVE ($active). Drop filler/hedging/pleasantries. $keep Code/tests/commits/security: write full. Quality-gate failures + destructive ops: write full."
146
+ $payload = @{
147
+ hookSpecificOutput = @{
148
+ hookEventName = 'UserPromptSubmit'
149
+ additionalContext = $text
150
+ }
151
+ } | ConvertTo-Json -Compress -Depth 5
152
+ Write-Output $payload
153
+ }
@@ -0,0 +1,187 @@
1
+ #!/usr/bin/env bash
2
+ # Claude Code UserPromptSubmit hook for rulebook-terse (v5.4.0).
3
+ #
4
+ # Parses slash commands + natural-language activation/deactivation
5
+ # phrases in the user prompt, updates the project-local flag file,
6
+ # and emits a short (~45 token) attention anchor as hookSpecificOutput
7
+ # when a persistent mode is active — keeps the compression register in
8
+ # the model's attention on every user message.
9
+ #
10
+ # Independent sub-skill modes (commit / review) do NOT get the anchor:
11
+ # their own SKILL.md files drive behavior for the single turn they're
12
+ # invoked.
13
+ #
14
+ # Silent-fails on every filesystem error.
15
+
16
+ set -u
17
+
18
+ input="$(cat || true)"
19
+ prompt=""
20
+ cwd=""
21
+ if [ -n "$input" ]; then
22
+ # Parse JSON via node (always available in Claude Code hook env).
23
+ # Emits two lines: prompt on line 1, cwd on line 2.
24
+ parsed="$(printf '%s' "$input" | node -e "
25
+ try {
26
+ const data = JSON.parse(require('fs').readFileSync(0, 'utf8'));
27
+ process.stdout.write((data.prompt || '') + '\\n' + (data.cwd || ''));
28
+ } catch { process.stdout.write('\\n'); }
29
+ " 2>/dev/null || printf '\n')"
30
+ prompt="$(printf '%s' "$parsed" | head -n 1)"
31
+ cwd="$(printf '%s' "$parsed" | tail -n +2)"
32
+ fi
33
+
34
+ PROJECT_ROOT="${cwd:-${CLAUDE_PROJECT_DIR:-$(pwd)}}"
35
+ FLAG_PATH="${PROJECT_ROOT}/.rulebook/.terse-mode"
36
+ CONFIG_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/rulebook"
37
+ USER_CONFIG="${CONFIG_DIR}/config.json"
38
+ PROJECT_CONFIG="${PROJECT_ROOT}/.rulebook/rulebook.json"
39
+
40
+ VALID_MODES_RE='^(off|brief|terse|ultra|commit|review)$'
41
+ MAX_FLAG_BYTES=32
42
+
43
+ resolve_default_mode() {
44
+ if [ -n "${RULEBOOK_TERSE_MODE:-}" ]; then
45
+ local m="$(printf '%s' "$RULEBOOK_TERSE_MODE" | tr '[:upper:]' '[:lower:]' | tr -d '[:space:]')"
46
+ if [[ "$m" =~ $VALID_MODES_RE ]]; then printf '%s' "$m"; return; fi
47
+ fi
48
+ if [ -f "$PROJECT_CONFIG" ]; then
49
+ local m
50
+ m="$(node -e "
51
+ try {
52
+ const cfg = JSON.parse(require('fs').readFileSync(process.argv[1], 'utf8'));
53
+ if (cfg.terse && cfg.terse.defaultMode) process.stdout.write(String(cfg.terse.defaultMode));
54
+ } catch { }
55
+ " "$PROJECT_CONFIG" 2>/dev/null | tr '[:upper:]' '[:lower:]' | tr -d '[:space:]' || true)"
56
+ if [ -n "$m" ] && [[ "$m" =~ $VALID_MODES_RE ]]; then printf '%s' "$m"; return; fi
57
+ fi
58
+ if [ -f "$USER_CONFIG" ]; then
59
+ local m
60
+ m="$(node -e "
61
+ try {
62
+ const cfg = JSON.parse(require('fs').readFileSync(process.argv[1], 'utf8'));
63
+ if (cfg.terse && cfg.terse.defaultMode) process.stdout.write(String(cfg.terse.defaultMode));
64
+ } catch { }
65
+ " "$USER_CONFIG" 2>/dev/null | tr '[:upper:]' '[:lower:]' | tr -d '[:space:]' || true)"
66
+ if [ -n "$m" ] && [[ "$m" =~ $VALID_MODES_RE ]]; then printf '%s' "$m"; return; fi
67
+ fi
68
+ printf 'terse'
69
+ }
70
+
71
+ safe_write_flag() {
72
+ local content="$1"
73
+ local dir
74
+ dir="$(dirname "$FLAG_PATH")"
75
+ mkdir -p "$dir" 2>/dev/null || return 0
76
+ [ -L "$dir" ] && return 0
77
+ [ -L "$FLAG_PATH" ] && return 0
78
+
79
+ local tmp
80
+ tmp="$(mktemp "$dir/.terse-mode.XXXXXX" 2>/dev/null)" || return 0
81
+ {
82
+ umask 077
83
+ printf '%s' "$content" > "$tmp" 2>/dev/null || { rm -f "$tmp"; return 0; }
84
+ }
85
+ chmod 600 "$tmp" 2>/dev/null || true
86
+ mv -f "$tmp" "$FLAG_PATH" 2>/dev/null || rm -f "$tmp"
87
+ }
88
+
89
+ read_flag() {
90
+ # Symlink-safe, size-capped, whitelist-validated read.
91
+ [ ! -f "$FLAG_PATH" ] && return 1
92
+ [ -L "$FLAG_PATH" ] && return 1
93
+ local size
94
+ size="$(wc -c < "$FLAG_PATH" 2>/dev/null || echo "$MAX_FLAG_BYTES")"
95
+ [ "$size" -gt "$MAX_FLAG_BYTES" ] && return 1
96
+
97
+ local raw
98
+ raw="$(head -c "$MAX_FLAG_BYTES" "$FLAG_PATH" 2>/dev/null | tr '[:upper:]' '[:lower:]' | tr -d '[:space:]')"
99
+ if [[ "$raw" =~ $VALID_MODES_RE ]]; then
100
+ printf '%s' "$raw"
101
+ return 0
102
+ fi
103
+ return 1
104
+ }
105
+
106
+ default_mode="$(resolve_default_mode)"
107
+ lower_prompt="$(printf '%s' "$prompt" | tr '[:upper:]' '[:lower:]')"
108
+
109
+ # Parse intent. Order matters: deactivation wins over accidental
110
+ # activation, slash commands win over natural-language patterns.
111
+ new_mode=""
112
+ deactivate=0
113
+
114
+ # Slash commands
115
+ if [[ "$lower_prompt" == /rulebook-terse-commit* ]]; then
116
+ new_mode="commit"
117
+ elif [[ "$lower_prompt" == /rulebook-terse-review* ]]; then
118
+ new_mode="review"
119
+ elif [[ "$lower_prompt" == /rulebook-terse ]]; then
120
+ new_mode="$default_mode"
121
+ elif [[ "$lower_prompt" == /rulebook-terse\ * ]]; then
122
+ arg="$(printf '%s' "$lower_prompt" | awk '{print $2}')"
123
+ case "$arg" in
124
+ off) deactivate=1 ;;
125
+ brief|terse|ultra|commit|review) new_mode="$arg" ;;
126
+ *) : ;; # unknown subcommand → leave state unchanged
127
+ esac
128
+ fi
129
+
130
+ # Natural-language deactivation — checked first so "stop terse" wins.
131
+ if [ -z "$new_mode" ] && [ "$deactivate" -eq 0 ]; then
132
+ if echo "$prompt" | grep -qiE '\b(stop|disable|turn off|deactivate)\b.*\brulebook[- ]?terse\b' \
133
+ || echo "$prompt" | grep -qiE '\brulebook[- ]?terse\b.*\b(stop|disable|turn off|deactivate)\b' \
134
+ || echo "$prompt" | grep -qiE '\b(stop|disable) terse\b' \
135
+ || echo "$prompt" | grep -qiE '\bnormal mode\b'; then
136
+ deactivate=1
137
+ fi
138
+ fi
139
+
140
+ # Natural-language activation
141
+ if [ -z "$new_mode" ] && [ "$deactivate" -eq 0 ]; then
142
+ if echo "$prompt" | grep -qiE '\b(activate|enable|turn on|start)\b.*\brulebook[- ]?terse\b' \
143
+ || echo "$prompt" | grep -qiE '\brulebook[- ]?terse\b.*\b(mode|activate|enable|turn on|start)\b' \
144
+ || echo "$prompt" | grep -qiE '\bbe terse\b' \
145
+ || echo "$prompt" | grep -qiE '\bterse mode\b' \
146
+ || echo "$prompt" | grep -qiE '\bless tokens?\b'; then
147
+ new_mode="$default_mode"
148
+ fi
149
+ fi
150
+
151
+ # Apply intent
152
+ if [ "$deactivate" -eq 1 ]; then
153
+ rm -f "$FLAG_PATH" 2>/dev/null || true
154
+ elif [ -n "$new_mode" ]; then
155
+ if [ "$new_mode" = "off" ]; then
156
+ rm -f "$FLAG_PATH" 2>/dev/null || true
157
+ else
158
+ safe_write_flag "$new_mode"
159
+ fi
160
+ fi
161
+
162
+ # Emit attention anchor for persistent modes only.
163
+ active_mode="$(read_flag || true)"
164
+ case "$active_mode" in
165
+ brief|terse|ultra)
166
+ if [ "$active_mode" = "brief" ]; then
167
+ keep_clause="Keep articles and full sentences."
168
+ else
169
+ keep_clause="Fragments OK."
170
+ fi
171
+ text="RULEBOOK-TERSE ACTIVE (${active_mode}). Drop filler/hedging/pleasantries. ${keep_clause} Code/tests/commits/security: write full. Quality-gate failures + destructive ops: write full."
172
+ # Emit hookSpecificOutput JSON via node — universal across platforms.
173
+ node -e "
174
+ const text = process.argv[1];
175
+ process.stdout.write(JSON.stringify({
176
+ hookSpecificOutput: {
177
+ hookEventName: 'UserPromptSubmit',
178
+ additionalContext: text
179
+ }
180
+ }));
181
+ " "$text" 2>/dev/null || true
182
+ ;;
183
+ *)
184
+ # No active persistent mode, or commit/review sub-skill — no anchor.
185
+ :
186
+ ;;
187
+ esac