@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,176 @@
|
|
|
1
|
+
"""Read/write `~/.claude/coach/.user_config.json` — the operator-tunable
|
|
2
|
+
settings the `/config` slash command edits.
|
|
3
|
+
|
|
4
|
+
Schema (v1):
|
|
5
|
+
{
|
|
6
|
+
"schema_version": 1,
|
|
7
|
+
"statusline_variant": one of VALID_VARIANTS (see below),
|
|
8
|
+
"theme": one of VALID_THEMES (see below),
|
|
9
|
+
"elo_min": int (default 1000, must be < elo_max),
|
|
10
|
+
"elo_max": int (default 2800, must be > elo_min)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
The valid sets are defined as constants below — they are the single
|
|
14
|
+
source of truth, consumed by `/config` validation and the statusline
|
|
15
|
+
preview. Don't duplicate the lists into this docstring; they will rot.
|
|
16
|
+
|
|
17
|
+
Missing file → defaults. Unknown keys → ignored. Invalid values →
|
|
18
|
+
fall back to defaults for that field. Reads never raise.
|
|
19
|
+
|
|
20
|
+
Writes are atomic (tempfile + os.replace) but NOT locked — `/config`
|
|
21
|
+
is interactive + slow-path, no concurrency concern with `stats.py`
|
|
22
|
+
which only reads.
|
|
23
|
+
|
|
24
|
+
Path resolution: by default the file lives at
|
|
25
|
+
`~/.claude/coach/.user_config.json`. Set `COACH_CONFIG_DIR=/some/dir` to
|
|
26
|
+
override the directory (the file name `.user_config.json` is fixed).
|
|
27
|
+
This is what lets `coach/bin/configure.py` invoked via `npx coach-claw
|
|
28
|
+
config` honor a custom `CLAUDE_DIR` install — the npm wrapper exports
|
|
29
|
+
`COACH_CONFIG_DIR` to match. Resolution happens at every read/write so
|
|
30
|
+
tests can monkeypatch `COACH_CONFIG_DIR` via `monkeypatch.setenv`.
|
|
31
|
+
"""
|
|
32
|
+
from __future__ import annotations
|
|
33
|
+
|
|
34
|
+
import json
|
|
35
|
+
import os
|
|
36
|
+
import tempfile
|
|
37
|
+
from pathlib import Path
|
|
38
|
+
|
|
39
|
+
from coach_paths import resolve_coach_dir
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _resolve_config_path() -> Path:
|
|
43
|
+
"""Return the path to .user_config.json. Delegates to
|
|
44
|
+
`coach_paths.resolve_coach_dir()` so the COACH_CONFIG_DIR contract
|
|
45
|
+
is enforced in exactly one place. Resolved per-call so the env var
|
|
46
|
+
can be set at test time or by the npm wrapper before the Python
|
|
47
|
+
entry point reads it."""
|
|
48
|
+
return resolve_coach_dir() / ".user_config.json"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def __getattr__(name):
|
|
52
|
+
"""Module-level __getattr__ (PEP 562). Lets external code read
|
|
53
|
+
`user_config.CONFIG_PATH` and get a fresh, env-aware path. Internal
|
|
54
|
+
code should call `_resolve_config_path()` directly so behavior is
|
|
55
|
+
explicit at the call site."""
|
|
56
|
+
if name == "CONFIG_PATH":
|
|
57
|
+
return _resolve_config_path()
|
|
58
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
59
|
+
|
|
60
|
+
DEFAULTS: dict = {
|
|
61
|
+
"schema_version": 1,
|
|
62
|
+
"statusline_variant": "crystal",
|
|
63
|
+
"theme": "craft",
|
|
64
|
+
"elo_min": 1000,
|
|
65
|
+
"elo_max": 2800,
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
VALID_VARIANTS = {"crystal", "pips", "slash", "forge"}
|
|
69
|
+
VALID_THEMES = {
|
|
70
|
+
# abstract themes
|
|
71
|
+
"craft", "forge", "cosmic", "ocean",
|
|
72
|
+
# pop-culture-inspired (fan-themed; see themes.py docstring on brand safety)
|
|
73
|
+
"skyrim", "marvel", "dc", "finalfantasy",
|
|
74
|
+
"military", "lotr", "starwars", "hacker",
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def load() -> dict:
|
|
79
|
+
"""Return a complete config dict. Always populates every key — caller
|
|
80
|
+
can index without `.get()`."""
|
|
81
|
+
cfg = dict(DEFAULTS)
|
|
82
|
+
config_path = _resolve_config_path()
|
|
83
|
+
try:
|
|
84
|
+
if config_path.exists():
|
|
85
|
+
raw = json.loads(config_path.read_text())
|
|
86
|
+
if isinstance(raw, dict):
|
|
87
|
+
_coerce_into(cfg, raw)
|
|
88
|
+
except Exception:
|
|
89
|
+
pass
|
|
90
|
+
return cfg
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _coerce_into(cfg: dict, raw: dict) -> None:
|
|
94
|
+
"""Apply each valid field from `raw` into `cfg`. Invalid values stay
|
|
95
|
+
at their default — never raises."""
|
|
96
|
+
v = raw.get("statusline_variant")
|
|
97
|
+
if isinstance(v, str) and v in VALID_VARIANTS:
|
|
98
|
+
cfg["statusline_variant"] = v
|
|
99
|
+
t = raw.get("theme")
|
|
100
|
+
if isinstance(t, str) and t in VALID_THEMES:
|
|
101
|
+
cfg["theme"] = t
|
|
102
|
+
emin = raw.get("elo_min")
|
|
103
|
+
emax = raw.get("elo_max")
|
|
104
|
+
if isinstance(emin, int) and isinstance(emax, int) and 0 < emin < emax:
|
|
105
|
+
cfg["elo_min"] = emin
|
|
106
|
+
cfg["elo_max"] = emax
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def save(cfg: dict) -> None:
|
|
110
|
+
"""Atomic write. Validates against schema before persisting; raises
|
|
111
|
+
ValueError on invalid input so `/config` can show a clear error."""
|
|
112
|
+
validated = dict(DEFAULTS)
|
|
113
|
+
_coerce_into(validated, cfg)
|
|
114
|
+
# If the caller passed an unknown variant/theme, _coerce_into silently
|
|
115
|
+
# kept the default. Detect that and complain so /config can surface
|
|
116
|
+
# which key was rejected.
|
|
117
|
+
for key in ("statusline_variant", "theme"):
|
|
118
|
+
if key in cfg and cfg[key] != validated[key]:
|
|
119
|
+
valid = (
|
|
120
|
+
VALID_VARIANTS if key == "statusline_variant" else VALID_THEMES
|
|
121
|
+
)
|
|
122
|
+
raise ValueError(
|
|
123
|
+
f"unknown {key} {cfg[key]!r}; valid: {sorted(valid)}"
|
|
124
|
+
)
|
|
125
|
+
if "elo_min" in cfg or "elo_max" in cfg:
|
|
126
|
+
emin = cfg.get("elo_min", validated["elo_min"])
|
|
127
|
+
emax = cfg.get("elo_max", validated["elo_max"])
|
|
128
|
+
if not (isinstance(emin, int) and isinstance(emax, int) and 0 < emin < emax):
|
|
129
|
+
raise ValueError(
|
|
130
|
+
f"elo_min ({emin}) must be a positive int less than "
|
|
131
|
+
f"elo_max ({emax})"
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
config_path = _resolve_config_path()
|
|
135
|
+
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
136
|
+
fd, tmp = tempfile.mkstemp(
|
|
137
|
+
prefix="." + config_path.name + ".",
|
|
138
|
+
suffix=".tmp",
|
|
139
|
+
dir=str(config_path.parent),
|
|
140
|
+
)
|
|
141
|
+
try:
|
|
142
|
+
with os.fdopen(fd, "w") as fh:
|
|
143
|
+
fh.write(json.dumps(validated, indent=2, sort_keys=True))
|
|
144
|
+
fh.flush()
|
|
145
|
+
os.fsync(fh.fileno())
|
|
146
|
+
os.replace(tmp, config_path)
|
|
147
|
+
except Exception:
|
|
148
|
+
try:
|
|
149
|
+
os.unlink(tmp)
|
|
150
|
+
except Exception:
|
|
151
|
+
pass
|
|
152
|
+
raise
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
# --- Convenience accessors used by stats.py / variants render path ------
|
|
156
|
+
|
|
157
|
+
def get_variant() -> str:
|
|
158
|
+
return load()["statusline_variant"]
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def get_theme() -> str:
|
|
162
|
+
return load()["theme"]
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def get_elo_range() -> tuple[int, int]:
|
|
166
|
+
cfg = load()
|
|
167
|
+
return cfg["elo_min"], cfg["elo_max"]
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def update(**kwargs) -> dict:
|
|
171
|
+
"""Merge updates into the existing config and persist. Returns the
|
|
172
|
+
new config. Raises ValueError on invalid input."""
|
|
173
|
+
cfg = load()
|
|
174
|
+
cfg.update(kwargs)
|
|
175
|
+
save(cfg)
|
|
176
|
+
return cfg
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""Shared lifetime XP accounting for Coach profile data.
|
|
2
|
+
|
|
3
|
+
The profile used to store all non-graduation lifetime XP in
|
|
4
|
+
``banked_session_xp``. Newer code keeps the sources split so status output,
|
|
5
|
+
exports, and future UI can explain where progress came from:
|
|
6
|
+
|
|
7
|
+
- session_banked_xp: completed-session XP converted at 10:1 by bank.py
|
|
8
|
+
- milestone_xp: mid-streak rewards from merge.py
|
|
9
|
+
- graduation_xp: derived from current graduated entries
|
|
10
|
+
- manual_adjustments: explicit operator edits
|
|
11
|
+
|
|
12
|
+
``banked_session_xp`` remains as a deprecated alias for session banking only.
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
GRADUATION_XP = 5
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _as_int(value, default: int = 0) -> int:
|
|
20
|
+
try:
|
|
21
|
+
return int(value or 0)
|
|
22
|
+
except Exception:
|
|
23
|
+
return default
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def graduation_xp(profile: dict) -> int:
|
|
27
|
+
graduated = [
|
|
28
|
+
g for g in (profile.get("graduated") or [])
|
|
29
|
+
if isinstance(g, dict)
|
|
30
|
+
]
|
|
31
|
+
return len(graduated) * GRADUATION_XP
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def max_active_clean_streak(profile: dict) -> int:
|
|
35
|
+
max_streak = 0
|
|
36
|
+
for entry in profile.get("entries", []) or []:
|
|
37
|
+
if not isinstance(entry, dict):
|
|
38
|
+
continue
|
|
39
|
+
max_streak = max(max_streak, _as_int(entry.get("clean_streak_runs")))
|
|
40
|
+
return max_streak
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def normalize_profile_xp(profile: dict) -> dict:
|
|
44
|
+
"""Ensure split XP fields exist and return an explainable breakdown.
|
|
45
|
+
|
|
46
|
+
Mutates ``profile`` in place. For legacy profiles with only
|
|
47
|
+
``banked_session_xp``, that value is migrated into ``session_banked_xp``.
|
|
48
|
+
Historical milestone/session split cannot be recovered, so preserving the
|
|
49
|
+
total is the safe migration.
|
|
50
|
+
"""
|
|
51
|
+
if not isinstance(profile, dict):
|
|
52
|
+
profile = {}
|
|
53
|
+
|
|
54
|
+
has_split = any(
|
|
55
|
+
key in profile
|
|
56
|
+
for key in ("session_banked_xp", "milestone_xp", "manual_adjustments")
|
|
57
|
+
)
|
|
58
|
+
legacy_banked = _as_int(profile.get("banked_session_xp"))
|
|
59
|
+
|
|
60
|
+
if has_split:
|
|
61
|
+
session_banked = _as_int(profile.get("session_banked_xp"))
|
|
62
|
+
else:
|
|
63
|
+
session_banked = legacy_banked
|
|
64
|
+
|
|
65
|
+
milestone = _as_int(profile.get("milestone_xp"))
|
|
66
|
+
manual = _as_int(profile.get("manual_adjustments"))
|
|
67
|
+
graduated = graduation_xp(profile)
|
|
68
|
+
clean_streak = max_active_clean_streak(profile)
|
|
69
|
+
|
|
70
|
+
profile["session_banked_xp"] = session_banked
|
|
71
|
+
profile["milestone_xp"] = milestone
|
|
72
|
+
profile["graduation_xp"] = graduated
|
|
73
|
+
profile["manual_adjustments"] = manual
|
|
74
|
+
# Deprecated compatibility alias. New code should read session_banked_xp.
|
|
75
|
+
profile["banked_session_xp"] = session_banked
|
|
76
|
+
|
|
77
|
+
lifetime = session_banked + milestone + graduated + clean_streak + manual
|
|
78
|
+
return {
|
|
79
|
+
"session_banked_xp": session_banked,
|
|
80
|
+
"milestone_xp": milestone,
|
|
81
|
+
"graduation_xp": graduated,
|
|
82
|
+
"manual_adjustments": manual,
|
|
83
|
+
"max_active_clean_streak": clean_streak,
|
|
84
|
+
"lifetime_xp": lifetime,
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def add_session_banked_xp(profile: dict, amount: int) -> dict:
|
|
89
|
+
normalize_profile_xp(profile)
|
|
90
|
+
profile["session_banked_xp"] = _as_int(profile.get("session_banked_xp")) + _as_int(amount)
|
|
91
|
+
profile["banked_session_xp"] = profile["session_banked_xp"]
|
|
92
|
+
return normalize_profile_xp(profile)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def add_milestone_xp(profile: dict, amount: int) -> dict:
|
|
96
|
+
normalize_profile_xp(profile)
|
|
97
|
+
profile["milestone_xp"] = _as_int(profile.get("milestone_xp")) + _as_int(amount)
|
|
98
|
+
return normalize_profile_xp(profile)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Coach Claw — default statusline composition.
|
|
3
|
+
#
|
|
4
|
+
# Thin trampoline: locates `bin/default_statusline.py` relative to this
|
|
5
|
+
# wrapper's own directory using bash parameter expansion only — no
|
|
6
|
+
# external commands, no PATH lookups. settings.json registers this
|
|
7
|
+
# wrapper by absolute path, so ${BASH_SOURCE%/*} reliably yields the
|
|
8
|
+
# wrapper's parent dir. Resolving relatively (instead of hardcoding
|
|
9
|
+
# $HOME/.claude/coach/...) keeps custom CLAUDE_DIR installs working.
|
|
10
|
+
#
|
|
11
|
+
# `@PY@` is substituted with the absolute python3 path at install time
|
|
12
|
+
# so the script doesn't depend on PATH at statusline-render time.
|
|
13
|
+
#
|
|
14
|
+
# To customize the rich default: edit `bin/default_statusline.py` (pure
|
|
15
|
+
# Python, no external dependencies). Or run `/config` to change the
|
|
16
|
+
# trailing coach segment's variant + theme without touching either
|
|
17
|
+
# file.
|
|
18
|
+
|
|
19
|
+
exec "@PY@" "${BASH_SOURCE%/*}/bin/default_statusline.py"
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Coach Claw — wrap-mode statusline trampoline.
|
|
3
|
+
#
|
|
4
|
+
# Runs the user's saved original statusLine command first (captured in
|
|
5
|
+
# `~/.claude/coach/.statusline-wrap.json` by `statusline_wrap_action.wrap`),
|
|
6
|
+
# then appends the Coach segment with trailing-aware separator handling.
|
|
7
|
+
#
|
|
8
|
+
# Symmetric with `default-statusline-command.sh` — same `${BASH_SOURCE%/*}`
|
|
9
|
+
# parameter expansion to locate the bin/ dir relative to this wrapper, so
|
|
10
|
+
# custom CLAUDE_DIR installs keep working without configuration. `@PY@`
|
|
11
|
+
# is substituted with the absolute python3 path at install time.
|
|
12
|
+
#
|
|
13
|
+
# To opt out of wrap mode after install: `/coach-claw:doctor --unwrap-statusline`.
|
|
14
|
+
|
|
15
|
+
exec "@PY@" "${BASH_SOURCE%/*}/bin/statusline_wrap.py"
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
schema_version: 1
|
|
2
|
+
updated: null
|
|
3
|
+
# Coach profile — autonomously maintained by /coach-insights.
|
|
4
|
+
# Hand-editing is allowed but not required. Git-tracked for rollback.
|
|
5
|
+
#
|
|
6
|
+
# Tiers:
|
|
7
|
+
# candidate — detected in 1 recent /coach-insights run; not yet injected
|
|
8
|
+
# probationary — detected in 2-of-3 runs; injected at 30% probability
|
|
9
|
+
# for first 7 days, then promoted to active
|
|
10
|
+
# active — fully injected (confidence × cooldown gating)
|
|
11
|
+
#
|
|
12
|
+
# Confidence grows on recurrence, decays 5%/day untouched. <0.3 auto-retires.
|
|
13
|
+
# Cap = 10 active entries; lowest-confidence evicted when a new one promotes.
|
|
14
|
+
#
|
|
15
|
+
# Each entry schema:
|
|
16
|
+
# id: stable-slug
|
|
17
|
+
# name: human-readable short name
|
|
18
|
+
# tier: candidate | probationary | active
|
|
19
|
+
# confidence: 0.0-1.0
|
|
20
|
+
# priority: 1-5 (for tie-breaking at the cap)
|
|
21
|
+
# nudge: single observational sentence, never prescriptive
|
|
22
|
+
# examples: short quoted evidence from transcripts (redacted)
|
|
23
|
+
# first_seen: ISO date
|
|
24
|
+
# last_seen_in_run: ISO date
|
|
25
|
+
# last_fired: ISO datetime (24h per-entry cooldown)
|
|
26
|
+
# promoted_at: ISO date | null (probationary→active transition)
|
|
27
|
+
# source_session_ids: [short hashes]
|
|
28
|
+
entries: []
|
|
29
|
+
graduated: []
|
|
30
|
+
archived: []
|
|
31
|
+
session_banked_xp: 0
|
|
32
|
+
milestone_xp: 0
|
|
33
|
+
graduation_xp: 0
|
|
34
|
+
manual_adjustments: 0
|
|
35
|
+
# Deprecated compatibility alias for older installs; new code reads
|
|
36
|
+
# session_banked_xp / milestone_xp / graduation_xp / manual_adjustments.
|
|
37
|
+
banked_session_xp: 0
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Shared pytest fixtures + path setup for the coach suite.
|
|
2
|
+
|
|
3
|
+
Adds coach/bin/ to sys.path so `import reward_hints, scoring, merge` works
|
|
4
|
+
whether you run pytest from ~/.claude/coach/ or from the shareable bundle.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
_BIN = Path(__file__).resolve().parent.parent / "bin"
|
|
12
|
+
if str(_BIN) not in sys.path:
|
|
13
|
+
sys.path.insert(0, str(_BIN))
|