@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
package/install.sh
ADDED
|
@@ -0,0 +1,597 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Coach Claw — installer
|
|
3
|
+
#
|
|
4
|
+
# Copies the coach binaries, hooks, and skills into ~/.claude/ (or CLAUDE_DIR),
|
|
5
|
+
# registers
|
|
6
|
+
# the SessionStart + UserPromptSubmit hooks plus a statusLine when one is not
|
|
7
|
+
# already configured in settings.json, and git-inits the coach data directory
|
|
8
|
+
# for rollback.
|
|
9
|
+
#
|
|
10
|
+
# Idempotent — re-running is safe:
|
|
11
|
+
# • existing coach dir is moved to coach.bak.<ts> before copy
|
|
12
|
+
# • hooks + settings.json are backed up to .bak.<ts> before patch
|
|
13
|
+
# • existing coach state is preserved (only ships template on fresh install)
|
|
14
|
+
# • settings.json hook/statusline entries are added only if not already present
|
|
15
|
+
#
|
|
16
|
+
# Uninstall: run `/coach uninstall` inside Claude Code after install,
|
|
17
|
+
# or see artifacts/infrastructure.md § Uninstall for manual steps.
|
|
18
|
+
|
|
19
|
+
set -uo pipefail
|
|
20
|
+
|
|
21
|
+
BUNDLE_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
22
|
+
CLAUDE_DIR="${CLAUDE_DIR:-$HOME/.claude}"
|
|
23
|
+
TS="$(date +%Y%m%d-%H%M%S)"
|
|
24
|
+
|
|
25
|
+
bold() { printf "\033[1m%s\033[0m\n" "$*"; }
|
|
26
|
+
note() { printf " %s\n" "$*"; }
|
|
27
|
+
warn() { printf "\033[33m WARN: %s\033[0m\n" "$*"; }
|
|
28
|
+
ok() { printf "\033[32m OK: %s\033[0m\n" "$*"; }
|
|
29
|
+
die() { printf "\033[31m ERROR: %s\033[0m\n" "$*"; exit 1; }
|
|
30
|
+
|
|
31
|
+
# --- Flags -------------------------------------------------------------------
|
|
32
|
+
# --seed / --bootstrap → after install, run insights.sh 7d once so the
|
|
33
|
+
# user doesn't have an empty profile on first Claude Code session.
|
|
34
|
+
|
|
35
|
+
SEED=0
|
|
36
|
+
NO_SEED=0
|
|
37
|
+
PRUNE_BACKUPS=1
|
|
38
|
+
FRESH=0
|
|
39
|
+
for arg in "$@"; do
|
|
40
|
+
case "$arg" in
|
|
41
|
+
--seed|--bootstrap) SEED=1 ;;
|
|
42
|
+
--no-seed) NO_SEED=1 ;;
|
|
43
|
+
--prune-backups) PRUNE_BACKUPS=1 ;;
|
|
44
|
+
--no-prune-backups) PRUNE_BACKUPS=0 ;;
|
|
45
|
+
--fresh) FRESH=1 ;;
|
|
46
|
+
-h|--help)
|
|
47
|
+
cat <<USAGE
|
|
48
|
+
Usage: $(basename "$0") [--seed | --no-seed] [--no-prune-backups] [--fresh]
|
|
49
|
+
|
|
50
|
+
--seed / --bootstrap After install, run insights.sh 7d against your
|
|
51
|
+
existing Claude Code transcripts so the profile
|
|
52
|
+
isn't empty on your first session. Safe to omit
|
|
53
|
+
(you can run ~/.claude/coach/bin/insights.sh 7d
|
|
54
|
+
later, or invoke /coach-insights inside Claude
|
|
55
|
+
Code).
|
|
56
|
+
|
|
57
|
+
--no-seed Explicitly skip seeding. The seed step is already
|
|
58
|
+
opt-in via --seed today, so this is forward-
|
|
59
|
+
compatible: scripted/CI installs use --no-seed
|
|
60
|
+
to make their intent unambiguous and to suppress
|
|
61
|
+
any future auto-seed prompt. Mutually exclusive
|
|
62
|
+
with --seed.
|
|
63
|
+
|
|
64
|
+
--no-prune-backups Keep all coach.bak.*, settings.json.bak.*, and
|
|
65
|
+
hooks/*.bak.* files. Default is to keep only the
|
|
66
|
+
3 most recent of each kind so ~/.claude/ doesn't
|
|
67
|
+
accumulate hundreds of backups across upgrades.
|
|
68
|
+
|
|
69
|
+
--fresh Skip recovery from a prior /coach uninstall. By
|
|
70
|
+
default, if no live coach/ dir exists but a
|
|
71
|
+
coach.bak.<ts>/ does, the most-recent backup is
|
|
72
|
+
restored before install (preserving profile,
|
|
73
|
+
throttle marker, and git history). --fresh forces
|
|
74
|
+
a true fresh install regardless.
|
|
75
|
+
USAGE
|
|
76
|
+
exit 0 ;;
|
|
77
|
+
*) warn "unknown arg: $arg (ignored)" ;;
|
|
78
|
+
esac
|
|
79
|
+
done
|
|
80
|
+
|
|
81
|
+
# Mutually-exclusive flag check — fail-fast BEFORE preflight so we don't
|
|
82
|
+
# touch anything on disk when the args are nonsense.
|
|
83
|
+
if [[ "$SEED" == "1" && "$NO_SEED" == "1" ]]; then
|
|
84
|
+
printf "\033[31m ERROR: --seed and --no-seed are mutually exclusive.\033[0m\n" >&2
|
|
85
|
+
exit 1
|
|
86
|
+
fi
|
|
87
|
+
|
|
88
|
+
# --- Preflight ---------------------------------------------------------------
|
|
89
|
+
|
|
90
|
+
bold "Preflight"
|
|
91
|
+
|
|
92
|
+
command -v python3 >/dev/null 2>&1 || die "python3 not found in PATH"
|
|
93
|
+
PY="$(command -v python3)"
|
|
94
|
+
note "python3: $PY"
|
|
95
|
+
|
|
96
|
+
# Resolve the Python version — need 3.8+ for f-strings + from __future__ annotations
|
|
97
|
+
PY_OK="$("$PY" - <<'PYEOF'
|
|
98
|
+
import sys
|
|
99
|
+
print("ok" if sys.version_info >= (3, 8) else f"too old ({sys.version_info[:2]})")
|
|
100
|
+
PYEOF
|
|
101
|
+
)"
|
|
102
|
+
[[ "$PY_OK" == "ok" ]] || die "python3 too old: $PY_OK. Coach needs Python 3.8+."
|
|
103
|
+
ok "python3 version adequate"
|
|
104
|
+
|
|
105
|
+
_have_yaml() { "$PY" -c "import yaml" 2>/dev/null; }
|
|
106
|
+
|
|
107
|
+
# Two-strategy fallback. pip --user covers system Python, pyenv, asdf,
|
|
108
|
+
# conda. --break-system-packages bypasses PEP 668 for Homebrew Python
|
|
109
|
+
# 3.12+ where the first attempt is rejected. (PyYAML is not in Homebrew
|
|
110
|
+
# core, so the brew formula is not a recovery path either.) We re-test
|
|
111
|
+
# `import yaml` after each pip call so a pip that "succeeded" but didn't
|
|
112
|
+
# actually land on PYTHONPATH still triggers the next step.
|
|
113
|
+
_install_pyyaml() {
|
|
114
|
+
note "Trying: $PY -m pip install --user pyyaml"
|
|
115
|
+
if "$PY" -m pip install --user pyyaml >/dev/null 2>&1 && _have_yaml; then
|
|
116
|
+
return 0
|
|
117
|
+
fi
|
|
118
|
+
|
|
119
|
+
note "Trying: $PY -m pip install --user --break-system-packages pyyaml"
|
|
120
|
+
note " (bypasses PEP 668 for the per-user install — safe for libraries)"
|
|
121
|
+
if "$PY" -m pip install --user --break-system-packages pyyaml >/dev/null 2>&1 && _have_yaml; then
|
|
122
|
+
return 0
|
|
123
|
+
fi
|
|
124
|
+
|
|
125
|
+
return 1
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if ! _have_yaml; then
|
|
129
|
+
warn "PyYAML not installed for $PY — attempting auto-install."
|
|
130
|
+
if ! _install_pyyaml; then
|
|
131
|
+
printf "\033[31m ERROR: could not install PyYAML automatically.\033[0m\n\n"
|
|
132
|
+
printf " Pick one of these and re-run ./install.sh:\n\n"
|
|
133
|
+
printf " %s -m pip install --user --break-system-packages pyyaml\n" "$PY"
|
|
134
|
+
printf " Bypasses PEP 668 for a per-user install. Safe.\n\n"
|
|
135
|
+
printf " %s -m venv ~/.coach-venv && ~/.coach-venv/bin/pip install pyyaml\n" "$PY"
|
|
136
|
+
printf " Then re-run install.sh with PATH=~/.coach-venv/bin:\$PATH so\n"
|
|
137
|
+
printf " the preflight uses the venv's python3.\n"
|
|
138
|
+
exit 1
|
|
139
|
+
fi
|
|
140
|
+
fi
|
|
141
|
+
ok "PyYAML available"
|
|
142
|
+
|
|
143
|
+
if [[ ! -d "$CLAUDE_DIR" ]]; then
|
|
144
|
+
warn "$CLAUDE_DIR does not exist — creating it. (Normally Claude Code creates this on first launch.)"
|
|
145
|
+
mkdir -p "$CLAUDE_DIR"
|
|
146
|
+
fi
|
|
147
|
+
ok "Claude config dir: $CLAUDE_DIR"
|
|
148
|
+
|
|
149
|
+
# --- Recover from prior /coach uninstall -----------------------------------
|
|
150
|
+
|
|
151
|
+
# If a previous /coach uninstall moved coach/ to coach.bak.<ts>/ (so coach/
|
|
152
|
+
# itself doesn't exist), revive the most-recent .bak before the existing
|
|
153
|
+
# preserve + .git restore flow runs. Without this, the install treats it
|
|
154
|
+
# as a true fresh install and silently drops the user's profile.yaml,
|
|
155
|
+
# .last_weekly_insights throttle marker (→ unintended paid /insights
|
|
156
|
+
# call on next SessionStart), and per-run git history. --fresh opts out.
|
|
157
|
+
RECOVERED_FROM=""
|
|
158
|
+
if [[ ! -e "$CLAUDE_DIR/coach" && "$FRESH" != "1" ]]; then
|
|
159
|
+
prior_bak="$(ls -dt "$CLAUDE_DIR"/coach.bak.* 2>/dev/null | head -1)"
|
|
160
|
+
if [[ -n "$prior_bak" && -d "$prior_bak" ]]; then
|
|
161
|
+
mv "$prior_bak" "$CLAUDE_DIR/coach"
|
|
162
|
+
RECOVERED_FROM="$(basename "$prior_bak")"
|
|
163
|
+
fi
|
|
164
|
+
fi
|
|
165
|
+
|
|
166
|
+
# Compute install mode for the banner: fresh / upgrade / recovered.
|
|
167
|
+
if [[ -n "$RECOVERED_FROM" ]]; then
|
|
168
|
+
MODE="recovered"
|
|
169
|
+
elif [[ -e "$CLAUDE_DIR/coach" ]]; then
|
|
170
|
+
MODE="upgrade"
|
|
171
|
+
else
|
|
172
|
+
MODE="fresh"
|
|
173
|
+
fi
|
|
174
|
+
|
|
175
|
+
case "$MODE" in
|
|
176
|
+
fresh)
|
|
177
|
+
bold "Install mode: fresh"
|
|
178
|
+
note "no prior coach/ or coach.bak.* detected"
|
|
179
|
+
;;
|
|
180
|
+
upgrade)
|
|
181
|
+
bold "Install mode: upgrade"
|
|
182
|
+
note "preserving live ~/.claude/coach/ state"
|
|
183
|
+
;;
|
|
184
|
+
recovered)
|
|
185
|
+
bold "Install mode: recovered"
|
|
186
|
+
note "restored prior uninstall from $RECOVERED_FROM"
|
|
187
|
+
note " (preserving profile, throttle marker, git history; pass --fresh to skip)"
|
|
188
|
+
;;
|
|
189
|
+
esac
|
|
190
|
+
|
|
191
|
+
# --- Recover launchd plist from prior /coach uninstall ---------------------
|
|
192
|
+
|
|
193
|
+
# Symmetric with the coach/ recovery above. /coach uninstall renames the
|
|
194
|
+
# plist to .uninstalled.<TS> and unloads it; without this block the user
|
|
195
|
+
# has to run install-launchd.sh separately to get the daily cron back —
|
|
196
|
+
# which is a real footgun (silent gap between reinstall and "Coach is
|
|
197
|
+
# autonomous again"). macOS-only; Linux uses cron, no plist.
|
|
198
|
+
#
|
|
199
|
+
# LAUNCHAGENTS_DIR override exists for test_install.py so the test can
|
|
200
|
+
# stage fixtures in a tmp dir without touching the real ~/Library/LaunchAgents/.
|
|
201
|
+
LAUNCHD_RECOVERED_FROM=""
|
|
202
|
+
LA_DIR="${LAUNCHAGENTS_DIR:-$HOME/Library/LaunchAgents}"
|
|
203
|
+
if [[ "$(uname)" == "Darwin" && "$FRESH" != "1" && -d "$LA_DIR" ]]; then
|
|
204
|
+
LIVE_PLIST="$LA_DIR/com.local.claude-coach.plist"
|
|
205
|
+
if [[ ! -e "$LIVE_PLIST" ]]; then
|
|
206
|
+
prior_plist="$(ls -dt "$LIVE_PLIST".uninstalled.* 2>/dev/null | head -1)"
|
|
207
|
+
if [[ -n "$prior_plist" && -f "$prior_plist" ]]; then
|
|
208
|
+
mv "$prior_plist" "$LIVE_PLIST"
|
|
209
|
+
LAUNCHD_RECOVERED_FROM="$(basename "$prior_plist")"
|
|
210
|
+
if command -v launchctl >/dev/null 2>&1; then
|
|
211
|
+
# unload first in case launchd somehow has a stale registration —
|
|
212
|
+
# mirrors the unload-then-load pattern in install-launchd.sh
|
|
213
|
+
launchctl unload "$LIVE_PLIST" 2>/dev/null || true
|
|
214
|
+
if launchctl load "$LIVE_PLIST" 2>/dev/null; then
|
|
215
|
+
note "restored launchd plist from $LAUNCHD_RECOVERED_FROM (job loaded)"
|
|
216
|
+
else
|
|
217
|
+
warn "restored launchd plist but launchctl load failed; run ./install-launchd.sh to reload"
|
|
218
|
+
fi
|
|
219
|
+
else
|
|
220
|
+
note "restored launchd plist from $LAUNCHD_RECOVERED_FROM (launchctl unavailable; load manually)"
|
|
221
|
+
fi
|
|
222
|
+
fi
|
|
223
|
+
fi
|
|
224
|
+
fi
|
|
225
|
+
|
|
226
|
+
# --- Backup existing pieces (if present) ------------------------------------
|
|
227
|
+
|
|
228
|
+
bold "Backups"
|
|
229
|
+
|
|
230
|
+
if [[ -e "$CLAUDE_DIR/coach" ]]; then
|
|
231
|
+
# Preserve user-owned state so reinstall updates code/docs without resetting
|
|
232
|
+
# progress, cooldowns, pending notifications, or the disabled flag.
|
|
233
|
+
TMP_PARENT="${TMPDIR:-/tmp}"
|
|
234
|
+
PRESERVE_DIR="$(mktemp -d "${TMP_PARENT%/}/coach-preserve.XXXXXX")" || \
|
|
235
|
+
die "failed to create temporary preserve directory"
|
|
236
|
+
for state_file in \
|
|
237
|
+
profile.yaml banked_sessions.json changelog.md log.ndjson \
|
|
238
|
+
.disabled .tip_state.json .level_state.json .last_session_start \
|
|
239
|
+
.last_weekly_insights .user_config.json \
|
|
240
|
+
.pending_* \
|
|
241
|
+
.statusline-wrap.json .statusline-wrap-disabled \
|
|
242
|
+
.statusline-wrap-announced .statusline-wrap-duplicate-detected; do
|
|
243
|
+
for src in "$CLAUDE_DIR/coach"/$state_file; do
|
|
244
|
+
[[ -e "$src" ]] || continue
|
|
245
|
+
cp -p "$src" "$PRESERVE_DIR/$(basename "$src")"
|
|
246
|
+
done
|
|
247
|
+
done
|
|
248
|
+
note "preserved existing coach state → $PRESERVE_DIR"
|
|
249
|
+
mv "$CLAUDE_DIR/coach" "$CLAUDE_DIR/coach.bak.$TS"
|
|
250
|
+
ok "moved existing coach dir → coach.bak.$TS"
|
|
251
|
+
fi
|
|
252
|
+
|
|
253
|
+
for hook in coach-session-start.py coach-user-prompt.py; do
|
|
254
|
+
if [[ -e "$CLAUDE_DIR/hooks/$hook" ]]; then
|
|
255
|
+
# Skip backup when bundle and live copy are byte-identical — re-running
|
|
256
|
+
# the installer with no code changes upstream would otherwise pile up
|
|
257
|
+
# an empty .bak.<ts> per run. Real diffs still get backed up.
|
|
258
|
+
if cmp -s "$BUNDLE_DIR/hooks/$hook" "$CLAUDE_DIR/hooks/$hook"; then
|
|
259
|
+
note "$hook unchanged — skipping backup"
|
|
260
|
+
else
|
|
261
|
+
cp "$CLAUDE_DIR/hooks/$hook" "$CLAUDE_DIR/hooks/$hook.bak.$TS"
|
|
262
|
+
ok "backed up existing hook: $hook"
|
|
263
|
+
fi
|
|
264
|
+
fi
|
|
265
|
+
done
|
|
266
|
+
|
|
267
|
+
if [[ -f "$CLAUDE_DIR/settings.json" ]]; then
|
|
268
|
+
# Snapshot first, then let the patch run. The post-patch cleanup block
|
|
269
|
+
# (after the python heredoc) drops this .bak when the patch is a no-op
|
|
270
|
+
# (settings.json byte-identical post-patch), so byte-identical reinstalls
|
|
271
|
+
# don't pile up backups.
|
|
272
|
+
cp "$CLAUDE_DIR/settings.json" "$CLAUDE_DIR/settings.json.bak.$TS"
|
|
273
|
+
ok "backed up settings.json"
|
|
274
|
+
fi
|
|
275
|
+
|
|
276
|
+
# v0.4.0 — Coach's old /insights skill shadowed Claude Code's built-in.
|
|
277
|
+
# Move the legacy skill aside so the built-in becomes reachable again.
|
|
278
|
+
# `mv` (not rm -rf) so any user customizations are recoverable.
|
|
279
|
+
if [[ -d "$CLAUDE_DIR/skills/insights" ]]; then
|
|
280
|
+
mv "$CLAUDE_DIR/skills/insights" "$CLAUDE_DIR/skills/insights.bak.$TS"
|
|
281
|
+
note "moved legacy /insights skill → skills/insights.bak.$TS"
|
|
282
|
+
note " (Coach's old skill no longer shadows Claude Code's built-in /insights)"
|
|
283
|
+
fi
|
|
284
|
+
|
|
285
|
+
# Claude Code's skill loader picks up any directory under `skills/` that
|
|
286
|
+
# contains a `SKILL.md`. A backup dir like `skills/insights.bak.<ts>/`
|
|
287
|
+
# would therefore become a live slash command (`/insights.bak.<ts>`) —
|
|
288
|
+
# polluting the catalog. Rename SKILL.md → SKILL.md.bak inside any
|
|
289
|
+
# `insights.bak.*/` dir so the loader skips it. Idempotent — runs every
|
|
290
|
+
# install, fixes both the freshly-moved bak from above AND any older
|
|
291
|
+
# bak dirs left over from a buggy v0.4.0 first-pass install.
|
|
292
|
+
for bak_skill in "$CLAUDE_DIR"/skills/insights.bak.*/SKILL.md; do
|
|
293
|
+
[[ -f "$bak_skill" ]] || continue
|
|
294
|
+
mv "$bak_skill" "${bak_skill}.bak"
|
|
295
|
+
note "defanged stale legacy SKILL.md → $(basename "$(dirname "$bak_skill")")/SKILL.md.bak"
|
|
296
|
+
done
|
|
297
|
+
|
|
298
|
+
# --- Copy files --------------------------------------------------------------
|
|
299
|
+
|
|
300
|
+
bold "Installing files"
|
|
301
|
+
|
|
302
|
+
mkdir -p "$CLAUDE_DIR/coach/bin" "$CLAUDE_DIR/coach/tests" \
|
|
303
|
+
"$CLAUDE_DIR/hooks" \
|
|
304
|
+
"$CLAUDE_DIR/skills/coach" "$CLAUDE_DIR/skills/coach-insights"
|
|
305
|
+
|
|
306
|
+
# Data files — profile is restored from preservation below if present
|
|
307
|
+
cp "$BUNDLE_DIR/coach/profile.yaml" "$CLAUDE_DIR/coach/profile.yaml"
|
|
308
|
+
cp "$BUNDLE_DIR/coach/changelog.md" "$CLAUDE_DIR/coach/changelog.md"
|
|
309
|
+
cp "$BUNDLE_DIR/coach/README.md" "$CLAUDE_DIR/coach/README.md"
|
|
310
|
+
[[ -f "$BUNDLE_DIR/coach/.gitignore" ]] && cp "$BUNDLE_DIR/coach/.gitignore" "$CLAUDE_DIR/coach/.gitignore"
|
|
311
|
+
touch "$CLAUDE_DIR/coach/log.ndjson"
|
|
312
|
+
|
|
313
|
+
# Binaries — ALL coach/bin/*.py + *.sh go in
|
|
314
|
+
for f in "$BUNDLE_DIR"/coach/bin/*.py "$BUNDLE_DIR"/coach/bin/*.sh; do
|
|
315
|
+
[[ -e "$f" ]] && cp "$f" "$CLAUDE_DIR/coach/bin/$(basename "$f")"
|
|
316
|
+
done
|
|
317
|
+
|
|
318
|
+
# Tests (optional — contributors can run pytest from ~/.claude/coach/)
|
|
319
|
+
for f in "$BUNDLE_DIR"/coach/tests/*.py; do
|
|
320
|
+
[[ -e "$f" ]] && cp "$f" "$CLAUDE_DIR/coach/tests/$(basename "$f")"
|
|
321
|
+
done
|
|
322
|
+
|
|
323
|
+
# Hooks — BOTH SessionStart AND UserPromptSubmit
|
|
324
|
+
cp "$BUNDLE_DIR/hooks/coach-session-start.py" "$CLAUDE_DIR/hooks/coach-session-start.py"
|
|
325
|
+
cp "$BUNDLE_DIR/hooks/coach-user-prompt.py" "$CLAUDE_DIR/hooks/coach-user-prompt.py"
|
|
326
|
+
|
|
327
|
+
# Skills (slash commands: /coach, /coach-insights, /config)
|
|
328
|
+
cp "$BUNDLE_DIR/skills/coach/SKILL.md" "$CLAUDE_DIR/skills/coach/SKILL.md"
|
|
329
|
+
cp "$BUNDLE_DIR/skills/coach-insights/SKILL.md" "$CLAUDE_DIR/skills/coach-insights/SKILL.md"
|
|
330
|
+
mkdir -p "$CLAUDE_DIR/skills/config"
|
|
331
|
+
[[ -f "$BUNDLE_DIR/skills/config/SKILL.md" ]] && \
|
|
332
|
+
cp "$BUNDLE_DIR/skills/config/SKILL.md" "$CLAUDE_DIR/skills/config/SKILL.md"
|
|
333
|
+
|
|
334
|
+
# Default statusline composition: model + context-bar + coach segment.
|
|
335
|
+
# `@PY@` is substituted with the resolved python path so the script
|
|
336
|
+
# doesn't depend on PATH at statusline-render time.
|
|
337
|
+
if [[ -f "$BUNDLE_DIR/coach/default-statusline-command.sh" ]]; then
|
|
338
|
+
sed "s|@PY@|$PY|g" "$BUNDLE_DIR/coach/default-statusline-command.sh" \
|
|
339
|
+
> "$CLAUDE_DIR/coach/default-statusline-command.sh"
|
|
340
|
+
fi
|
|
341
|
+
|
|
342
|
+
# Wrap-mode statusline trampoline (v0.1.4): symmetric installer-time
|
|
343
|
+
# substitution. settings.json:statusLine.command points at this when
|
|
344
|
+
# the user's existing statusLine got auto-wrapped by the wrap helper.
|
|
345
|
+
if [[ -f "$BUNDLE_DIR/coach/default-statusline-wrap-command.sh" ]]; then
|
|
346
|
+
sed "s|@PY@|$PY|g" "$BUNDLE_DIR/coach/default-statusline-wrap-command.sh" \
|
|
347
|
+
> "$CLAUDE_DIR/coach/default-statusline-wrap-command.sh"
|
|
348
|
+
fi
|
|
349
|
+
|
|
350
|
+
# Make the executables executable
|
|
351
|
+
chmod +x "$CLAUDE_DIR/coach/bin/"*.py "$CLAUDE_DIR/coach/bin/"*.sh \
|
|
352
|
+
"$CLAUDE_DIR/coach/default-statusline-command.sh" \
|
|
353
|
+
"$CLAUDE_DIR/coach/default-statusline-wrap-command.sh" \
|
|
354
|
+
"$CLAUDE_DIR/hooks/coach-session-start.py" \
|
|
355
|
+
"$CLAUDE_DIR/hooks/coach-user-prompt.py" 2>/dev/null || true
|
|
356
|
+
ok "files copied"
|
|
357
|
+
|
|
358
|
+
# Restore preserved user data if this was an upgrade (not a fresh install)
|
|
359
|
+
if [[ -n "${PRESERVE_DIR:-}" && -d "$PRESERVE_DIR" ]]; then
|
|
360
|
+
restored=0
|
|
361
|
+
for src in "$PRESERVE_DIR"/* "$PRESERVE_DIR"/.[!.]*; do
|
|
362
|
+
[[ -f "$src" ]] || continue
|
|
363
|
+
cp -p "$src" "$CLAUDE_DIR/coach/$(basename "$src")"
|
|
364
|
+
restored=$((restored + 1))
|
|
365
|
+
done
|
|
366
|
+
rm -rf "$PRESERVE_DIR"
|
|
367
|
+
ok "restored existing coach state ($restored files; progress preserved)"
|
|
368
|
+
fi
|
|
369
|
+
|
|
370
|
+
# Restore the per-run git history if this was an upgrade. The profile-mutation
|
|
371
|
+
# log lives at ~/.claude/coach/.git/ and the documented rollback UX is
|
|
372
|
+
# `git -C ~/.claude/coach checkout HEAD~1 -- profile.yaml`. Without this,
|
|
373
|
+
# every upgrade would reset that history to a single bootstrap commit.
|
|
374
|
+
if [[ -d "$CLAUDE_DIR/coach.bak.$TS/.git" && ! -d "$CLAUDE_DIR/coach/.git" ]]; then
|
|
375
|
+
cp -R "$CLAUDE_DIR/coach.bak.$TS/.git" "$CLAUDE_DIR/coach/.git"
|
|
376
|
+
ok "restored git history from previous install (rollback UX preserved)"
|
|
377
|
+
fi
|
|
378
|
+
|
|
379
|
+
# --- Git-init the coach data dir for rollback -------------------------------
|
|
380
|
+
|
|
381
|
+
bold "Git-init coach data dir (so every profile change is a commit)"
|
|
382
|
+
|
|
383
|
+
if [[ ! -d "$CLAUDE_DIR/coach/.git" ]]; then
|
|
384
|
+
( cd "$CLAUDE_DIR/coach" && git init -q && git add -A && \
|
|
385
|
+
git commit -q -m "Bootstrap coach directory" --allow-empty )
|
|
386
|
+
ok "git initialized at ~/.claude/coach"
|
|
387
|
+
else
|
|
388
|
+
ok "git already initialized"
|
|
389
|
+
fi
|
|
390
|
+
|
|
391
|
+
# --- Patch settings.json -----------------------------------------------------
|
|
392
|
+
|
|
393
|
+
bold "Patching settings.json (additive, safe)"
|
|
394
|
+
|
|
395
|
+
SETTINGS="$CLAUDE_DIR/settings.json"
|
|
396
|
+
if [[ ! -f "$SETTINGS" ]]; then
|
|
397
|
+
echo '{}' > "$SETTINGS"
|
|
398
|
+
warn "no existing settings.json — created an empty one"
|
|
399
|
+
fi
|
|
400
|
+
|
|
401
|
+
# Patch is wrapped in try/except so a corrupt settings.json doesn't crash the
|
|
402
|
+
# installer — we report + point the user at their .bak file.
|
|
403
|
+
"$PY" - "$SETTINGS" "$PY" "$CLAUDE_DIR" <<'PYEOF'
|
|
404
|
+
import json, shlex, sys, traceback
|
|
405
|
+
|
|
406
|
+
settings_path, py, claude_dir = sys.argv[1], sys.argv[2], sys.argv[3]
|
|
407
|
+
|
|
408
|
+
try:
|
|
409
|
+
with open(settings_path) as f:
|
|
410
|
+
data = json.load(f)
|
|
411
|
+
except Exception as e:
|
|
412
|
+
print(f" ERROR: settings.json is not valid JSON: {e}")
|
|
413
|
+
print(f" Your original was backed up. Fix the JSON and re-run ./install.sh.")
|
|
414
|
+
sys.exit(1)
|
|
415
|
+
|
|
416
|
+
# We resolve `$PY` at install time and hardcode it in the hook command so the
|
|
417
|
+
# hook fires correctly even if Claude Code's runtime shell PATH doesn't
|
|
418
|
+
# include Homebrew/pyenv etc. Users can swap this manually later if they
|
|
419
|
+
# change interpreter.
|
|
420
|
+
hook_specs = [
|
|
421
|
+
("SessionStart", "coach-session-start.py", 3),
|
|
422
|
+
("UserPromptSubmit", "coach-user-prompt.py", 2),
|
|
423
|
+
]
|
|
424
|
+
|
|
425
|
+
py_cmd = shlex.quote(py)
|
|
426
|
+
stats_path = shlex.quote(f"{claude_dir}/coach/bin/stats.py")
|
|
427
|
+
default_statusline_path = shlex.quote(
|
|
428
|
+
f"{claude_dir}/coach/default-statusline-command.sh"
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
hooks = data.setdefault("hooks", {})
|
|
432
|
+
changed = False
|
|
433
|
+
for event, script_name, timeout in hook_specs:
|
|
434
|
+
buckets = hooks.setdefault(event, [])
|
|
435
|
+
already = any(
|
|
436
|
+
script_name in h.get("command", "")
|
|
437
|
+
for group in buckets if isinstance(group, dict)
|
|
438
|
+
for h in (group.get("hooks") or []) if isinstance(h, dict)
|
|
439
|
+
)
|
|
440
|
+
if already:
|
|
441
|
+
print(f" OK: {event} hook already registered (no change)")
|
|
442
|
+
continue
|
|
443
|
+
entry = {
|
|
444
|
+
"type": "command",
|
|
445
|
+
"command": f"{py_cmd} {shlex.quote(f'{claude_dir}/hooks/{script_name}')}",
|
|
446
|
+
"timeout": timeout,
|
|
447
|
+
}
|
|
448
|
+
buckets.append({"hooks": [entry]})
|
|
449
|
+
changed = True
|
|
450
|
+
print(f" OK: {event} hook added ({script_name}, timeout={timeout}s)")
|
|
451
|
+
|
|
452
|
+
status = data.get("statusLine")
|
|
453
|
+
if isinstance(status, dict) and (
|
|
454
|
+
"default-statusline-command.sh" in str(status.get("command", ""))
|
|
455
|
+
or "stats.py" in str(status.get("command", ""))
|
|
456
|
+
):
|
|
457
|
+
print(" OK: statusLine already registered for Coach (no change)")
|
|
458
|
+
elif status:
|
|
459
|
+
print(" OK: existing statusLine left unchanged (Coach default not installed)")
|
|
460
|
+
else:
|
|
461
|
+
data["statusLine"] = {
|
|
462
|
+
"type": "command",
|
|
463
|
+
"command": f"bash {default_statusline_path}",
|
|
464
|
+
}
|
|
465
|
+
changed = True
|
|
466
|
+
print(" OK: statusLine added (coach/default-statusline-command.sh)")
|
|
467
|
+
|
|
468
|
+
if changed:
|
|
469
|
+
with open(settings_path, "w") as f:
|
|
470
|
+
json.dump(data, f, indent=2)
|
|
471
|
+
PYEOF
|
|
472
|
+
|
|
473
|
+
[[ $? -eq 0 ]] || die "settings.json patch failed — see message above"
|
|
474
|
+
|
|
475
|
+
# Wrap-mode auto-wrap (v0.1.4). When settings.json:statusLine is
|
|
476
|
+
# `claimed` (user's custom shell script), the helper appends Coach's
|
|
477
|
+
# segment by saving the original and replacing the command with our
|
|
478
|
+
# wrap trampoline. Skips if a sticky opt-out marker is present OR the
|
|
479
|
+
# user's script already references Coach internals (manual-Coach
|
|
480
|
+
# pre-flight). Always exits 0 — never breaks an install.
|
|
481
|
+
COACH_CONFIG_DIR="$CLAUDE_DIR/coach" \
|
|
482
|
+
CLAUDE_SETTINGS_PATH="$SETTINGS" \
|
|
483
|
+
"$PY" "$CLAUDE_DIR/coach/bin/statusline_wrap_action.py" wrap-if-claimed || true
|
|
484
|
+
|
|
485
|
+
# If the patch ran cleanly but didn't actually change settings.json (everything
|
|
486
|
+
# already registered, byte-identical output), the .bak.<ts> from the snapshot
|
|
487
|
+
# above is dead weight. Drop it. Real diffs leave the .bak in place for the
|
|
488
|
+
# user to recover from if anything went sideways.
|
|
489
|
+
if [[ -f "$SETTINGS.bak.$TS" ]] && cmp -s "$SETTINGS" "$SETTINGS.bak.$TS"; then
|
|
490
|
+
rm -f "$SETTINGS.bak.$TS"
|
|
491
|
+
note "settings.json unchanged — discarded redundant backup"
|
|
492
|
+
fi
|
|
493
|
+
|
|
494
|
+
# --- Smoke-test the hooks ----------------------------------------------------
|
|
495
|
+
|
|
496
|
+
bold "Smoke-testing hooks"
|
|
497
|
+
for hook in coach-session-start.py coach-user-prompt.py; do
|
|
498
|
+
OUT="$(echo '{}' | COACH_DISABLE=1 "$PY" "$CLAUDE_DIR/hooks/$hook" 2>/dev/null || true)"
|
|
499
|
+
if [[ -z "$OUT" ]]; then
|
|
500
|
+
ok "$hook exits cleanly with COACH_DISABLE=1 (side-effect-free)"
|
|
501
|
+
else
|
|
502
|
+
note "$hook emitted output even with COACH_DISABLE=1"
|
|
503
|
+
fi
|
|
504
|
+
done
|
|
505
|
+
|
|
506
|
+
# --- Prune accumulated backups (default; opt out with --no-prune-backups) ---
|
|
507
|
+
|
|
508
|
+
if [[ "$PRUNE_BACKUPS" == "1" ]]; then
|
|
509
|
+
bold "Pruning old backups (keeping 3 most recent of each kind)"
|
|
510
|
+
pruned=0
|
|
511
|
+
# `while read` instead of `for $(ls ...)` so paths with spaces in
|
|
512
|
+
# $CLAUDE_DIR (e.g. ~/Library/Application Support/...) survive
|
|
513
|
+
# word-splitting. ls -t is mtime-descending, tail -n +4 skips the 3
|
|
514
|
+
# most recent.
|
|
515
|
+
|
|
516
|
+
# coach.bak.<ts>/ directories
|
|
517
|
+
while IFS= read -r old; do
|
|
518
|
+
[[ -z "$old" ]] && continue
|
|
519
|
+
rm -rf -- "$old" && pruned=$((pruned + 1))
|
|
520
|
+
done <<< "$(ls -dt "$CLAUDE_DIR"/coach.bak.* 2>/dev/null | tail -n +4)"
|
|
521
|
+
|
|
522
|
+
# settings.json.bak.<ts> files
|
|
523
|
+
while IFS= read -r old; do
|
|
524
|
+
[[ -z "$old" ]] && continue
|
|
525
|
+
rm -f -- "$old" && pruned=$((pruned + 1))
|
|
526
|
+
done <<< "$(ls -t "$CLAUDE_DIR"/settings.json.bak.* 2>/dev/null | tail -n +4)"
|
|
527
|
+
|
|
528
|
+
# hooks/<hook>.bak.<ts> files (per-hook accounting so each hook keeps 3)
|
|
529
|
+
for hook in coach-session-start.py coach-user-prompt.py; do
|
|
530
|
+
while IFS= read -r old; do
|
|
531
|
+
[[ -z "$old" ]] && continue
|
|
532
|
+
rm -f -- "$old" && pruned=$((pruned + 1))
|
|
533
|
+
done <<< "$(ls -t "$CLAUDE_DIR"/hooks/"$hook".bak.* 2>/dev/null | tail -n +4)"
|
|
534
|
+
done
|
|
535
|
+
|
|
536
|
+
ok "pruned $pruned old backup(s)"
|
|
537
|
+
fi
|
|
538
|
+
|
|
539
|
+
# --- Optional: seed the profile from recent transcripts ---------------------
|
|
540
|
+
|
|
541
|
+
SEEDED=0
|
|
542
|
+
if [[ "$SEED" == "1" ]]; then
|
|
543
|
+
bold "Seeding profile (--seed)"
|
|
544
|
+
if [[ -d "$HOME/.claude/projects" ]]; then
|
|
545
|
+
note "running insights.sh 7d against $HOME/.claude/projects (may take ~30s)…"
|
|
546
|
+
if "$CLAUDE_DIR/coach/bin/insights.sh" 7d 2>&1 | tail -4; then
|
|
547
|
+
ok "profile seeded — run '/coach status' inside Claude Code to see it"
|
|
548
|
+
SEEDED=1
|
|
549
|
+
else
|
|
550
|
+
warn "seed run didn't complete cleanly — non-fatal, you can run ~/.claude/coach/bin/insights.sh 7d manually later"
|
|
551
|
+
fi
|
|
552
|
+
else
|
|
553
|
+
warn "no $HOME/.claude/projects dir yet — skipping seed (no transcripts to analyze)"
|
|
554
|
+
note "open Claude Code at least once, use it a bit, then re-run with --seed"
|
|
555
|
+
fi
|
|
556
|
+
fi
|
|
557
|
+
|
|
558
|
+
# --- Done --------------------------------------------------------------------
|
|
559
|
+
|
|
560
|
+
bold "Installed. Coach Claw is now active."
|
|
561
|
+
|
|
562
|
+
# Seed-step copy depends on the flag the user passed (or didn't).
|
|
563
|
+
if [[ "$SEEDED" == "1" ]]; then
|
|
564
|
+
SEED_LINE="profile seeded from the last 7 days of transcripts."
|
|
565
|
+
elif [[ "$NO_SEED" == "1" ]]; then
|
|
566
|
+
SEED_LINE="--no-seed honored; to seed later: ~/.claude/coach/bin/insights.sh 7d"
|
|
567
|
+
else
|
|
568
|
+
SEED_LINE="empty profile (no --seed). Re-run with --seed to bootstrap, or just use Claude Code — the daily cron will fill it in."
|
|
569
|
+
fi
|
|
570
|
+
cat <<EOF
|
|
571
|
+
|
|
572
|
+
What's next:
|
|
573
|
+
1. Restart Claude Code (or open a new session) — the hooks need to load.
|
|
574
|
+
|
|
575
|
+
2. Send any prompt. Watch the bottom-right statusline:
|
|
576
|
+
◆ Ⅰ 1000 Drafter
|
|
577
|
+
That's your level + ELO + rank name. It updates as you ship code.
|
|
578
|
+
|
|
579
|
+
3. Customize the look any time (theme also changes rank names + celebration banners):
|
|
580
|
+
Inside Claude Code:
|
|
581
|
+
/config preview (see all 4 variants × 12 themes)
|
|
582
|
+
/config theme ocean (try a different ladder)
|
|
583
|
+
/config statusline pips (try a different statusline shape)
|
|
584
|
+
From the terminal — same backing file:
|
|
585
|
+
npx @rm0nroe/coach-claw@latest config wizard (interactive)
|
|
586
|
+
npx @rm0nroe/coach-claw@latest config set --theme ocean
|
|
587
|
+
|
|
588
|
+
4. Schedule daily auto-analysis:
|
|
589
|
+
macOS: npx @rm0nroe/coach-claw@latest launchd
|
|
590
|
+
Linux: README.md → Install → step 3
|
|
591
|
+
|
|
592
|
+
5. Other slash commands: /coach status /coach off | on /coach uninstall
|
|
593
|
+
|
|
594
|
+
Seed: $SEED_LINE
|
|
595
|
+
|
|
596
|
+
See README.md for design rationale, feature docs, and troubleshooting.
|
|
597
|
+
EOF
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
3
|
+
<plist version="1.0">
|
|
4
|
+
<dict>
|
|
5
|
+
<key>Label</key>
|
|
6
|
+
<string>com.local.claude-coach</string>
|
|
7
|
+
|
|
8
|
+
<key>ProgramArguments</key>
|
|
9
|
+
<array>
|
|
10
|
+
<string>@HOME@/.claude/coach/bin/run-insights.sh</string>
|
|
11
|
+
</array>
|
|
12
|
+
|
|
13
|
+
<key>StartCalendarInterval</key>
|
|
14
|
+
<dict>
|
|
15
|
+
<key>Hour</key><integer>4</integer>
|
|
16
|
+
<key>Minute</key><integer>0</integer>
|
|
17
|
+
</dict>
|
|
18
|
+
|
|
19
|
+
<key>RunAtLoad</key>
|
|
20
|
+
<false/>
|
|
21
|
+
|
|
22
|
+
<key>StandardOutPath</key>
|
|
23
|
+
<string>/tmp/claude-coach.out</string>
|
|
24
|
+
|
|
25
|
+
<key>StandardErrorPath</key>
|
|
26
|
+
<string>/tmp/claude-coach.err</string>
|
|
27
|
+
|
|
28
|
+
<key>EnvironmentVariables</key>
|
|
29
|
+
<dict>
|
|
30
|
+
<key>PATH</key>
|
|
31
|
+
<string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin</string>
|
|
32
|
+
</dict>
|
|
33
|
+
</dict>
|
|
34
|
+
</plist>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Wrapper invoked by launchd to run the Coach insights pass once. Logs to
|
|
3
|
+
# /tmp. Runs the deterministic insights.sh — does NOT go through the
|
|
4
|
+
# claude CLI, so no cold-start cost and no slash-command routing issues.
|
|
5
|
+
# (The on-demand `/coach-insights` skill is the LLM-driven counterpart
|
|
6
|
+
# that runs from inside Claude Code; this wrapper is launchd-only.)
|
|
7
|
+
|
|
8
|
+
set -u
|
|
9
|
+
export PATH="/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin"
|
|
10
|
+
export HOME="${HOME:-$(eval echo ~$(whoami))}"
|
|
11
|
+
|
|
12
|
+
LOG="/tmp/claude-coach.log"
|
|
13
|
+
TS="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
14
|
+
echo "[$TS] starting insights.sh 1d" >> "$LOG"
|
|
15
|
+
|
|
16
|
+
"$HOME/.claude/coach/bin/insights.sh" 1d >> "$LOG" 2>&1
|
|
17
|
+
EXIT=$?
|
|
18
|
+
|
|
19
|
+
echo "[$TS] insights.sh exited $EXIT" >> "$LOG"
|
|
20
|
+
exit "$EXIT"
|