trigger-tree 1.14.0__py3-none-any.whl
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.
- trigger_tree/__init__.py +1 -0
- trigger_tree/_scripts/tt-codex-hook.py +120 -0
- trigger_tree/_scripts/tt-config.sh +32 -0
- trigger_tree/_scripts/tt-doctor.py +288 -0
- trigger_tree/_scripts/tt-log.py +656 -0
- trigger_tree/_scripts/tt-open.sh +164 -0
- trigger_tree/_scripts/tt-publish-badge.sh +26 -0
- trigger_tree/_scripts/tt-report.py +623 -0
- trigger_tree/_scripts/tt-setup.py +266 -0
- trigger_tree/_scripts/tt-shell-capture.sh +48 -0
- trigger_tree/_scripts/tt-stats.py +937 -0
- trigger_tree/_scripts/tt-statusline.py +154 -0
- trigger_tree/_scripts/tt-suggestions.py +128 -0
- trigger_tree/_scripts/tt-tips.py +107 -0
- trigger_tree/_scripts/tt-uninstall.py +60 -0
- trigger_tree/_scripts/tt-watch.py +1013 -0
- trigger_tree/_scripts/tt_runtime.py +27 -0
- trigger_tree/_scripts/tt_scope.py +76 -0
- trigger_tree/_scripts/validate_palette.js +28 -0
- trigger_tree/cli.py +52 -0
- trigger_tree/hooks/claude-hooks.json +11 -0
- trigger_tree/hooks/hooks.json +9 -0
- trigger_tree-1.14.0.dist-info/METADATA +106 -0
- trigger_tree-1.14.0.dist-info/RECORD +27 -0
- trigger_tree-1.14.0.dist-info/WHEEL +4 -0
- trigger_tree-1.14.0.dist-info/entry_points.txt +2 -0
- trigger_tree-1.14.0.dist-info/licenses/LICENSE +21 -0
trigger_tree/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""trigger-tree — local, zero-token documentation-discovery telemetry (standalone CLI)."""
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Translate Codex lifecycle hooks to trigger-tree's stable logger contract."""
|
|
3
|
+
|
|
4
|
+
import io
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import runpy
|
|
8
|
+
import sys
|
|
9
|
+
|
|
10
|
+
from tt_runtime import project_root
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def read_payload(stream):
|
|
14
|
+
try:
|
|
15
|
+
value = json.load(stream)
|
|
16
|
+
except (json.JSONDecodeError, ValueError):
|
|
17
|
+
return {}
|
|
18
|
+
return value if isinstance(value, dict) else {}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def normalize_tool(payload):
|
|
22
|
+
tool = payload.get("tool_name", "")
|
|
23
|
+
tool_input = payload.get("tool_input")
|
|
24
|
+
tool_input = dict(tool_input) if isinstance(tool_input, dict) else {}
|
|
25
|
+
|
|
26
|
+
if tool == "Bash":
|
|
27
|
+
tool_input["command"] = tool_input.get("command") or tool_input.get("cmd", "")
|
|
28
|
+
route = "bash"
|
|
29
|
+
elif tool in ("Read", "Glob", "Grep"):
|
|
30
|
+
route = "read"
|
|
31
|
+
elif tool == "Skill":
|
|
32
|
+
route = "skill"
|
|
33
|
+
elif tool.startswith("mcp__"):
|
|
34
|
+
target = next(
|
|
35
|
+
(
|
|
36
|
+
tool_input.get(key)
|
|
37
|
+
for key in ("file_path", "path", "filename", "uri")
|
|
38
|
+
if tool_input.get(key)
|
|
39
|
+
),
|
|
40
|
+
None,
|
|
41
|
+
)
|
|
42
|
+
if not target or str(target).startswith(("http://", "https://")):
|
|
43
|
+
return None, payload
|
|
44
|
+
is_search = any(word in tool.lower() for word in ("search", "grep", "find"))
|
|
45
|
+
tool_input = {"path" if is_search else "file_path": target}
|
|
46
|
+
tool = "Grep" if is_search else "Read"
|
|
47
|
+
route = "read"
|
|
48
|
+
else:
|
|
49
|
+
return None, payload
|
|
50
|
+
|
|
51
|
+
normalized = dict(payload)
|
|
52
|
+
normalized["tool_name"] = tool
|
|
53
|
+
normalized["tool_input"] = tool_input
|
|
54
|
+
return route, normalized
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def translate(payload):
|
|
58
|
+
event = payload.get("hook_event_name", "")
|
|
59
|
+
if event == "SessionStart":
|
|
60
|
+
normalized = dict(payload)
|
|
61
|
+
normalized["source"] = payload.get("source", "codex")
|
|
62
|
+
return "session", normalized
|
|
63
|
+
if event == "UserPromptSubmit":
|
|
64
|
+
return "prompt", payload
|
|
65
|
+
if event == "PostToolUse":
|
|
66
|
+
return normalize_tool(payload)
|
|
67
|
+
if event == "PostToolUseFailure" and payload.get("tool_name") == "Bash":
|
|
68
|
+
return "bash-failure", payload
|
|
69
|
+
if event == "SessionEnd":
|
|
70
|
+
normalized = dict(payload)
|
|
71
|
+
normalized["reason"] = payload.get("reason", "claude-session-end")
|
|
72
|
+
return "outcome", normalized
|
|
73
|
+
if event == "Stop":
|
|
74
|
+
if not os.environ.get("PLUGIN_ROOT"):
|
|
75
|
+
return None, payload
|
|
76
|
+
normalized = dict(payload)
|
|
77
|
+
normalized["reason"] = payload.get("reason", "codex-stop")
|
|
78
|
+
return "outcome", normalized
|
|
79
|
+
return None, payload
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def main():
|
|
83
|
+
# Codex documents PLUGIN_ROOT for plugin-bundled hooks. Claude compatibility
|
|
84
|
+
# exports CLAUDE_PLUGIN_ROOT but not this Codex-specific presence marker.
|
|
85
|
+
if "--client" in sys.argv:
|
|
86
|
+
try:
|
|
87
|
+
client = sys.argv[sys.argv.index("--client") + 1]
|
|
88
|
+
except IndexError:
|
|
89
|
+
client = ""
|
|
90
|
+
if client == "codex" and not os.environ.get("PLUGIN_ROOT"):
|
|
91
|
+
return
|
|
92
|
+
payload = read_payload(sys.stdin)
|
|
93
|
+
route, normalized = translate(payload)
|
|
94
|
+
if not route:
|
|
95
|
+
return
|
|
96
|
+
|
|
97
|
+
script = os.path.join(os.path.dirname(os.path.abspath(__file__)), "tt-log.py")
|
|
98
|
+
old_argv, old_stdin = sys.argv, sys.stdin
|
|
99
|
+
old_root = os.environ.get("TT_PROJECT_DIR")
|
|
100
|
+
try:
|
|
101
|
+
os.environ["TT_PROJECT_DIR"] = project_root(normalized.get("cwd"))
|
|
102
|
+
sys.argv = [script, route]
|
|
103
|
+
sys.stdin = io.StringIO(json.dumps(normalized))
|
|
104
|
+
runpy.run_path(script, run_name="__main__")
|
|
105
|
+
except SystemExit:
|
|
106
|
+
pass
|
|
107
|
+
finally:
|
|
108
|
+
sys.argv, sys.stdin = old_argv, old_stdin
|
|
109
|
+
if old_root is None:
|
|
110
|
+
os.environ.pop("TT_PROJECT_DIR", None)
|
|
111
|
+
else:
|
|
112
|
+
os.environ["TT_PROJECT_DIR"] = old_root
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
if __name__ == "__main__":
|
|
116
|
+
try:
|
|
117
|
+
main()
|
|
118
|
+
except Exception:
|
|
119
|
+
pass
|
|
120
|
+
sys.exit(0)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# trigger-tree — default configuration.
|
|
2
|
+
# Override per project: create $PROJECT/.trigger-tree/config.sh with the same variables.
|
|
3
|
+
# Paths are relative to the project root.
|
|
4
|
+
|
|
5
|
+
# A Read of a file matching this regex counts as a documentation read.
|
|
6
|
+
TT_WATCH_REGEX='^(docs|agents|skills|agent-briefs)/.*\.md$|^\.claude/(rules|skills)/.*\.md$|^(CLAUDE|AGENTS|GEMINI)\.md$'
|
|
7
|
+
|
|
8
|
+
# A Glob/Grep whose explicit target dir matches this regex counts as search activity.
|
|
9
|
+
# The event records that a search happened, not why it happened or whether routing failed.
|
|
10
|
+
TT_SCAN_REGEX='^(docs|agents|skills|agent-briefs)(/|$)'
|
|
11
|
+
|
|
12
|
+
# Files matching this regex are loaded automatically (system-prompt injection, nested
|
|
13
|
+
# CLAUDE.md on-demand loading, Skill tool) and therefore cannot be judged through
|
|
14
|
+
# Read telemetry — excluded from untouched review-candidate analysis.
|
|
15
|
+
TT_ALWAYS_LOADED_REGEX='(^|/)(CLAUDE|AGENTS|GEMINI)\.md$|(^|/)CLAUDE\.local\.md$|^\.claude/skills/'
|
|
16
|
+
# Example project override additions for other instruction systems:
|
|
17
|
+
# TT_ALWAYS_LOADED_REGEX='...|^\.github/copilot-instructions\.md$|^\.cursor/rules/'
|
|
18
|
+
|
|
19
|
+
# Comma-separated globs for rare-but-critical documentation that must be reviewed,
|
|
20
|
+
# never treated as an archive candidate. Safety paths are protected regardless.
|
|
21
|
+
TT_CRITICAL_GLOB=''
|
|
22
|
+
|
|
23
|
+
# Default: a recognizable preview of the first 200 characters, stored only in the
|
|
24
|
+
# project's gitignored telemetry. Setup offers truncate, hash, and off explicitly.
|
|
25
|
+
TT_LOG_PROMPTS='truncate'
|
|
26
|
+
|
|
27
|
+
# Rotate history.jsonl to history-<timestamp>.jsonl when it exceeds this many bytes.
|
|
28
|
+
TT_ROTATE_BYTES='5242880'
|
|
29
|
+
|
|
30
|
+
# Experimental, correlational view joining reads with local session outcomes.
|
|
31
|
+
# Values: off (default) or on. This never makes causal claims or sends data anywhere.
|
|
32
|
+
TT_EXPERIMENTAL_OUTCOMES='off'
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Explain whether trigger-tree is installed and receiving usable telemetry."""
|
|
3
|
+
|
|
4
|
+
import glob
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import re
|
|
8
|
+
import sys
|
|
9
|
+
import time
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
|
|
12
|
+
from tt_scope import is_poor_coverage, scan_markdown
|
|
13
|
+
|
|
14
|
+
ROOT = os.environ.get("CLAUDE_PROJECT_DIR") or os.getcwd()
|
|
15
|
+
PLUGIN_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
16
|
+
SCHEMA_VERSION = 1
|
|
17
|
+
SUPPORTED_PYTHON = (3, 10), (3, 13)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def load_json(path):
|
|
21
|
+
try:
|
|
22
|
+
with open(path, encoding="utf-8") as fh:
|
|
23
|
+
return json.load(fh)
|
|
24
|
+
except (OSError, json.JSONDecodeError):
|
|
25
|
+
return None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def history_health():
|
|
29
|
+
paths = sorted(glob.glob(os.path.join(ROOT, ".trigger-tree", "history*.jsonl")))
|
|
30
|
+
if not paths:
|
|
31
|
+
return "WARN", "telemetry: no history yet — start a fresh session and read a doc"
|
|
32
|
+
valid = corrupt = legacy = future = 0
|
|
33
|
+
latest = None
|
|
34
|
+
try:
|
|
35
|
+
for path in paths:
|
|
36
|
+
with open(path, encoding="utf-8", errors="replace") as fh:
|
|
37
|
+
for line in fh:
|
|
38
|
+
try:
|
|
39
|
+
event = json.loads(line)
|
|
40
|
+
except json.JSONDecodeError:
|
|
41
|
+
corrupt += 1
|
|
42
|
+
continue
|
|
43
|
+
if not isinstance(event, dict) or not event.get("t"):
|
|
44
|
+
corrupt += 1
|
|
45
|
+
continue
|
|
46
|
+
version = event.get("schema_version", 0)
|
|
47
|
+
if version == 0:
|
|
48
|
+
legacy += 1
|
|
49
|
+
elif version != SCHEMA_VERSION:
|
|
50
|
+
future += 1
|
|
51
|
+
continue
|
|
52
|
+
valid += 1
|
|
53
|
+
latest = event.get("ts") or latest
|
|
54
|
+
except OSError:
|
|
55
|
+
return "FAIL", "telemetry: history exists but cannot be read"
|
|
56
|
+
if future:
|
|
57
|
+
return (
|
|
58
|
+
"FAIL",
|
|
59
|
+
f"telemetry: {future} event(s) use a newer schema — update trigger-tree before reading this history",
|
|
60
|
+
)
|
|
61
|
+
if not valid:
|
|
62
|
+
return (
|
|
63
|
+
"FAIL",
|
|
64
|
+
"telemetry: history contains no usable events — move corrupt logs aside or restart telemetry",
|
|
65
|
+
)
|
|
66
|
+
suffix = f", latest {latest}" if latest else ""
|
|
67
|
+
rotation = f", {len(paths) - 1} rotated file(s) included" if len(paths) > 1 else ""
|
|
68
|
+
migration = f", {legacy} legacy event(s) migrated" if legacy else ""
|
|
69
|
+
if corrupt:
|
|
70
|
+
return (
|
|
71
|
+
"WARN",
|
|
72
|
+
f"telemetry: {valid} usable events{suffix}{rotation}{migration}; {corrupt} corrupt line(s) ignored — inspect history*.jsonl",
|
|
73
|
+
)
|
|
74
|
+
return "PASS", f"telemetry: {valid} usable events{suffix}{rotation}{migration}"
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def hooks_health():
|
|
78
|
+
claude_manifest = load_json(os.path.join(PLUGIN_ROOT, "hooks", "claude-hooks.json"))
|
|
79
|
+
hooks = claude_manifest.get("hooks", {}) if isinstance(claude_manifest, dict) else {}
|
|
80
|
+
commands = json.dumps(claude_manifest) if claude_manifest else ""
|
|
81
|
+
claude_ok = {
|
|
82
|
+
"SessionStart",
|
|
83
|
+
"UserPromptSubmit",
|
|
84
|
+
"PostToolUse",
|
|
85
|
+
"PostToolUseFailure",
|
|
86
|
+
"SessionEnd",
|
|
87
|
+
}.issubset(hooks) and all(
|
|
88
|
+
marker in commands
|
|
89
|
+
for marker in (
|
|
90
|
+
"Bash",
|
|
91
|
+
"Read",
|
|
92
|
+
"Glob",
|
|
93
|
+
"Grep",
|
|
94
|
+
"Skill",
|
|
95
|
+
"tt-codex-hook.py",
|
|
96
|
+
"--client",
|
|
97
|
+
"claude",
|
|
98
|
+
)
|
|
99
|
+
)
|
|
100
|
+
codex_manifest = load_json(os.path.join(PLUGIN_ROOT, "hooks", "hooks.json"))
|
|
101
|
+
codex_hooks = codex_manifest.get("hooks", {}) if isinstance(codex_manifest, dict) else {}
|
|
102
|
+
codex_commands = json.dumps(codex_manifest) if codex_manifest else ""
|
|
103
|
+
codex_ok = {"SessionStart", "UserPromptSubmit", "PostToolUse", "Stop"}.issubset(
|
|
104
|
+
codex_hooks
|
|
105
|
+
) and all(
|
|
106
|
+
marker in codex_commands
|
|
107
|
+
for marker in ("tt-codex-hook.py", "CLAUDE_PLUGIN_ROOT", "--client codex")
|
|
108
|
+
)
|
|
109
|
+
if claude_ok and codex_ok:
|
|
110
|
+
return "PASS", "plugin hook files: Claude Code and Codex routes are intact"
|
|
111
|
+
return "FAIL", "plugin hook files: missing logger routes — reinstall the plugin"
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def watch_regex():
|
|
115
|
+
for path in (
|
|
116
|
+
os.path.join(ROOT, ".trigger-tree", "config.sh"),
|
|
117
|
+
os.path.join(PLUGIN_ROOT, "scripts", "tt-config.sh"),
|
|
118
|
+
):
|
|
119
|
+
try:
|
|
120
|
+
text = open(path, encoding="utf-8").read()
|
|
121
|
+
except OSError:
|
|
122
|
+
continue
|
|
123
|
+
match = re.search(r"TT_WATCH_REGEX='([^']+)'", text)
|
|
124
|
+
if match:
|
|
125
|
+
return match.group(1)
|
|
126
|
+
return r"(?!)"
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def coverage_health():
|
|
130
|
+
result = scan_markdown(ROOT, watch_regex())
|
|
131
|
+
summary = f"coverage: {result['watched']} of {result['markdown']} markdown files watched"
|
|
132
|
+
remediation = "set TT_WATCH_REGEX in .trigger-tree/config.sh"
|
|
133
|
+
if result["markdown"] and result["watched"] == 0:
|
|
134
|
+
return "FAIL", f"{summary} — {remediation}"
|
|
135
|
+
if is_poor_coverage(result):
|
|
136
|
+
return "WARN", f"{summary} (very low) — {remediation}"
|
|
137
|
+
return "PASS", summary
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _lifecycle_events():
|
|
141
|
+
events = []
|
|
142
|
+
for path in sorted(glob.glob(os.path.join(ROOT, ".trigger-tree", "history*.jsonl"))):
|
|
143
|
+
try:
|
|
144
|
+
lines = open(path, encoding="utf-8", errors="replace")
|
|
145
|
+
except OSError:
|
|
146
|
+
continue
|
|
147
|
+
with lines:
|
|
148
|
+
for line in lines:
|
|
149
|
+
try:
|
|
150
|
+
event = json.loads(line)
|
|
151
|
+
except json.JSONDecodeError:
|
|
152
|
+
continue
|
|
153
|
+
if isinstance(event, dict) and event.get("t") in ("session", "read"):
|
|
154
|
+
events.append(event)
|
|
155
|
+
return events
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def liveness_health():
|
|
159
|
+
events = _lifecycle_events()
|
|
160
|
+
current = os.environ.get("CLAUDE_SESSION_ID")
|
|
161
|
+
if current:
|
|
162
|
+
if any(event.get("t") == "session" and event.get("session") == current for event in events):
|
|
163
|
+
return "PASS", "hook liveness: current session start was recorded"
|
|
164
|
+
return (
|
|
165
|
+
"FAIL",
|
|
166
|
+
"hook liveness: current session start is absent — check /hooks, restart the session, then reinstall the plugin if still absent",
|
|
167
|
+
)
|
|
168
|
+
state_paths = glob.glob(os.path.join(ROOT, ".trigger-tree", "sessions", "*.json"))
|
|
169
|
+
newest_state = max((os.path.getmtime(path) for path in state_paths), default=0)
|
|
170
|
+
timestamps = []
|
|
171
|
+
for event in events:
|
|
172
|
+
try:
|
|
173
|
+
timestamps.append(
|
|
174
|
+
datetime.fromisoformat(event.get("ts", "").replace("Z", "+00:00")).timestamp()
|
|
175
|
+
)
|
|
176
|
+
except (AttributeError, TypeError, ValueError):
|
|
177
|
+
pass
|
|
178
|
+
newest = max(timestamps + [newest_state], default=0)
|
|
179
|
+
if not newest:
|
|
180
|
+
return (
|
|
181
|
+
"WARN",
|
|
182
|
+
"hook liveness: no hook events have ever been recorded — run /tt setup and start a fresh session",
|
|
183
|
+
)
|
|
184
|
+
age_days = max(0, int((time.time() - newest) / 86400))
|
|
185
|
+
if age_days > 7:
|
|
186
|
+
return (
|
|
187
|
+
"WARN",
|
|
188
|
+
f"hook liveness: events exist but are stale ({age_days} days) — informational; start a fresh session to verify",
|
|
189
|
+
)
|
|
190
|
+
return "PASS", "hook liveness: recent session/read activity found"
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def config_health():
|
|
194
|
+
path = os.path.join(ROOT, ".trigger-tree", "config.sh")
|
|
195
|
+
if not os.path.isfile(path):
|
|
196
|
+
return "PASS", "config: defaults valid (no project override)"
|
|
197
|
+
try:
|
|
198
|
+
text = open(path, encoding="utf-8").read()
|
|
199
|
+
except OSError:
|
|
200
|
+
return "FAIL", "config: project override cannot be read — fix permissions or remove it"
|
|
201
|
+
assignments = dict(re.findall(r"(?m)^(TT_[A-Z_]+)='([^']*)'\s*$", text))
|
|
202
|
+
malformed = [
|
|
203
|
+
line.strip()
|
|
204
|
+
for line in text.splitlines()
|
|
205
|
+
if line.strip().startswith("TT_") and not re.match(r"^TT_[A-Z_]+='[^']*'\s*$", line.strip())
|
|
206
|
+
]
|
|
207
|
+
if malformed:
|
|
208
|
+
return "FAIL", f"config: unparseable assignment `{malformed[0]}` — use KEY='value'"
|
|
209
|
+
for name in ("TT_WATCH_REGEX", "TT_SCAN_REGEX", "TT_ALWAYS_LOADED_REGEX"):
|
|
210
|
+
if name in assignments:
|
|
211
|
+
try:
|
|
212
|
+
re.compile(assignments[name])
|
|
213
|
+
except re.error as exc:
|
|
214
|
+
return "FAIL", f"config: {name} is invalid ({exc}) — fix the regex"
|
|
215
|
+
prompt_mode = assignments.get("TT_LOG_PROMPTS")
|
|
216
|
+
if prompt_mode is not None and prompt_mode not in ("hash", "truncate", "off"):
|
|
217
|
+
return "FAIL", "config: TT_LOG_PROMPTS must be hash, truncate, or off"
|
|
218
|
+
experimental = assignments.get("TT_EXPERIMENTAL_OUTCOMES")
|
|
219
|
+
if experimental is not None and experimental not in ("on", "off"):
|
|
220
|
+
return "FAIL", "config: TT_EXPERIMENTAL_OUTCOMES must be on or off"
|
|
221
|
+
rotate = assignments.get("TT_ROTATE_BYTES")
|
|
222
|
+
if rotate is not None:
|
|
223
|
+
try:
|
|
224
|
+
if int(rotate) <= 0:
|
|
225
|
+
raise ValueError
|
|
226
|
+
except ValueError:
|
|
227
|
+
return "FAIL", "config: TT_ROTATE_BYTES must be a positive integer"
|
|
228
|
+
return "PASS", "config: project override parses and validates"
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def python_health():
|
|
232
|
+
current = sys.version_info[:2]
|
|
233
|
+
if SUPPORTED_PYTHON[0] <= current <= SUPPORTED_PYTHON[1]:
|
|
234
|
+
return "PASS", f"python: {current[0]}.{current[1]} is supported (3.10–3.13)"
|
|
235
|
+
return "FAIL", f"python: {current[0]}.{current[1]} unsupported — configure Python 3.10–3.13"
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def ignore_health():
|
|
239
|
+
path = os.path.join(ROOT, ".gitignore")
|
|
240
|
+
try:
|
|
241
|
+
lines = open(path, encoding="utf-8").read().splitlines()
|
|
242
|
+
except OSError:
|
|
243
|
+
lines = []
|
|
244
|
+
ignored = ".trigger-tree/" in lines or ".trigger-tree/*" in lines
|
|
245
|
+
if ignored:
|
|
246
|
+
return "PASS", "privacy: telemetry directory is gitignored"
|
|
247
|
+
return "FAIL", "privacy: .trigger-tree is not gitignored — run /tt setup"
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def statusline_health():
|
|
251
|
+
script = os.path.join(ROOT, ".claude", "tt-statusline.py")
|
|
252
|
+
settings = load_json(os.path.join(ROOT, ".claude", "settings.json"))
|
|
253
|
+
status = settings.get("statusLine", {}) if isinstance(settings, dict) else {}
|
|
254
|
+
command = status.get("command", "") if isinstance(status, dict) else ""
|
|
255
|
+
if os.path.isfile(script) and "tt-statusline.py" in command:
|
|
256
|
+
return "PASS", "statusline: installed and registered"
|
|
257
|
+
return "WARN", "statusline: not fully wired (telemetry still works) — run /tt setup"
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def main():
|
|
261
|
+
checks = [
|
|
262
|
+
hooks_health(),
|
|
263
|
+
liveness_health(),
|
|
264
|
+
config_health(),
|
|
265
|
+
coverage_health(),
|
|
266
|
+
python_health(),
|
|
267
|
+
ignore_health(),
|
|
268
|
+
statusline_health(),
|
|
269
|
+
history_health(),
|
|
270
|
+
]
|
|
271
|
+
print("🌳 trigger-tree doctor")
|
|
272
|
+
for state, message in checks:
|
|
273
|
+
icon = {"PASS": "✓", "WARN": "!", "FAIL": "✗"}[state]
|
|
274
|
+
print(f"{icon} {message}")
|
|
275
|
+
failures = sum(state == "FAIL" for state, _ in checks)
|
|
276
|
+
warnings = sum(state == "WARN" for state, _ in checks)
|
|
277
|
+
if failures:
|
|
278
|
+
print(f"attention needed — {failures} failed, {warnings} warnings")
|
|
279
|
+
elif warnings:
|
|
280
|
+
plural = "s" if warnings != 1 else ""
|
|
281
|
+
print(f"telemetry healthy — {warnings} optional setup warning{plural}")
|
|
282
|
+
else:
|
|
283
|
+
print("all checks passed — telemetry is wired and receiving events")
|
|
284
|
+
return 1 if failures else 0
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
if __name__ == "__main__":
|
|
288
|
+
sys.exit(main())
|