@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,169 @@
1
+ /**
2
+ * Symlink-safe, size-capped, whitelist-validated flag file I/O.
3
+ *
4
+ * Rulebook hooks coordinate via small flag files at predictable user-
5
+ * owned paths (`.rulebook/.terse-mode`, `~/.rulebook/.state`, etc.).
6
+ * Without hardening, these are vulnerable to two classes of local
7
+ * attack by any process with write access to the flag directory:
8
+ *
9
+ * 1. **Clobber** — attacker replaces the flag with a symlink to
10
+ * another file (e.g. `~/.ssh/authorized_keys`). The next hook
11
+ * write destroys that file.
12
+ *
13
+ * 2. **Exfil** — attacker replaces the flag with a symlink to a
14
+ * user-readable secret (e.g. `~/.ssh/id_rsa`). The next hook
15
+ * read slurps the private key and the reinforcement hook
16
+ * injects its contents into model context, where subsequent
17
+ * turns can leak it via output or tool calls.
18
+ *
19
+ * This module closes both attack classes with defense-in-depth:
20
+ *
21
+ * - `lstat` pre-check refuses symlinks at the target AND at the
22
+ * immediate parent directory.
23
+ * - `O_NOFOLLOW` on open where the platform supports it — symlink
24
+ * races during the window after lstat cause the open to fail.
25
+ * - Atomic write via temp + rename with `0600` permissions.
26
+ * - Reads are capped at `MAX_FLAG_BYTES` (32) — the longest valid
27
+ * value is "rulebook-terse-review-full" which is well under that.
28
+ * - Reads validate against `VALID_MODES`; anything else returns null.
29
+ * - Silent-fail on every filesystem error. A broken flag file must
30
+ * never block session start or poison a reinforcement hook.
31
+ *
32
+ * Grounded in `docs/analysis/caveman/06-hook-deep-dive.md`.
33
+ */
34
+ import { closeSync, constants, fchmodSync, lstatSync, openSync, readSync, renameSync, writeSync, mkdirSync, } from 'node:fs';
35
+ import { dirname, join } from 'node:path';
36
+ /**
37
+ * Whitelist of acceptable mode strings.
38
+ *
39
+ * Keep in sync with `.rulebook/specs/RULEBOOK_TERSE.md` §Intensity Levels
40
+ * and §Sub-skills. Every string here is a possible contents of the flag
41
+ * file; anything else read from disk is rejected.
42
+ */
43
+ export const VALID_MODES = ['off', 'brief', 'terse', 'ultra', 'commit', 'review'];
44
+ /**
45
+ * Hard cap on how many bytes `readFlag` will ever read. The longest
46
+ * legitimate value is `review` at 6 bytes; 32 leaves ample slack
47
+ * without enabling a useful exfiltration primitive.
48
+ */
49
+ export const MAX_FLAG_BYTES = 32;
50
+ /**
51
+ * True if `O_NOFOLLOW` is a real flag on this platform (Linux/macOS).
52
+ * On Windows it is typically undefined; we fall back to the lstat
53
+ * pre-check alone. That still protects against the pre-open symlink
54
+ * case; Windows symlinks are also far less common on user profiles.
55
+ */
56
+ const O_NOFOLLOW = typeof constants.O_NOFOLLOW === 'number' ? constants.O_NOFOLLOW : 0;
57
+ function isValidMode(s) {
58
+ return VALID_MODES.includes(s);
59
+ }
60
+ /**
61
+ * Write `content` to `flagPath` safely.
62
+ *
63
+ * Contract:
64
+ * - Refuses if `flagPath` already exists as a symlink.
65
+ * - Refuses if the parent directory is a symlink.
66
+ * - Creates with `0600` mode.
67
+ * - Atomic: writes to `<flagPath>.tmp-<pid>-<ts>`, then renames.
68
+ * - Silent-fails on any other filesystem error.
69
+ *
70
+ * `content` is coerced to a string. Callers should pass one of the
71
+ * `VALID_MODES` entries — the function does NOT validate input, since
72
+ * this is useful for test fixtures that want to simulate corruption.
73
+ */
74
+ export function safeWriteFlag(flagPath, content) {
75
+ try {
76
+ const flagDir = dirname(flagPath);
77
+ // Ensure the parent exists. If a parent along the chain is a
78
+ // symlink, that is NOT caught here — we only guard the immediate
79
+ // parent because macOS legitimately routes home directories
80
+ // through symlinks (`/tmp → /private/tmp`) and a full walk would
81
+ // produce false positives. The attack surface requires write
82
+ // access to the immediate parent, which is what we check.
83
+ mkdirSync(flagDir, { recursive: true });
84
+ try {
85
+ if (lstatSync(flagDir).isSymbolicLink())
86
+ return;
87
+ }
88
+ catch {
89
+ return;
90
+ }
91
+ try {
92
+ if (lstatSync(flagPath).isSymbolicLink())
93
+ return;
94
+ }
95
+ catch (e) {
96
+ const code = e.code;
97
+ if (code !== 'ENOENT')
98
+ return;
99
+ }
100
+ const tempPath = join(flagDir, `.terse-mode.tmp.${process.pid}.${Date.now()}`);
101
+ const flags = constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | O_NOFOLLOW;
102
+ let fd;
103
+ try {
104
+ fd = openSync(tempPath, flags, 0o600);
105
+ writeSync(fd, String(content));
106
+ // Best-effort chmod — Windows ignores but doesn't throw.
107
+ try {
108
+ fchmodSync(fd, 0o600);
109
+ }
110
+ catch {
111
+ /* ignored on platforms where fchmod is a no-op */
112
+ }
113
+ }
114
+ finally {
115
+ if (fd !== undefined)
116
+ closeSync(fd);
117
+ }
118
+ renameSync(tempPath, flagPath);
119
+ }
120
+ catch {
121
+ // Silent fail — flag is best-effort.
122
+ }
123
+ }
124
+ /**
125
+ * Read `flagPath` and return a validated mode, or null on any anomaly.
126
+ *
127
+ * Contract:
128
+ * - Returns null if target is missing, not a regular file, or a
129
+ * symlink.
130
+ * - Returns null if file size exceeds `MAX_FLAG_BYTES` (defense
131
+ * against symlink-to-secret exfil).
132
+ * - Returns null if the content (trimmed, lowercased) is not in
133
+ * `VALID_MODES`.
134
+ * - Silent-fails on any other filesystem error.
135
+ */
136
+ export function readFlag(flagPath) {
137
+ try {
138
+ let st;
139
+ try {
140
+ st = lstatSync(flagPath);
141
+ }
142
+ catch {
143
+ return null;
144
+ }
145
+ if (st.isSymbolicLink() || !st.isFile())
146
+ return null;
147
+ if (st.size > MAX_FLAG_BYTES)
148
+ return null;
149
+ const flags = constants.O_RDONLY | O_NOFOLLOW;
150
+ let fd;
151
+ let raw;
152
+ try {
153
+ fd = openSync(flagPath, flags);
154
+ const buf = Buffer.alloc(MAX_FLAG_BYTES);
155
+ const n = readSync(fd, buf, 0, MAX_FLAG_BYTES, 0);
156
+ raw = buf.subarray(0, n).toString('utf8');
157
+ }
158
+ finally {
159
+ if (fd !== undefined)
160
+ closeSync(fd);
161
+ }
162
+ const normalized = raw.trim().toLowerCase();
163
+ return isValidMode(normalized) ? normalized : null;
164
+ }
165
+ catch {
166
+ return null;
167
+ }
168
+ }
169
+ //# sourceMappingURL=safe-flag-io.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"safe-flag-io.js","sourceRoot":"","sources":["../../src/hooks/safe-flag-io.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Claude Code SessionStart hook for rulebook-terse.
3
+ *
4
+ * Runs once per session. Responsibilities:
5
+ *
6
+ * 1. Resolve the active intensity mode (env → project config →
7
+ * user-global config → tier default → `brief`).
8
+ * 2. Write the mode to `<project>/.rulebook/.terse-mode` using
9
+ * `safeWriteFlag`.
10
+ * 3. Read the invocable `rulebook-terse` SKILL.md, filter the
11
+ * intensity table + examples down to only the active level's
12
+ * rows, and emit the filtered body to stdout.
13
+ *
14
+ * Claude Code treats SessionStart stdout as hidden `additionalContext`
15
+ * — the user never sees the injection in their transcript, but the
16
+ * model's system context now carries the compression rules.
17
+ *
18
+ * Silent-fails on every filesystem error. A broken hook must NEVER
19
+ * prevent session start.
20
+ */
21
+ import { type TerseMode } from './safe-flag-io.js';
22
+ /**
23
+ * Strip YAML frontmatter from a SKILL.md body. Returns the body as-is
24
+ * if no frontmatter is present.
25
+ */
26
+ export declare function stripFrontmatter(content: string): string;
27
+ /**
28
+ * Filter the intensity table + worked-example rows to keep only the
29
+ * active level's row. The SKILL.md has six intensity-level rows but
30
+ * the model only needs the one it will use this session; keeping all
31
+ * six wastes context and invites cross-level confusion.
32
+ *
33
+ * Rows start with `| **<level>** |` for the table and `- **<level>**:`
34
+ * for the examples. The table header + separator rows pass through
35
+ * unchanged because they don't match the level-row pattern.
36
+ */
37
+ export declare function filterSkillForLevel(body: string, level: TerseMode): string;
38
+ /**
39
+ * Build the full SessionStart stdout payload.
40
+ */
41
+ export declare function buildSessionStartOutput(args: {
42
+ mode: TerseMode;
43
+ skillBody: string | null;
44
+ }): string;
45
+ /**
46
+ * Read the installed `rulebook-terse` SKILL.md. Returns the raw content
47
+ * on success, or null if no candidate path resolves.
48
+ */
49
+ export declare function loadSkillBody(projectRoot: string): string | null;
50
+ /**
51
+ * CLI entry for the SessionStart hook. Always exits 0 — any failure
52
+ * silently falls back to a no-op so session start is never blocked.
53
+ */
54
+ export declare function main(options?: {
55
+ projectRoot?: string;
56
+ env?: NodeJS.ProcessEnv;
57
+ stdout?: NodeJS.WriteStream;
58
+ }): void;
59
+ //# sourceMappingURL=terse-activate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"terse-activate.d.ts","sourceRoot":"","sources":["../../src/hooks/terse-activate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAKH,OAAO,EAAiB,KAAK,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAyBlE;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAGxD;AAED;;;;;;;;;GASG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,GAAG,MAAM,CAoB1E;AAED;;GAEG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE;IAC5C,IAAI,EAAE,SAAS,CAAC;IAChB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B,GAAG,MAAM,CAOT;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAehE;AAED;;;GAGG;AACH,wBAAgB,IAAI,CAClB,OAAO,GAAE;IACP,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,MAAM,CAAC,EAAE,MAAM,CAAC,WAAW,CAAC;CACxB,GACL,IAAI,CA0BN"}
@@ -0,0 +1,149 @@
1
+ /**
2
+ * Claude Code SessionStart hook for rulebook-terse.
3
+ *
4
+ * Runs once per session. Responsibilities:
5
+ *
6
+ * 1. Resolve the active intensity mode (env → project config →
7
+ * user-global config → tier default → `brief`).
8
+ * 2. Write the mode to `<project>/.rulebook/.terse-mode` using
9
+ * `safeWriteFlag`.
10
+ * 3. Read the invocable `rulebook-terse` SKILL.md, filter the
11
+ * intensity table + examples down to only the active level's
12
+ * rows, and emit the filtered body to stdout.
13
+ *
14
+ * Claude Code treats SessionStart stdout as hidden `additionalContext`
15
+ * — the user never sees the injection in their transcript, but the
16
+ * model's system context now carries the compression rules.
17
+ *
18
+ * Silent-fails on every filesystem error. A broken hook must NEVER
19
+ * prevent session start.
20
+ */
21
+ import { existsSync, readFileSync, unlinkSync } from 'node:fs';
22
+ import { join } from 'node:path';
23
+ import { fileURLToPath } from 'node:url';
24
+ import { safeWriteFlag } from './safe-flag-io.js';
25
+ import { getDefaultMode, getFlagPath, } from './terse-config.js';
26
+ /**
27
+ * Minimal fallback ruleset emitted when SKILL.md cannot be found on
28
+ * disk — e.g. a standalone installer that did not copy the templates
29
+ * directory. Matches the Caveman precedent (see analysis report 06).
30
+ */
31
+ const FALLBACK_RULES = `Respond tersely. All technical substance stays. Only fluff dies.
32
+
33
+ ## Persistence
34
+ ACTIVE EVERY RESPONSE once set. Off only via "/rulebook-terse off", "normal mode", or session end.
35
+
36
+ ## Rules
37
+ Drop filler (just, really, basically), pleasantries, hedging. Keep technical terms exact. Code blocks byte-for-byte unchanged.
38
+
39
+ ## Auto-Clarity
40
+ Full prose for: security warnings, destructive-op confirmations, quality-gate failures, multi-step sequences, user confusion.
41
+
42
+ ## Boundaries
43
+ Code/tests/commits/specs: unchanged.`;
44
+ /**
45
+ * Strip YAML frontmatter from a SKILL.md body. Returns the body as-is
46
+ * if no frontmatter is present.
47
+ */
48
+ export function stripFrontmatter(content) {
49
+ const match = content.match(/^---\s*\n[\s\S]*?\n---\s*\n([\s\S]*)$/);
50
+ return match ? match[1] : content;
51
+ }
52
+ /**
53
+ * Filter the intensity table + worked-example rows to keep only the
54
+ * active level's row. The SKILL.md has six intensity-level rows but
55
+ * the model only needs the one it will use this session; keeping all
56
+ * six wastes context and invites cross-level confusion.
57
+ *
58
+ * Rows start with `| **<level>** |` for the table and `- **<level>**:`
59
+ * for the examples. The table header + separator rows pass through
60
+ * unchanged because they don't match the level-row pattern.
61
+ */
62
+ export function filterSkillForLevel(body, level) {
63
+ const out = [];
64
+ const tableRow = /^\|\s*\*\*(\S+?)\*\*\s*\|/;
65
+ const exampleLine = /^-\s*\*\*(\S+?)\*\*\s*:/;
66
+ for (const line of body.split('\n')) {
67
+ const tMatch = line.match(tableRow);
68
+ if (tMatch) {
69
+ if (tMatch[1] === level)
70
+ out.push(line);
71
+ continue;
72
+ }
73
+ const eMatch = line.match(exampleLine);
74
+ if (eMatch) {
75
+ if (eMatch[1] === level)
76
+ out.push(line);
77
+ continue;
78
+ }
79
+ out.push(line);
80
+ }
81
+ return out.join('\n');
82
+ }
83
+ /**
84
+ * Build the full SessionStart stdout payload.
85
+ */
86
+ export function buildSessionStartOutput(args) {
87
+ const header = `RULEBOOK-TERSE MODE ACTIVE — level: ${args.mode}`;
88
+ if (args.skillBody === null) {
89
+ return `${header}\n\n${FALLBACK_RULES}`;
90
+ }
91
+ const filtered = filterSkillForLevel(args.skillBody, args.mode);
92
+ return `${header}\n\n${filtered}`.trimEnd() + '\n';
93
+ }
94
+ /**
95
+ * Read the installed `rulebook-terse` SKILL.md. Returns the raw content
96
+ * on success, or null if no candidate path resolves.
97
+ */
98
+ export function loadSkillBody(projectRoot) {
99
+ const candidates = [
100
+ join(projectRoot, '.claude', 'skills', 'rulebook-terse', 'SKILL.md'),
101
+ join(projectRoot, 'templates', 'skills', 'core', 'rulebook-terse', 'SKILL.md'),
102
+ ];
103
+ for (const path of candidates) {
104
+ try {
105
+ if (existsSync(path)) {
106
+ return stripFrontmatter(readFileSync(path, 'utf8'));
107
+ }
108
+ }
109
+ catch {
110
+ /* try next candidate */
111
+ }
112
+ }
113
+ return null;
114
+ }
115
+ /**
116
+ * CLI entry for the SessionStart hook. Always exits 0 — any failure
117
+ * silently falls back to a no-op so session start is never blocked.
118
+ */
119
+ export function main(options = {}) {
120
+ try {
121
+ const projectRoot = options.projectRoot ?? process.cwd();
122
+ const env = options.env ?? process.env;
123
+ const stdout = options.stdout ?? process.stdout;
124
+ const mode = getDefaultMode({ env, projectRoot, tier: env.RULEBOOK_AGENT_TIER });
125
+ const flagPath = getFlagPath(projectRoot);
126
+ if (mode === 'off') {
127
+ try {
128
+ unlinkSync(flagPath);
129
+ }
130
+ catch {
131
+ /* flag already absent — fine */
132
+ }
133
+ return;
134
+ }
135
+ safeWriteFlag(flagPath, mode);
136
+ const skillBody = loadSkillBody(projectRoot);
137
+ const output = buildSessionStartOutput({ mode, skillBody });
138
+ stdout.write(output);
139
+ }
140
+ catch {
141
+ /* silent fail — never block session start */
142
+ }
143
+ }
144
+ // CLI guard — only auto-run when invoked as the entry script.
145
+ const __filename = fileURLToPath(import.meta.url);
146
+ if (process.argv[1] === __filename) {
147
+ main();
148
+ }
149
+ //# sourceMappingURL=terse-activate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"terse-activate.js","sourceRoot":"","sources":["../../src/hooks/terse-activate.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Default-mode resolution for rulebook-terse.
3
+ *
4
+ * Resolution order (first match wins):
5
+ *
6
+ * 1. `RULEBOOK_TERSE_MODE` environment variable.
7
+ * 2. Project-local config at `<projectRoot>/.rulebook/rulebook.json`,
8
+ * field `terse.defaultMode`. Project config dominates user-global
9
+ * because a project's team standard should not be overridden by
10
+ * an individual contributor's personal preference.
11
+ * 3. User-global config at `$XDG_CONFIG_HOME/rulebook/config.json`,
12
+ * `~/.config/rulebook/config.json`, or `%APPDATA%/rulebook/config.json`.
13
+ * 4. The fallback literal `brief`.
14
+ *
15
+ * The active agent-tier can also override — see `resolveTierDefault`.
16
+ *
17
+ * Every read silent-fails on filesystem errors. A missing or corrupt
18
+ * config file never prevents the hook from proceeding; we fall through
19
+ * to the next tier.
20
+ */
21
+ import { type TerseMode } from './safe-flag-io.js';
22
+ /**
23
+ * Compute the user-global rulebook config path in OS-appropriate order:
24
+ * - `$XDG_CONFIG_HOME/rulebook/config.json` if XDG_CONFIG_HOME is set.
25
+ * - `%APPDATA%/rulebook/config.json` on Windows.
26
+ * - `~/.config/rulebook/config.json` otherwise.
27
+ */
28
+ export declare function getUserGlobalConfigPath(env?: NodeJS.ProcessEnv): string;
29
+ /**
30
+ * Read the project-local config if present.
31
+ */
32
+ export declare function getProjectConfigPath(projectRoot: string): string;
33
+ /**
34
+ * Translate an agent-tier name into the default intensity level, or
35
+ * null if the tier is unknown.
36
+ */
37
+ export declare function resolveTierDefault(tier: string | undefined): TerseMode | null;
38
+ /**
39
+ * Resolve the default intensity mode. Options allow tests to inject
40
+ * env + project root deterministically.
41
+ */
42
+ export declare function getDefaultMode(options?: {
43
+ env?: NodeJS.ProcessEnv;
44
+ projectRoot?: string;
45
+ tier?: string;
46
+ }): TerseMode;
47
+ /**
48
+ * Path to the mode flag file (project-local).
49
+ */
50
+ export declare function getFlagPath(projectRoot?: string): string;
51
+ //# sourceMappingURL=terse-config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"terse-config.d.ts","sourceRoot":"","sources":["../../src/hooks/terse-config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAKH,OAAO,EAAe,KAAK,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAmChE;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,MAAM,CAOpF;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAEhE;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,SAAS,GAAG,IAAI,CAI7E;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAC5B,OAAO,GAAE;IACP,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC;CACV,GACL,SAAS,CAgCX;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,WAAW,GAAE,MAAsB,GAAG,MAAM,CAEvE"}
@@ -0,0 +1,130 @@
1
+ /**
2
+ * Default-mode resolution for rulebook-terse.
3
+ *
4
+ * Resolution order (first match wins):
5
+ *
6
+ * 1. `RULEBOOK_TERSE_MODE` environment variable.
7
+ * 2. Project-local config at `<projectRoot>/.rulebook/rulebook.json`,
8
+ * field `terse.defaultMode`. Project config dominates user-global
9
+ * because a project's team standard should not be overridden by
10
+ * an individual contributor's personal preference.
11
+ * 3. User-global config at `$XDG_CONFIG_HOME/rulebook/config.json`,
12
+ * `~/.config/rulebook/config.json`, or `%APPDATA%/rulebook/config.json`.
13
+ * 4. The fallback literal `brief`.
14
+ *
15
+ * The active agent-tier can also override — see `resolveTierDefault`.
16
+ *
17
+ * Every read silent-fails on filesystem errors. A missing or corrupt
18
+ * config file never prevents the hook from proceeding; we fall through
19
+ * to the next tier.
20
+ */
21
+ import { existsSync, readFileSync } from 'node:fs';
22
+ import { homedir, platform } from 'node:os';
23
+ import { join } from 'node:path';
24
+ import { VALID_MODES } from './safe-flag-io.js';
25
+ const FALLBACK_MODE = 'brief';
26
+ /**
27
+ * Tier → default-intensity map. Per
28
+ * `.rulebook/specs/RULEBOOK_TERSE.md` §Tier-aware defaults.
29
+ */
30
+ const TIER_DEFAULTS = {
31
+ research: 'terse',
32
+ haiku: 'terse',
33
+ standard: 'brief',
34
+ sonnet: 'brief',
35
+ 'team-lead': 'brief',
36
+ core: 'off',
37
+ opus: 'off',
38
+ };
39
+ function readJsonSilently(path) {
40
+ try {
41
+ if (!existsSync(path))
42
+ return null;
43
+ return JSON.parse(readFileSync(path, 'utf8'));
44
+ }
45
+ catch {
46
+ return null;
47
+ }
48
+ }
49
+ function asTerseMode(v) {
50
+ if (typeof v !== 'string')
51
+ return null;
52
+ const lower = v.trim().toLowerCase();
53
+ return VALID_MODES.includes(lower)
54
+ ? lower
55
+ : null;
56
+ }
57
+ /**
58
+ * Compute the user-global rulebook config path in OS-appropriate order:
59
+ * - `$XDG_CONFIG_HOME/rulebook/config.json` if XDG_CONFIG_HOME is set.
60
+ * - `%APPDATA%/rulebook/config.json` on Windows.
61
+ * - `~/.config/rulebook/config.json` otherwise.
62
+ */
63
+ export function getUserGlobalConfigPath(env = process.env) {
64
+ if (env.XDG_CONFIG_HOME)
65
+ return join(env.XDG_CONFIG_HOME, 'rulebook', 'config.json');
66
+ if (platform() === 'win32') {
67
+ const appData = env.APPDATA ?? join(homedir(), 'AppData', 'Roaming');
68
+ return join(appData, 'rulebook', 'config.json');
69
+ }
70
+ return join(homedir(), '.config', 'rulebook', 'config.json');
71
+ }
72
+ /**
73
+ * Read the project-local config if present.
74
+ */
75
+ export function getProjectConfigPath(projectRoot) {
76
+ return join(projectRoot, '.rulebook', 'rulebook.json');
77
+ }
78
+ /**
79
+ * Translate an agent-tier name into the default intensity level, or
80
+ * null if the tier is unknown.
81
+ */
82
+ export function resolveTierDefault(tier) {
83
+ if (!tier)
84
+ return null;
85
+ const key = tier.trim().toLowerCase();
86
+ return TIER_DEFAULTS[key] ?? null;
87
+ }
88
+ /**
89
+ * Resolve the default intensity mode. Options allow tests to inject
90
+ * env + project root deterministically.
91
+ */
92
+ export function getDefaultMode(options = {}) {
93
+ const env = options.env ?? process.env;
94
+ const projectRoot = options.projectRoot ?? process.cwd();
95
+ // 1. Env var override.
96
+ const fromEnv = asTerseMode(env.RULEBOOK_TERSE_MODE);
97
+ if (fromEnv)
98
+ return fromEnv;
99
+ // 2. Project-local .rulebook/rulebook.json → terse.defaultMode.
100
+ const projectConfig = readJsonSilently(getProjectConfigPath(projectRoot));
101
+ const projectTerse = projectConfig?.['terse'];
102
+ if (projectTerse && typeof projectTerse === 'object') {
103
+ const m = asTerseMode(projectTerse.defaultMode);
104
+ if (m)
105
+ return m;
106
+ }
107
+ // 3. User-global config.
108
+ const userConfig = readJsonSilently(getUserGlobalConfigPath(env));
109
+ const userTerse = userConfig?.['terse'];
110
+ if (userTerse && typeof userTerse === 'object') {
111
+ const m = asTerseMode(userTerse.defaultMode);
112
+ if (m)
113
+ return m;
114
+ }
115
+ // 4. Agent-tier default (only if explicitly provided).
116
+ if (options.tier) {
117
+ const tierMode = resolveTierDefault(options.tier);
118
+ if (tierMode)
119
+ return tierMode;
120
+ }
121
+ // 5. Literal fallback.
122
+ return FALLBACK_MODE;
123
+ }
124
+ /**
125
+ * Path to the mode flag file (project-local).
126
+ */
127
+ export function getFlagPath(projectRoot = process.cwd()) {
128
+ return join(projectRoot, '.rulebook', '.terse-mode');
129
+ }
130
+ //# sourceMappingURL=terse-config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"terse-config.js","sourceRoot":"","sources":["../../src/hooks/terse-config.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Claude Code UserPromptSubmit hook for rulebook-terse.
3
+ *
4
+ * Runs on every user message. Three responsibilities:
5
+ *
6
+ * 1. **Mode switching.** Parses slash commands (`/rulebook-terse`,
7
+ * `/rulebook-terse brief|terse|ultra|off`, `/rulebook-terse-commit`,
8
+ * `/rulebook-terse-review`) and natural-language activation
9
+ * phrases ("be terse", "less tokens please", "terse mode",
10
+ * "activate rulebook-terse"). Writes the resolved mode to the
11
+ * flag file via `safeWriteFlag`.
12
+ *
13
+ * 2. **Mode deactivation.** Recognizes "normal mode", "stop terse",
14
+ * "disable terse", etc. Deletes the flag file.
15
+ *
16
+ * 3. **Per-turn reinforcement.** When the flag is set to a persistent
17
+ * mode (brief / terse / ultra), emits a short ~45 token attention
18
+ * anchor as `hookSpecificOutput.additionalContext` JSON. The
19
+ * SessionStart hook supplied the full rules once; this reminder
20
+ * keeps the register in the model's attention when other plugins
21
+ * inject competing instructions mid-conversation.
22
+ *
23
+ * Independent modes (commit / review) do not get the anchor —
24
+ * they have their own skill files that fully own the behavior
25
+ * during the single turn they're invoked.
26
+ *
27
+ * Silent-fails on every filesystem error. A broken hook must NEVER
28
+ * prevent a user message from reaching the model.
29
+ */
30
+ import { type TerseMode } from './safe-flag-io.js';
31
+ /**
32
+ * Result of parsing a user prompt. `null` means no mode change is
33
+ * implied by the prompt.
34
+ */
35
+ export type ParsedIntent = {
36
+ kind: 'set';
37
+ mode: TerseMode;
38
+ } | {
39
+ kind: 'off';
40
+ } | null;
41
+ /**
42
+ * Parse a user prompt for an intent to activate, switch, or disable
43
+ * rulebook-terse. Slash commands take priority over natural language;
44
+ * natural-language deactivation takes priority over activation when
45
+ * both appear in the same prompt.
46
+ *
47
+ * `defaultMode` is used for plain `/rulebook-terse` (no argument).
48
+ */
49
+ export declare function parseIntent(prompt: string, defaultMode: TerseMode): ParsedIntent;
50
+ /**
51
+ * Build the per-turn attention-anchor JSON emitted when a persistent
52
+ * mode is active. Format matches Claude Code's `hookSpecificOutput`
53
+ * contract for UserPromptSubmit hooks.
54
+ */
55
+ export declare function buildAttentionAnchor(mode: TerseMode): string;
56
+ /**
57
+ * Core hook logic, pure and testable. Given an input object and
58
+ * options, returns the string (if any) that should be emitted to
59
+ * stdout, and performs the flag-file side effects.
60
+ */
61
+ export declare function runHook(input: {
62
+ prompt?: string;
63
+ cwd?: string;
64
+ }, options?: {
65
+ projectRoot?: string;
66
+ env?: NodeJS.ProcessEnv;
67
+ }): string | null;
68
+ /**
69
+ * CLI entry. Reads JSON from stdin, invokes `runHook`, emits any
70
+ * returned string to stdout. Always exits 0.
71
+ */
72
+ export declare function main(options?: {
73
+ stdin?: NodeJS.ReadableStream;
74
+ stdout?: NodeJS.WriteStream;
75
+ projectRoot?: string;
76
+ env?: NodeJS.ProcessEnv;
77
+ }): Promise<void>;
78
+ //# sourceMappingURL=terse-mode-tracker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"terse-mode-tracker.d.ts","sourceRoot":"","sources":["../../src/hooks/terse-mode-tracker.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAIH,OAAO,EAA2B,KAAK,SAAS,EAAE,MAAM,mBAAmB,CAAC;AA+B5E;;;GAGG;AACH,MAAM,MAAM,YAAY,GACpB;IAAE,IAAI,EAAE,KAAK,CAAC;IAAC,IAAI,EAAE,SAAS,CAAA;CAAE,GAChC;IAAE,IAAI,EAAE,KAAK,CAAA;CAAE,GACf,IAAI,CAAC;AAET;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,SAAS,GAAG,YAAY,CA6ChF;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,SAAS,GAAG,MAAM,CAa5D;AAcD;;;;GAIG;AACH,wBAAgB,OAAO,CACrB,KAAK,EAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,EACxC,OAAO,GAAE;IACP,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;CACpB,GACL,MAAM,GAAG,IAAI,CA6Cf;AAED;;;GAGG;AACH,wBAAsB,IAAI,CACxB,OAAO,GAAE;IACP,KAAK,CAAC,EAAE,MAAM,CAAC,cAAc,CAAC;IAC9B,MAAM,CAAC,EAAE,MAAM,CAAC,WAAW,CAAC;IAC5B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;CACpB,GACL,OAAO,CAAC,IAAI,CAAC,CAkBf"}