@rm0nroe/coach-claw 1.0.6

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 (100) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +311 -0
  3. package/coach/README.md +99 -0
  4. package/coach/bin/aggregate_facets.py +274 -0
  5. package/coach/bin/analyze.py +678 -0
  6. package/coach/bin/bank.py +247 -0
  7. package/coach/bin/banner_themes.py +645 -0
  8. package/coach/bin/coach_paths.py +33 -0
  9. package/coach/bin/coexistence_check.py +129 -0
  10. package/coach/bin/configure.py +245 -0
  11. package/coach/bin/cron_check.py +81 -0
  12. package/coach/bin/default_statusline.py +135 -0
  13. package/coach/bin/doctor.py +663 -0
  14. package/coach/bin/insights-llm.sh +264 -0
  15. package/coach/bin/insights.sh +163 -0
  16. package/coach/bin/insights_window.py +111 -0
  17. package/coach/bin/marker_io.py +154 -0
  18. package/coach/bin/merge.py +671 -0
  19. package/coach/bin/redact.py +86 -0
  20. package/coach/bin/render_env.py +148 -0
  21. package/coach/bin/reward_hints.py +87 -0
  22. package/coach/bin/run-insights.sh +20 -0
  23. package/coach/bin/run_with_lock.py +85 -0
  24. package/coach/bin/scoring.py +260 -0
  25. package/coach/bin/skill_inventory.py +215 -0
  26. package/coach/bin/stats.py +459 -0
  27. package/coach/bin/status.py +293 -0
  28. package/coach/bin/statusline_self_patch.py +205 -0
  29. package/coach/bin/statusline_variants.py +146 -0
  30. package/coach/bin/statusline_wrap.py +244 -0
  31. package/coach/bin/statusline_wrap_action.py +460 -0
  32. package/coach/bin/switch_to_plugin.py +256 -0
  33. package/coach/bin/themes.py +256 -0
  34. package/coach/bin/user_config.py +176 -0
  35. package/coach/bin/xp_accounting.py +98 -0
  36. package/coach/changelog.md +4 -0
  37. package/coach/default-statusline-command.sh +19 -0
  38. package/coach/default-statusline-wrap-command.sh +15 -0
  39. package/coach/profile.yaml +37 -0
  40. package/coach/tests/conftest.py +13 -0
  41. package/coach/tests/test_aggregate_facets.py +379 -0
  42. package/coach/tests/test_analyze_aggregate.py +153 -0
  43. package/coach/tests/test_analyze_redaction.py +105 -0
  44. package/coach/tests/test_analyze_strengths.py +165 -0
  45. package/coach/tests/test_bank_atomic_write.py +61 -0
  46. package/coach/tests/test_bank_concurrency.py +126 -0
  47. package/coach/tests/test_banner_themes.py +981 -0
  48. package/coach/tests/test_celebrate_dedup.py +409 -0
  49. package/coach/tests/test_coach_paths.py +50 -0
  50. package/coach/tests/test_coexistence_check.py +128 -0
  51. package/coach/tests/test_configure.py +258 -0
  52. package/coach/tests/test_cron_check.py +118 -0
  53. package/coach/tests/test_cron_nudge_hook.py +134 -0
  54. package/coach/tests/test_detection_parity.py +105 -0
  55. package/coach/tests/test_doctor.py +595 -0
  56. package/coach/tests/test_hook_bespoke_dispatch.py +288 -0
  57. package/coach/tests/test_hook_module_resolution.py +116 -0
  58. package/coach/tests/test_hook_relevance.py +996 -0
  59. package/coach/tests/test_hook_render_env.py +364 -0
  60. package/coach/tests/test_hook_session_id_guard.py +160 -0
  61. package/coach/tests/test_insights_llm.py +759 -0
  62. package/coach/tests/test_insights_llm_venv_path.py +109 -0
  63. package/coach/tests/test_insights_window.py +237 -0
  64. package/coach/tests/test_install.py +1150 -0
  65. package/coach/tests/test_install_pyyaml_fallback.py +142 -0
  66. package/coach/tests/test_marker_consumption.py +167 -0
  67. package/coach/tests/test_marker_writer_locking.py +305 -0
  68. package/coach/tests/test_merge.py +413 -0
  69. package/coach/tests/test_no_broken_mktemp.py +90 -0
  70. package/coach/tests/test_render_env.py +137 -0
  71. package/coach/tests/test_render_env_glyphs.py +119 -0
  72. package/coach/tests/test_reward_hints.py +59 -0
  73. package/coach/tests/test_scoring.py +147 -0
  74. package/coach/tests/test_session_start_weekly_trigger.py +92 -0
  75. package/coach/tests/test_skill_inventory.py +368 -0
  76. package/coach/tests/test_stats_hybrid.py +142 -0
  77. package/coach/tests/test_status_accounting.py +41 -0
  78. package/coach/tests/test_statusline_failsafe.py +70 -0
  79. package/coach/tests/test_statusline_self_patch.py +261 -0
  80. package/coach/tests/test_statusline_variants.py +110 -0
  81. package/coach/tests/test_statusline_wrap.py +196 -0
  82. package/coach/tests/test_statusline_wrap_action.py +408 -0
  83. package/coach/tests/test_switch_to_plugin.py +360 -0
  84. package/coach/tests/test_themes.py +104 -0
  85. package/coach/tests/test_user_config.py +160 -0
  86. package/coach/tests/test_wrap_announce_hook.py +130 -0
  87. package/coach/tests/test_xp_accounting.py +55 -0
  88. package/hooks/coach-session-start.py +536 -0
  89. package/hooks/coach-user-prompt.py +2288 -0
  90. package/install-launchd.sh +102 -0
  91. package/install.sh +597 -0
  92. package/launchd/com.local.claude-coach.plist.template +34 -0
  93. package/launchd/run-insights.sh +20 -0
  94. package/npm/coach-claw.js +259 -0
  95. package/package.json +52 -0
  96. package/requirements.txt +11 -0
  97. package/settings-snippet.json +31 -0
  98. package/skills/coach/SKILL.md +107 -0
  99. package/skills/coach-insights/SKILL.md +78 -0
  100. package/skills/config/SKILL.md +149 -0
@@ -0,0 +1,259 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ const fs = require("fs");
5
+ const os = require("os");
6
+ const path = require("path");
7
+ const { spawnSync } = require("child_process");
8
+
9
+ const ROOT = path.resolve(__dirname, "..");
10
+ const VERSION = require(path.join(ROOT, "package.json")).version;
11
+
12
+ function usage() {
13
+ console.log(`Coach Claw ${VERSION}
14
+
15
+ Usage:
16
+ coach-claw doctor
17
+ coach-claw install [--seed | --no-seed] [--fresh] [--no-prune-backups]
18
+ coach-claw launchd
19
+ coach-claw config <set|preview|wizard> [...]
20
+ coach-claw help
21
+
22
+ Examples:
23
+ coach-claw doctor
24
+ coach-claw install --seed
25
+ coach-claw install --no-seed
26
+ coach-claw launchd
27
+ coach-claw config wizard
28
+ coach-claw config preview
29
+ coach-claw config set --theme ocean --statusline pips
30
+ coach-claw config set --elo 1200 2800
31
+
32
+ The /config slash command inside Claude Code edits the same file.`);
33
+ }
34
+
35
+ function fail(message) {
36
+ console.error(`ERROR: ${message}`);
37
+ process.exit(1);
38
+ }
39
+
40
+ function warn(message) {
41
+ console.error(`WARN: ${message}`);
42
+ }
43
+
44
+ function ok(message) {
45
+ console.log(`OK: ${message}`);
46
+ }
47
+
48
+ function isSupportedPlatform() {
49
+ return process.platform === "darwin" || process.platform === "linux";
50
+ }
51
+
52
+ function run(command, args, options = {}) {
53
+ return spawnSync(command, args, {
54
+ cwd: ROOT,
55
+ encoding: "utf8",
56
+ stdio: options.stdio || "pipe",
57
+ env: options.env || process.env
58
+ });
59
+ }
60
+
61
+ function commandExists(command) {
62
+ const result = run("sh", ["-c", `command -v ${quoteForShell(command)}`]);
63
+ return result.status === 0;
64
+ }
65
+
66
+ function quoteForShell(value) {
67
+ return `'${String(value).replace(/'/g, "'\\''")}'`;
68
+ }
69
+
70
+ function pythonVersion() {
71
+ const probe = [
72
+ "import sys",
73
+ "print(f'{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}')"
74
+ ].join("; ");
75
+ const result = run("python3", ["-c", probe]);
76
+ if (result.status !== 0) {
77
+ return null;
78
+ }
79
+ return result.stdout.trim();
80
+ }
81
+
82
+ function pythonIsAdequate(version) {
83
+ if (!version) {
84
+ return false;
85
+ }
86
+ const parts = version.split(".").map((part) => Number(part));
87
+ if (parts.length < 2 || parts.some((part) => Number.isNaN(part))) {
88
+ return false;
89
+ }
90
+ return parts[0] > 3 || (parts[0] === 3 && parts[1] >= 8);
91
+ }
92
+
93
+ function claudeDir() {
94
+ return process.env.CLAUDE_DIR || path.join(os.homedir(), ".claude");
95
+ }
96
+
97
+ function ensureWritableClaudeDir() {
98
+ const dir = claudeDir();
99
+ try {
100
+ fs.mkdirSync(dir, { recursive: true });
101
+ fs.accessSync(dir, fs.constants.W_OK);
102
+ return true;
103
+ } catch (error) {
104
+ return false;
105
+ }
106
+ }
107
+
108
+ function doctor({ fatal = false } = {}) {
109
+ let failures = 0;
110
+ const recordFailure = (message) => {
111
+ failures += 1;
112
+ if (fatal) {
113
+ fail(message);
114
+ }
115
+ console.error(`FAIL: ${message}`);
116
+ };
117
+
118
+ if (isSupportedPlatform()) {
119
+ ok(`supported OS: ${process.platform}`);
120
+ } else {
121
+ recordFailure("Coach Claw supports macOS/Linux only.");
122
+ }
123
+
124
+ if (commandExists("bash")) {
125
+ ok("bash available");
126
+ } else {
127
+ recordFailure("bash not found in PATH.");
128
+ }
129
+
130
+ if (commandExists("git")) {
131
+ ok("git available");
132
+ } else {
133
+ recordFailure("git not found in PATH.");
134
+ }
135
+
136
+ if (commandExists("python3")) {
137
+ const version = pythonVersion();
138
+ if (pythonIsAdequate(version)) {
139
+ ok(`python3 ${version} available`);
140
+ } else {
141
+ recordFailure("Coach Claw needs python3 >= 3.8. Install Python 3, then retry.");
142
+ }
143
+ } else {
144
+ recordFailure("Coach Claw needs python3 >= 3.8. Install Python 3, then retry.");
145
+ }
146
+
147
+ if (ensureWritableClaudeDir()) {
148
+ ok(`Claude dir writable: ${claudeDir()}`);
149
+ } else {
150
+ recordFailure(`Claude dir is not writable: ${claudeDir()}`);
151
+ }
152
+
153
+ if (commandExists("claude")) {
154
+ ok("claude CLI available for weekly /coach-insights refresh");
155
+ } else {
156
+ warn("claude CLI not found; install still works, but weekly /coach-insights refresh needs Claude Code on PATH.");
157
+ }
158
+
159
+ const installScript = path.join(ROOT, "install.sh");
160
+ if (fs.existsSync(installScript)) {
161
+ ok("install.sh present");
162
+ } else {
163
+ recordFailure("package is missing install.sh.");
164
+ }
165
+
166
+ const liveCoach = path.join(claudeDir(), "coach");
167
+ const liveSessionHook = path.join(claudeDir(), "hooks", "coach-session-start.py");
168
+ const livePromptHook = path.join(claudeDir(), "hooks", "coach-user-prompt.py");
169
+ if (fs.existsSync(liveCoach)) {
170
+ ok(`installed coach dir present: ${liveCoach}`);
171
+ } else {
172
+ warn("Coach is not installed yet; run `coach-claw install --seed`.");
173
+ }
174
+ if (fs.existsSync(liveSessionHook) && fs.existsSync(livePromptHook)) {
175
+ ok("installed hooks present");
176
+ } else {
177
+ warn("installed hooks not found yet; run `coach-claw install --seed`.");
178
+ }
179
+
180
+ if (failures > 0) {
181
+ process.exit(1);
182
+ }
183
+ }
184
+
185
+ function runInstall(args) {
186
+ doctor({ fatal: true });
187
+ const script = path.join(ROOT, "install.sh");
188
+ const result = run("bash", [script, ...args], { stdio: "inherit" });
189
+ process.exit(result.status === null ? 1 : result.status);
190
+ }
191
+
192
+ function runLaunchd() {
193
+ if (process.platform !== "darwin") {
194
+ fail(
195
+ "launchd is macOS-only. On Linux, add this cron entry: " +
196
+ "0 4 * * * $HOME/.claude/coach/bin/insights.sh 1d >> /tmp/claude-coach.log 2>&1"
197
+ );
198
+ }
199
+ doctor({ fatal: true });
200
+ const script = path.join(ROOT, "install-launchd.sh");
201
+ const result = run("bash", [script], { stdio: "inherit" });
202
+ process.exit(result.status === null ? 1 : result.status);
203
+ }
204
+
205
+ function runConfig(args) {
206
+ // configure.py lives in the LIVE install (claudeDir()/coach/bin/), not
207
+ // the npm package — the npx cache is read-only and the script needs
208
+ // to import its sibling modules (themes, statusline_variants,
209
+ // user_config) which only exist in the install target.
210
+ const script = path.join(claudeDir(), "coach", "bin", "configure.py");
211
+ if (!fs.existsSync(script)) {
212
+ fail(
213
+ "coach-claw config: Coach Claw isn't installed yet. " +
214
+ "Run: npx @rm0nroe/coach-claw@latest install"
215
+ );
216
+ }
217
+ // If the user has CLAUDE_DIR set (custom install location), propagate
218
+ // COACH_CONFIG_DIR so user_config.py writes to the matching coach dir
219
+ // instead of falling back to ~/.claude/coach.
220
+ const env = { ...process.env };
221
+ if (process.env.CLAUDE_DIR) {
222
+ env.COACH_CONFIG_DIR = path.join(process.env.CLAUDE_DIR, "coach");
223
+ }
224
+ const result = spawnSync("python3", [script, ...args], {
225
+ stdio: "inherit",
226
+ env,
227
+ });
228
+ process.exit(result.status === null ? 1 : result.status);
229
+ }
230
+
231
+ const [command, ...args] = process.argv.slice(2);
232
+
233
+ switch (command || "help") {
234
+ case "doctor":
235
+ doctor();
236
+ break;
237
+ case "install":
238
+ runInstall(args);
239
+ break;
240
+ case "launchd":
241
+ runLaunchd();
242
+ break;
243
+ case "config":
244
+ runConfig(args);
245
+ break;
246
+ case "--version":
247
+ case "-v":
248
+ case "version":
249
+ console.log(VERSION);
250
+ break;
251
+ case "--help":
252
+ case "-h":
253
+ case "help":
254
+ usage();
255
+ break;
256
+ default:
257
+ usage();
258
+ fail(`unknown command: ${command}`);
259
+ }
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@rm0nroe/coach-claw",
3
+ "version": "1.0.6",
4
+ "description": "A self-evolving coaching layer for Claude Code.",
5
+ "license": "MIT",
6
+ "homepage": "https://rm0nroe.github.io/coach-claw/",
7
+ "bugs": {
8
+ "url": "https://github.com/rm0nroe/coach-claw/issues"
9
+ },
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://github.com/rm0nroe/coach-claw.git"
13
+ },
14
+ "publishConfig": {
15
+ "access": "public"
16
+ },
17
+ "bin": {
18
+ "coach-claw": "npm/coach-claw.js"
19
+ },
20
+ "files": [
21
+ "coach/",
22
+ "hooks/",
23
+ "skills/",
24
+ "launchd/",
25
+ "npm/",
26
+ "install.sh",
27
+ "install-launchd.sh",
28
+ "settings-snippet.json",
29
+ "requirements.txt",
30
+ "README.md",
31
+ "LICENSE"
32
+ ],
33
+ "os": [
34
+ "darwin",
35
+ "linux"
36
+ ],
37
+ "engines": {
38
+ "node": ">=18"
39
+ },
40
+ "scripts": {
41
+ "doctor": "node npm/coach-claw.js doctor",
42
+ "test": "python3 -m pytest coach/tests",
43
+ "pack:dry": "npm pack --dry-run --json"
44
+ },
45
+ "keywords": [
46
+ "claude-code",
47
+ "coach",
48
+ "claude",
49
+ "hooks",
50
+ "cli"
51
+ ]
52
+ }
@@ -0,0 +1,11 @@
1
+ # Claude Code Coach dependencies.
2
+ #
3
+ # install.sh installs these automatically via `python3 -m pip install --user`.
4
+ # Listed here for transparency.
5
+
6
+ # Required at runtime (merge.py, stats.py, status.py, bank.py, hooks)
7
+ pyyaml>=5.4
8
+
9
+ # Required only to run the test suite (coach/tests/).
10
+ # pytest isn't installed by default — add with: python3 -m pip install --user pytest
11
+ # pytest>=7.0
@@ -0,0 +1,31 @@
1
+ {
2
+ "_comment": "Merge the 'hooks' block below into your ~/.claude/settings.json. Optionally add the statusLine block too if you do not already have a custom statusline. ./install.sh does this automatically for hooks, rewrites `python3` to the absolute path it resolved at install time, substitutes that path into the statusline wrapper, and adds statusLine only when one is absent. If you are merging by hand, replace `python3` in the hook command strings with the full path from `command -v python3` — settings.json runs in a non-interactive shell where PATH is not guaranteed to include Homebrew / pyenv. Do NOT replace your whole settings.json — add these keys alongside your existing 'enabledPlugins', 'permissions', etc.",
3
+ "hooks": {
4
+ "SessionStart": [
5
+ {
6
+ "hooks": [
7
+ {
8
+ "type": "command",
9
+ "command": "python3 ~/.claude/hooks/coach-session-start.py",
10
+ "timeout": 3
11
+ }
12
+ ]
13
+ }
14
+ ],
15
+ "UserPromptSubmit": [
16
+ {
17
+ "hooks": [
18
+ {
19
+ "type": "command",
20
+ "command": "python3 ~/.claude/hooks/coach-user-prompt.py",
21
+ "timeout": 2
22
+ }
23
+ ]
24
+ }
25
+ ]
26
+ },
27
+ "statusLine": {
28
+ "type": "command",
29
+ "command": "bash ~/.claude/coach/default-statusline-command.sh"
30
+ }
31
+ }
@@ -0,0 +1,107 @@
1
+ ---
2
+ description: "Toggle the autonomous Coach Claw on/off, inspect state, or uninstall. Usage: /coach <on|off|status|uninstall>"
3
+ ---
4
+
5
+ The Coach is an autonomous system: `/coach-insights` runs daily and maintains
6
+ `~/.claude/coach/profile.yaml`; a `SessionStart` hook injects the active
7
+ profile as context so Claude can append at most one observational footnote
8
+ per session when your behavior matches a tracked pattern.
9
+
10
+ This skill is the **control surface** for that system. It does NOT review
11
+ proposed changes, approve deltas, or edit entries by hand. It only toggles
12
+ and inspects. Profile updates happen autonomously through `/coach-insights`.
13
+
14
+ ## Argument
15
+
16
+ The argument after `/coach` is one of: `on` | `off` | `status` | `uninstall`.
17
+ If no argument is provided, default to `status`.
18
+
19
+ ## Behavior per argument
20
+
21
+ ### `off`
22
+ 1. Create `~/.claude/coach/.disabled` as an empty flag file.
23
+ 2. Confirm with one line: "Coach disabled. Hook will exit silently on SessionStart."
24
+
25
+ ### `on`
26
+ 1. Remove `~/.claude/coach/.disabled` if present.
27
+ 2. Confirm with one line: "Coach enabled."
28
+
29
+ ### `status` (default)
30
+ Invoke `~/.claude/coach/bin/status.py` via Bash and print its output verbatim.
31
+ The script emits an ANSI-colored breakdown:
32
+
33
+ - Current level, XP total, wide progress bar, distance to next level
34
+ - Lifetime XP breakdown (graduations, longest active clean streak, banked
35
+ session XP with 10:1 discount)
36
+ - Session XP breakdown (test runs, commits, unique skills invoked in the
37
+ current session — live from the most recently modified transcript)
38
+ - "How to earn more" cheat sheet
39
+ - Profile state (active / probationary / retired counts + probationary
40
+ streak values)
41
+ - Last `/coach-insights` run summary
42
+
43
+ Then, as a separate short line below the report, also surface:
44
+ - Whether `.disabled` is present (`Coach disabled` / `Coach enabled`)
45
+ - Whether `COACH_DISABLE=1` env is set (note the override)
46
+
47
+ Use only `python3 ~/.claude/coach/bin/status.py` and bash existence
48
+ checks for `.disabled` / env var. Do not mutate anything.
49
+
50
+ ### `uninstall`
51
+ This is a reversible but disruptive action. Compute `TS=$(date +%Y%m%d-%H%M%S)`
52
+ once and reuse it for every `.bak.<ts>` / `.uninstalled.<ts>` suffix below so
53
+ the whole uninstall is one timestamped batch. Before doing anything:
54
+ 1. Ask the user for explicit confirmation ("This will move `~/.claude/coach/`
55
+ to `~/.claude/coach.bak.<TS>/`, move both coach hook scripts to
56
+ `.uninstalled.<TS>` (reversible rename), remove the coach
57
+ `SessionStart` + `UserPromptSubmit` hook entries from `settings.json`,
58
+ and unload + rename the daily-insights launchd plist (macOS) or print
59
+ the cron line for you to remove (Linux). Proceed?").
60
+ 2. Only on explicit "yes":
61
+ a. `mv ~/.claude/coach ~/.claude/coach.bak.<TS>` — **never `rm`**.
62
+ Investigate before deleting any application-data directory; prefer
63
+ a reversible rename to preserve the data.
64
+ b. Move the hook scripts so the live `~/.claude/hooks/` no longer
65
+ carries dead coach hooks (settings.json no longer references them
66
+ after step d, but stale files are confusing in any future debug):
67
+ - `mv ~/.claude/hooks/coach-session-start.py ~/.claude/hooks/coach-session-start.py.uninstalled.<TS>`
68
+ - `mv ~/.claude/hooks/coach-user-prompt.py ~/.claude/hooks/coach-user-prompt.py.uninstalled.<TS>`
69
+ Skip silently if a file isn't present.
70
+ c. Back up settings: `cp ~/.claude/settings.json ~/.claude/settings.json.bak.<TS>`.
71
+ d. Edit `~/.claude/settings.json` to remove only coach hook entries:
72
+ `hooks.SessionStart` commands containing `coach-session-start.py` and
73
+ `hooks.UserPromptSubmit` commands containing `coach-user-prompt.py`.
74
+ Preserve all other hooks and the rest of the file verbatim. Validate
75
+ the result is still valid JSON (`python3 -c "import json; json.load(open('...'))"`).
76
+ e. Unregister the daily Coach insights scheduler.
77
+ - macOS: if `~/Library/LaunchAgents/com.local.claude-coach.plist` exists,
78
+ `launchctl unload ~/Library/LaunchAgents/com.local.claude-coach.plist 2>/dev/null`
79
+ then `mv ~/Library/LaunchAgents/com.local.claude-coach.plist
80
+ ~/Library/LaunchAgents/com.local.claude-coach.plist.uninstalled.<TS>`.
81
+ The unload is best-effort (already-unloaded plists return non-zero
82
+ but cause no harm); the `mv` is the load-bearing step that prevents
83
+ the next `launchctl bootstrap` from re-loading it.
84
+ - Linux: `crontab` cannot be safely edited from a skill (the user's
85
+ crontab may have many lines we shouldn't touch). Print:
86
+ "Run `crontab -e` and remove the line containing
87
+ `coach/bin/insights.sh`."
88
+ f. Confirm with: "Coach uninstalled. Backups at:
89
+ `~/.claude/coach.bak.<TS>/`,
90
+ `~/.claude/hooks/coach-*.py.uninstalled.<TS>`,
91
+ `~/.claude/settings.json.bak.<TS>`,
92
+ `~/Library/LaunchAgents/com.local.claude-coach.plist.uninstalled.<TS>` (macOS).
93
+ To restore: `mv` each backup back to its original path and re-run
94
+ `launchctl load ...plist` (macOS) or re-add the cron line (Linux)."
95
+
96
+ ## Rules
97
+
98
+ - Never edit `profile.yaml` entries by hand from this skill. The autonomous
99
+ loop is the point; hand-edits create drift.
100
+ - Never `rm -rf` anything. Use `mv` to a `.bak` path.
101
+ - Never touch the `enabledPlugins`, `permissions`, or `env` blocks of
102
+ `settings.json`. Only the `hooks` block.
103
+ - If the user asks to change nudge wording, cooldown, decay rate, cap,
104
+ or thresholds, point them at the constants at the top of
105
+ `~/.claude/hooks/coach-session-start.py` (hook-side) or
106
+ `~/.claude/skills/coach-insights/SKILL.md` (runner-side). Don't invent a config
107
+ file — the constants are the config.
@@ -0,0 +1,78 @@
1
+ ---
2
+ description: "Run an LLM-driven Coach insights pass — refreshes /insights facets, aggregates structured friction/wins, merges into profile. Usage: /coach-insights [--dry-run]"
3
+ disable-model-invocation: true
4
+ ---
5
+
6
+ Thin wrapper around `~/.claude/coach/bin/insights-llm.sh --force`. The
7
+ underlying script (also fired automatically by the SessionStart hook on
8
+ a 7-day cadence) refreshes the `/insights` facets sidecars, aggregates
9
+ their stable enum keys deterministically, and hands detections to
10
+ `merge.py`. `--force` bypasses the 7-day cooldown so a manual run
11
+ always does work.
12
+
13
+ ## What this does
14
+
15
+ 1. Spawns `claude -p "/insights"` with `COACH_DISABLE=1` to refresh
16
+ `~/.claude/usage-data/facets/<uuid>.json` sidecars. The CLI's stdout
17
+ is discarded — we run it for the side effect on disk only.
18
+ 2. Pipes the facets directory through `aggregate_facets.py` to convert
19
+ stable enum keys (`friction_counts.*`, `primary_success`) into
20
+ detections JSON with kebab-case ids (`misunderstood_request` →
21
+ `misunderstood-request`). Threshold-gated: ≥25% of sessions for
22
+ negatives, ≥60% for positives. Capped at 8 detections per run.
23
+ 3. Hands detections to `merge.py` with `--run-id "insights-weekly-<ts>"`
24
+ so downstream consumers can distinguish from the daily deterministic
25
+ path's `insights-<ts>` runs.
26
+ 4. Auto-commits the profile change and touches
27
+ `~/.claude/coach/.last_weekly_insights` to throttle the next
28
+ automatic trigger.
29
+
30
+ ## Privacy
31
+
32
+ - **Daily cron path: local-only, zero network.** `analyze.py +
33
+ redact.py` over redacted transcripts. Zero LLM cost.
34
+ - **Weekly path + on-demand `/coach-insights`:** triggers Anthropic-side
35
+ `/insights` once per 7 days (via `claude -p`) to refresh structured
36
+ facets data. Coach itself does not independently upload transcripts;
37
+ the nested `/insights` refresh is an Anthropic-side Claude Code
38
+ operation that runs inside the user's existing authenticated session
39
+ and writes only to local sidecar files. Coach reads those local
40
+ `facets/*.json` files. The LLM call's output is discarded; only the
41
+ sidecar JSON refresh matters. `profile.yaml` stays local.
42
+
43
+ ## Arguments
44
+
45
+ - `--dry-run` (optional): aggregate facets and print the detections
46
+ JSON without invoking `merge.py` or touching the throttle marker.
47
+
48
+ ## Steps
49
+
50
+ ```bash
51
+ ~/.claude/coach/bin/insights-llm.sh --force "$@"
52
+ ```
53
+
54
+ That's it. Pass `--dry-run` through if the user supplied it.
55
+
56
+ Capture the script's stdout, then summarize for the user:
57
+
58
+ ```
59
+ /coach-insights <run_id>
60
+ detections: <N>
61
+ → <changelog line from merge.py stdout>
62
+ ```
63
+
64
+ If `--dry-run`, report the dry-run banner instead and skip the changelog
65
+ line.
66
+
67
+ ## Rules
68
+
69
+ - **Never invent detections.** Zero is a valid output. The aggregator
70
+ emits `[]` when no enum key crosses its threshold; do not pad.
71
+ - **Never edit `profile.yaml` directly.** Only `merge.py` mutates it.
72
+ - **No translation, no fuzzy matching.** The aggregator consumes
73
+ facets enum keys 1:1 (Anthropic's data contract), so the manual
74
+ path and the auto-spawned weekly path always emit the same ids
75
+ for the same evidence.
76
+ - **One run per `RUN_ID`.** The script generates a unique
77
+ `insights-weekly-<ts>` per invocation; do not re-run with the same
78
+ id.
@@ -0,0 +1,149 @@
1
+ ---
2
+ description: "Customize the Coach Claw display — statusline variant, rank-name theme, ELO range. Usage: /config [show|preview|statusline <variant>|theme <name>|elo <min> <max>|reset]"
3
+ ---
4
+
5
+ The Coach reads `~/.claude/coach/.user_config.json` at every render. This
6
+ skill is the slash-command surface for editing it without touching files
7
+ by hand. Three things are tunable:
8
+
9
+ - **Statusline variant** — how the trailing coach segment renders. Four
10
+ options: `crystal`, `pips`, `slash`, `forge`.
11
+ - **Theme** — the 50-name level ladder. Twelve options: `craft`
12
+ (default), `forge`, `cosmic`, `ocean`, `skyrim`, `marvel`, `dc`,
13
+ `finalfantasy`, `military`, `lotr`, `starwars`, `hacker`.
14
+ - **ELO range** — `elo_min` and `elo_max` (defaults 1000 → 2800). The
15
+ rating is linearly interpolated across the 50-level ladder.
16
+
17
+ The threshold curve (XP per level) is not configurable — keeping it
18
+ fixed means existing XP totals never trigger retroactive level-ups.
19
+
20
+ ## Argument
21
+
22
+ The argument after `/config` is one of:
23
+ `show` | `preview` | `statusline <variant>` | `theme <name>` |
24
+ `elo <min> <max>` | `reset`. If no argument is provided, default to `show`.
25
+
26
+ ## Behavior per argument
27
+
28
+ ### `show` (default)
29
+
30
+ Read the current config and print a tidy summary. No mutations.
31
+
32
+ ```bash
33
+ python3 - <<'PY'
34
+ import os, sys
35
+ sys.path.insert(0, os.path.expanduser("~/.claude/coach/bin"))
36
+ from user_config import load
37
+ from statusline_variants import list_variants
38
+ from themes import list_themes
39
+ cfg = load()
40
+ print(f" Variant : {cfg['statusline_variant']} (options: {', '.join(list_variants())})")
41
+ print(f" Theme : {cfg['theme']} (options: {', '.join(list_themes())})")
42
+ print(f" ELO : {cfg['elo_min']} → {cfg['elo_max']}")
43
+ PY
44
+ ```
45
+
46
+ ### `preview`
47
+
48
+ Render every variant × the user's current theme, plus every theme name
49
+ at L1 / L25 / L50, so the user can see what the options look like
50
+ side-by-side before committing. Pure read — no mutations.
51
+
52
+ Delegates to `coach/bin/configure.py preview` so the slash command and
53
+ the `npx coach-claw config preview` terminal command run literally the
54
+ same code — output is byte-identical, no drift surface.
55
+
56
+ ```bash
57
+ python3 ~/.claude/coach/bin/configure.py preview
58
+ ```
59
+
60
+ ### `statusline <variant>`
61
+
62
+ Set `statusline_variant`. Validate against the registered set; on a
63
+ typo, print the valid options and exit without mutating.
64
+
65
+ ```bash
66
+ VARIANT="$1" # the second word from the user's /config invocation
67
+ python3 - "$VARIANT" <<'PY'
68
+ import os, sys
69
+ sys.path.insert(0, os.path.expanduser("~/.claude/coach/bin"))
70
+ from user_config import update, VALID_VARIANTS
71
+ v = sys.argv[1] if len(sys.argv) > 1 else ""
72
+ if v not in VALID_VARIANTS:
73
+ print(f"unknown variant {v!r}. valid: {sorted(VALID_VARIANTS)}")
74
+ sys.exit(1)
75
+ update(statusline_variant=v)
76
+ print(f"statusline → {v}")
77
+ PY
78
+ ```
79
+
80
+ Confirm with one line: `Statusline updated to <variant>. Open a new prompt to see it.`
81
+
82
+ ### `theme <name>`
83
+
84
+ Set `theme` analogously. Same shape as `statusline` — validate, update,
85
+ confirm.
86
+
87
+ ```bash
88
+ THEME="$1"
89
+ python3 - "$THEME" <<'PY'
90
+ import os, sys
91
+ sys.path.insert(0, os.path.expanduser("~/.claude/coach/bin"))
92
+ from user_config import update, VALID_THEMES
93
+ t = sys.argv[1] if len(sys.argv) > 1 else ""
94
+ if t not in VALID_THEMES:
95
+ print(f"unknown theme {t!r}. valid: {sorted(VALID_THEMES)}")
96
+ sys.exit(1)
97
+ update(theme=t)
98
+ print(f"theme → {t}")
99
+ PY
100
+ ```
101
+
102
+ ### `elo <min> <max>`
103
+
104
+ Set the ELO interpolation range. Validate `0 < min < max`. Same
105
+ update + confirm shape.
106
+
107
+ ```bash
108
+ MIN="$1"; MAX="$2"
109
+ python3 - "$MIN" "$MAX" <<'PY'
110
+ import os, sys
111
+ sys.path.insert(0, os.path.expanduser("~/.claude/coach/bin"))
112
+ from user_config import update
113
+ try:
114
+ emin = int(sys.argv[1]); emax = int(sys.argv[2])
115
+ except (IndexError, ValueError):
116
+ print("usage: /config elo <min> <max>"); sys.exit(1)
117
+ if not (0 < emin < emax):
118
+ print(f"elo_min ({emin}) must be a positive int less than elo_max ({emax})")
119
+ sys.exit(1)
120
+ update(elo_min=emin, elo_max=emax)
121
+ print(f"elo → {emin} → {emax}")
122
+ PY
123
+ ```
124
+
125
+ ### `reset`
126
+
127
+ Delete `~/.claude/coach/.user_config.json` so all settings revert to
128
+ their defaults (`crystal` + `craft` + `1000–2800`). Ask for explicit
129
+ confirmation first.
130
+
131
+ ```bash
132
+ rm -f "$HOME/.claude/coach/.user_config.json"
133
+ echo "Config reset. Defaults: crystal + craft + 1000-2800."
134
+ ```
135
+
136
+ ## Rules
137
+
138
+ - Never edit `profile.yaml`, `banked_sessions.json`, or any `.pending_*`
139
+ marker via this skill — those are autonomous-loop state, not user
140
+ config.
141
+ - Never invent new variant or theme names. The validators in
142
+ `user_config.py` reject unknown values; show the user the registered
143
+ options instead.
144
+ - For `elo`: the threshold curve is hardcoded; only the ELO
145
+ interpolation range is user-tunable. If asked to change XP-per-level
146
+ thresholds, point at `coach/bin/stats.py:_build_level_ladder` and
147
+ warn that doing so retroactively shifts existing user levels.
148
+ - After any mutation, remind the user: "Open a new prompt or restart
149
+ Claude Code to see the new statusline render."