@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.
- package/LICENSE +21 -0
- package/README.md +311 -0
- package/coach/README.md +99 -0
- package/coach/bin/aggregate_facets.py +274 -0
- package/coach/bin/analyze.py +678 -0
- package/coach/bin/bank.py +247 -0
- package/coach/bin/banner_themes.py +645 -0
- package/coach/bin/coach_paths.py +33 -0
- package/coach/bin/coexistence_check.py +129 -0
- package/coach/bin/configure.py +245 -0
- package/coach/bin/cron_check.py +81 -0
- package/coach/bin/default_statusline.py +135 -0
- package/coach/bin/doctor.py +663 -0
- package/coach/bin/insights-llm.sh +264 -0
- package/coach/bin/insights.sh +163 -0
- package/coach/bin/insights_window.py +111 -0
- package/coach/bin/marker_io.py +154 -0
- package/coach/bin/merge.py +671 -0
- package/coach/bin/redact.py +86 -0
- package/coach/bin/render_env.py +148 -0
- package/coach/bin/reward_hints.py +87 -0
- package/coach/bin/run-insights.sh +20 -0
- package/coach/bin/run_with_lock.py +85 -0
- package/coach/bin/scoring.py +260 -0
- package/coach/bin/skill_inventory.py +215 -0
- package/coach/bin/stats.py +459 -0
- package/coach/bin/status.py +293 -0
- package/coach/bin/statusline_self_patch.py +205 -0
- package/coach/bin/statusline_variants.py +146 -0
- package/coach/bin/statusline_wrap.py +244 -0
- package/coach/bin/statusline_wrap_action.py +460 -0
- package/coach/bin/switch_to_plugin.py +256 -0
- package/coach/bin/themes.py +256 -0
- package/coach/bin/user_config.py +176 -0
- package/coach/bin/xp_accounting.py +98 -0
- package/coach/changelog.md +4 -0
- package/coach/default-statusline-command.sh +19 -0
- package/coach/default-statusline-wrap-command.sh +15 -0
- package/coach/profile.yaml +37 -0
- package/coach/tests/conftest.py +13 -0
- package/coach/tests/test_aggregate_facets.py +379 -0
- package/coach/tests/test_analyze_aggregate.py +153 -0
- package/coach/tests/test_analyze_redaction.py +105 -0
- package/coach/tests/test_analyze_strengths.py +165 -0
- package/coach/tests/test_bank_atomic_write.py +61 -0
- package/coach/tests/test_bank_concurrency.py +126 -0
- package/coach/tests/test_banner_themes.py +981 -0
- package/coach/tests/test_celebrate_dedup.py +409 -0
- package/coach/tests/test_coach_paths.py +50 -0
- package/coach/tests/test_coexistence_check.py +128 -0
- package/coach/tests/test_configure.py +258 -0
- package/coach/tests/test_cron_check.py +118 -0
- package/coach/tests/test_cron_nudge_hook.py +134 -0
- package/coach/tests/test_detection_parity.py +105 -0
- package/coach/tests/test_doctor.py +595 -0
- package/coach/tests/test_hook_bespoke_dispatch.py +288 -0
- package/coach/tests/test_hook_module_resolution.py +116 -0
- package/coach/tests/test_hook_relevance.py +996 -0
- package/coach/tests/test_hook_render_env.py +364 -0
- package/coach/tests/test_hook_session_id_guard.py +160 -0
- package/coach/tests/test_insights_llm.py +759 -0
- package/coach/tests/test_insights_llm_venv_path.py +109 -0
- package/coach/tests/test_insights_window.py +237 -0
- package/coach/tests/test_install.py +1150 -0
- package/coach/tests/test_install_pyyaml_fallback.py +142 -0
- package/coach/tests/test_marker_consumption.py +167 -0
- package/coach/tests/test_marker_writer_locking.py +305 -0
- package/coach/tests/test_merge.py +413 -0
- package/coach/tests/test_no_broken_mktemp.py +90 -0
- package/coach/tests/test_render_env.py +137 -0
- package/coach/tests/test_render_env_glyphs.py +119 -0
- package/coach/tests/test_reward_hints.py +59 -0
- package/coach/tests/test_scoring.py +147 -0
- package/coach/tests/test_session_start_weekly_trigger.py +92 -0
- package/coach/tests/test_skill_inventory.py +368 -0
- package/coach/tests/test_stats_hybrid.py +142 -0
- package/coach/tests/test_status_accounting.py +41 -0
- package/coach/tests/test_statusline_failsafe.py +70 -0
- package/coach/tests/test_statusline_self_patch.py +261 -0
- package/coach/tests/test_statusline_variants.py +110 -0
- package/coach/tests/test_statusline_wrap.py +196 -0
- package/coach/tests/test_statusline_wrap_action.py +408 -0
- package/coach/tests/test_switch_to_plugin.py +360 -0
- package/coach/tests/test_themes.py +104 -0
- package/coach/tests/test_user_config.py +160 -0
- package/coach/tests/test_wrap_announce_hook.py +130 -0
- package/coach/tests/test_xp_accounting.py +55 -0
- package/hooks/coach-session-start.py +536 -0
- package/hooks/coach-user-prompt.py +2288 -0
- package/install-launchd.sh +102 -0
- package/install.sh +597 -0
- package/launchd/com.local.claude-coach.plist.template +34 -0
- package/launchd/run-insights.sh +20 -0
- package/npm/coach-claw.js +259 -0
- package/package.json +52 -0
- package/requirements.txt +11 -0
- package/settings-snippet.json +31 -0
- package/skills/coach/SKILL.md +107 -0
- package/skills/coach-insights/SKILL.md +78 -0
- 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
|
+
}
|
package/requirements.txt
ADDED
|
@@ -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."
|