stdtel 0.2.3__tar.gz
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.
- stdtel-0.2.3/PKG-INFO +246 -0
- stdtel-0.2.3/README.md +221 -0
- stdtel-0.2.3/eval/__init__.py +0 -0
- stdtel-0.2.3/eval/fixtures/fastapi-min/app/main.py +14 -0
- stdtel-0.2.3/eval/power.py +199 -0
- stdtel-0.2.3/eval/run_eval.py +139 -0
- stdtel-0.2.3/pyproject.toml +49 -0
- stdtel-0.2.3/setup.cfg +4 -0
- stdtel-0.2.3/stdtel/__init__.py +2 -0
- stdtel-0.2.3/stdtel/doctor.py +211 -0
- stdtel-0.2.3/stdtel/enrich.py +59 -0
- stdtel-0.2.3/stdtel/exporter.py +113 -0
- stdtel-0.2.3/stdtel/hooks/__init__.py +0 -0
- stdtel-0.2.3/stdtel/hooks/cli.py +318 -0
- stdtel-0.2.3/stdtel/install.py +173 -0
- stdtel-0.2.3/stdtel/manifest.py +217 -0
- stdtel-0.2.3/stdtel/policy_report.py +126 -0
- stdtel-0.2.3/stdtel/skillmap.py +15 -0
- stdtel-0.2.3/stdtel/spool.py +124 -0
- stdtel-0.2.3/stdtel/spool_export.py +86 -0
- stdtel-0.2.3/stdtel/state.py +118 -0
- stdtel-0.2.3/stdtel/statusline.py +101 -0
- stdtel-0.2.3/stdtel/transcript.py +174 -0
- stdtel-0.2.3/stdtel.egg-info/PKG-INFO +246 -0
- stdtel-0.2.3/stdtel.egg-info/SOURCES.txt +51 -0
- stdtel-0.2.3/stdtel.egg-info/dependency_links.txt +1 -0
- stdtel-0.2.3/stdtel.egg-info/entry_points.txt +9 -0
- stdtel-0.2.3/stdtel.egg-info/requires.txt +10 -0
- stdtel-0.2.3/stdtel.egg-info/top_level.txt +2 -0
- stdtel-0.2.3/tests/test_adrs.py +69 -0
- stdtel-0.2.3/tests/test_dashboards.py +124 -0
- stdtel-0.2.3/tests/test_demo_seed.py +110 -0
- stdtel-0.2.3/tests/test_distribution.py +276 -0
- stdtel-0.2.3/tests/test_doc_links.py +25 -0
- stdtel-0.2.3/tests/test_docs_coverage.py +115 -0
- stdtel-0.2.3/tests/test_doctor.py +175 -0
- stdtel-0.2.3/tests/test_eval_and_map.py +14 -0
- stdtel-0.2.3/tests/test_hooks_e2e.py +47 -0
- stdtel-0.2.3/tests/test_live_payloads.py +69 -0
- stdtel-0.2.3/tests/test_load_delivery.py +140 -0
- stdtel-0.2.3/tests/test_manifest.py +112 -0
- stdtel-0.2.3/tests/test_policies.py +158 -0
- stdtel-0.2.3/tests/test_policy_report.py +156 -0
- stdtel-0.2.3/tests/test_power.py +143 -0
- stdtel-0.2.3/tests/test_publish_workflow.py +64 -0
- stdtel-0.2.3/tests/test_regressions.py +332 -0
- stdtel-0.2.3/tests/test_scorecard.py +107 -0
- stdtel-0.2.3/tests/test_skills_roots.py +72 -0
- stdtel-0.2.3/tests/test_spool.py +185 -0
- stdtel-0.2.3/tests/test_statusline.py +141 -0
- stdtel-0.2.3/tests/test_transcript.py +41 -0
- stdtel-0.2.3/tests/test_walkthroughs.py +75 -0
- stdtel-0.2.3/tests/test_warehouse.py +189 -0
stdtel-0.2.3/PKG-INFO
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: stdtel
|
|
3
|
+
Version: 0.2.3
|
|
4
|
+
Summary: Telemetry and metadata capture for standards-as-skills (Claude Code + Copilot)
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Project-URL: Homepage, https://github.com/amiable-dev/skills-telemetry
|
|
7
|
+
Project-URL: Repository, https://github.com/amiable-dev/skills-telemetry
|
|
8
|
+
Project-URL: Issues, https://github.com/amiable-dev/skills-telemetry/issues
|
|
9
|
+
Keywords: telemetry,opentelemetry,claude-code,copilot,skills,governance
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
14
|
+
Classifier: Topic :: System :: Monitoring
|
|
15
|
+
Requires-Python: >=3.10
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
Requires-Dist: pyyaml>=6
|
|
18
|
+
Requires-Dist: opentelemetry-sdk>=1.25
|
|
19
|
+
Requires-Dist: opentelemetry-exporter-otlp-proto-http>=1.25
|
|
20
|
+
Provides-Extra: dev
|
|
21
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
22
|
+
Provides-Extra: warehouse
|
|
23
|
+
Requires-Dist: psycopg[binary]>=3.1; extra == "warehouse"
|
|
24
|
+
Requires-Dist: requests>=2.31; extra == "warehouse"
|
|
25
|
+
|
|
26
|
+
# standards-telemetry
|
|
27
|
+
|
|
28
|
+
Telemetry and metadata capture for **standards-as-skills** — attributes token cost and outcomes to
|
|
29
|
+
individual skills across Claude Code and GitHub Copilot, joined to policy (OPA/Rego), delivery
|
|
30
|
+
(Linear/GitHub) and quality data. Design rationale: [docs/design-proposal.md](docs/design-proposal.md).
|
|
31
|
+
|
|
32
|
+
Metadata only. No prompt, response or file content is ever emitted; the collector drops it again as
|
|
33
|
+
defence in depth. If it runs on your machine, [docs/for-developers.md](docs/for-developers.md) lists
|
|
34
|
+
every field that leaves it — and `export STDTEL_DISABLED=1` turns it off entirely.
|
|
35
|
+
|
|
36
|
+
## When can I trust these numbers?
|
|
37
|
+
|
|
38
|
+
Telemetry starts producing plausible-looking ratios on day one. Most of them mean nothing yet. The
|
|
39
|
+
phases below are set by **data volume, not elapsed time** — how long each takes depends entirely on
|
|
40
|
+
team size, and a small team may never leave the first one.
|
|
41
|
+
|
|
42
|
+
| phase | you have | what it supports | what it does not |
|
|
43
|
+
|---|---|---|---|
|
|
44
|
+
| **Descriptive** | anything below the floor | cost, usage, and finding data-quality faults — `unversioned` skills, `unattributed` branches | any comparison between skills, harnesses, or arms |
|
|
45
|
+
| **Directional** | 30+ merged PRs per arm, 5+ developers | spotting large effects (>40%) as a hypothesis, always with an interval | point estimates, or a keep/deprecate decision |
|
|
46
|
+
| **Inferential** | 150-400+ PRs per arm, depending on effect size | keep / refine / merge / deprecate decisions | detecting effects under 20%, which needs 900+ |
|
|
47
|
+
|
|
48
|
+
**The hard floor: below 30 merged PRs per arm, or fewer than 5 developers, report descriptively and make no comparative claim.**
|
|
49
|
+
|
|
50
|
+
Early on, the most valuable thing this data does is find its own faults. A high share of `unversioned`
|
|
51
|
+
skills or `unattributed` tickets bounds every later conclusion, and both are fixable now — see
|
|
52
|
+
[`stdtel-onboard`](skills/stdtel-onboard/SKILL.md) and ticket-prefixed branches.
|
|
53
|
+
|
|
54
|
+
The failure mode this exists to prevent: reading a scorecard after two weeks, seeing a skill
|
|
55
|
+
"underperform" across nine PRs, and deprecating it. Nine PRs cannot distinguish a bad skill from a
|
|
56
|
+
quiet fortnight. Full derivations and the assumptions behind every figure:
|
|
57
|
+
[docs/evaluation-power.md](docs/evaluation-power.md). Worked examples of asking these
|
|
58
|
+
questions, including the ones the data cannot answer:
|
|
59
|
+
[docs/insight-walkthroughs.md](docs/insight-walkthroughs.md).
|
|
60
|
+
|
|
61
|
+
## Documentation
|
|
62
|
+
|
|
63
|
+
| page | for |
|
|
64
|
+
|---|---|
|
|
65
|
+
| [docs/for-developers.md](docs/for-developers.md) | **if this runs on your machine**: exactly what is collected, and how to switch it off |
|
|
66
|
+
| [docs/reference.md](docs/reference.md) | every CLI, its flags and exit codes, and the authoritative `STDTEL_*` table |
|
|
67
|
+
| [docs/skills.md](docs/skills.md) | each skill and the agent — when to use, when not to, what it refuses |
|
|
68
|
+
| [docs/evaluation-power.md](docs/evaluation-power.md) | how much data before a comparison means anything |
|
|
69
|
+
| [docs/insight-walkthroughs.md](docs/insight-walkthroughs.md) | worked examples with real output, including the misreadings |
|
|
70
|
+
| [docs/local-stack.md](docs/local-stack.md) | endpoints, credentials, the verification ladder, and how to reset |
|
|
71
|
+
| [docs/adrs/](docs/adrs/) | why things are the way they are |
|
|
72
|
+
|
|
73
|
+
## Layout
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
skills/<name>/SKILL.md skill catalogue with validated front-matter (the contract)
|
|
77
|
+
stdtel/manifest.py front-matter parser + `stdtel-validate` CI gate
|
|
78
|
+
stdtel/hooks/cli.py Claude Code hooks: session-start | pre-tool-use | post-tool-use | stop
|
|
79
|
+
stdtel/transcript.py incremental JSONL reader + token attribution (tail rule, first-only sensitivity)
|
|
80
|
+
stdtel/exporter.py std.skill.invocation spans via OTLP/HTTP (content scrubbed)
|
|
81
|
+
stdtel/enrich.py join keys: std.ticket.id from branch, std.repo, std.team, std.harness
|
|
82
|
+
stdtel/skillmap.py generates collector/copilot-skill-map.yaml for Copilot tool-call mapping
|
|
83
|
+
collector/otel-collector.yaml drop content → normalise gen_ai.* → map Copilot skills → pseudonymise → spanmetrics
|
|
84
|
+
deploy/docker-compose.yml collector + Tempo + Prometheus + Grafana + Postgres; `langfuse` profile optional
|
|
85
|
+
deploy/smoke.sh eight-hop verification ladder (`mise run smoke`)
|
|
86
|
+
collector/overlay-*.yaml merged over the base config; `none` is the default no-op, `langfuse` adds an exporter
|
|
87
|
+
warehouse/schema.sql skill_invocation, session_cost, ticket, pull_request, policy_result, defect, skill_eval
|
|
88
|
+
warehouse/scorecard.sql weekly per-skill scorecard → keep / refine / review-merge / deprecate
|
|
89
|
+
warehouse/load_traces.py Tempo → Postgres loader
|
|
90
|
+
eval/run_eval.py offline with/without-skill eval, real OPA grading (`--dry-run` is a smoke test)
|
|
91
|
+
eval/power.py generates every table in docs/evaluation-power.md; CI checks it is current
|
|
92
|
+
policies/ Rego behind the primary metric: logging.*, telemetry.manifest_valid
|
|
93
|
+
agents/ skill-scorecard-analyst: keep / refine / merge / deprecate from the data
|
|
94
|
+
warehouse/load_delivery.py GitHub → ticket / pull_request / defect; policy_result from a CI artefact
|
|
95
|
+
examples/settings.*.json hook + OTel wiring: `global` installs once, `project` overrides per repo
|
|
96
|
+
docs/adrs/ six ADRs: distribution, delivery data, hook constraints, identity, integrity, Langfuse
|
|
97
|
+
plugin.json Agent Plugins v1 manifest (portable `skills/` is the shared half)
|
|
98
|
+
.claude-plugin/ Claude Code plugin + marketplace manifest
|
|
99
|
+
hooks/, com.github.copilot/ per-harness hook manifests, generated from stdtel/install.py::EVENTS
|
|
100
|
+
stdtel/install.py `stdtel-install`: absolute-path resolution + settings merge
|
|
101
|
+
mise.toml toolchain (Python 3.13) + `.venv` + tasks wrapping the Makefile
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Quick start
|
|
105
|
+
|
|
106
|
+
Two separate jobs: **install the harness wiring once for your user**, then **onboard each project** you
|
|
107
|
+
want attributed telemetry from. Skipping the second step still gives you spans — the skills just come
|
|
108
|
+
through as `unversioned`, with `std.team=unknown`.
|
|
109
|
+
|
|
110
|
+
### 1. Install once, globally
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
uv tool install stdtel # or: pipx install stdtel
|
|
114
|
+
stdtel-install settings # merges hooks into ~/.claude/settings.json
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
> **Not published yet.** `stdtel` is not on PyPI until the first release
|
|
118
|
+
> ([docs/releasing.md](docs/releasing.md)). Until then install from a checkout:
|
|
119
|
+
> `uv tool install /path/to/skills-telemetry`. This matters more than it looks: the plugin's hooks
|
|
120
|
+
> call a launcher that exits silently when it cannot find the CLI, so an uninstalled package produces
|
|
121
|
+
> **no data and no error**. `stdtel-doctor` says so explicitly.
|
|
122
|
+
|
|
123
|
+
`stdtel-install` resolves the **absolute path** of the `stdtel-hook` it was installed alongside and
|
|
124
|
+
writes that into the config. This is not cosmetic: hook processes get a non-login `sh -c` and inherit
|
|
125
|
+
whatever PATH launched the harness, so a bare `stdtel-hook` is unresolvable whenever a version manager
|
|
126
|
+
(mise, asdf, pyenv) or an activated venv is what put it there. The same reason pre-commit bakes
|
|
127
|
+
`sys.executable` into the git hook it generates. `stdtel-install where` prints the path it will use;
|
|
128
|
+
`--dry-run` shows the JSON without writing.
|
|
129
|
+
|
|
130
|
+
Claude Code also **strips `OTEL_*` from every subprocess it spawns**, so point the exporter at your
|
|
131
|
+
collector with the `STDTEL_`-namespaced variables, which survive:
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
export STDTEL_OTLP_ENDPOINT=http://collector.internal:4318 # default: http://localhost:4318
|
|
135
|
+
export STDTEL_OTLP_TIMEOUT=2 # seconds; bounds a dead-collector stall
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
With no `STDTEL_SKILLS_ROOT` set, the catalogue is read from `~/.claude/skills`. Symlink your
|
|
139
|
+
standards there and every project gets versioned spans:
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
ln -s "$PWD/skills/structured-logging" ~/.claude/skills/structured-logging
|
|
143
|
+
stdtel-validate ~/.claude/skills # same contract gate CI runs
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
#### As a plugin
|
|
147
|
+
|
|
148
|
+
The repo is laid out for three loaders at once (ADR-001), so it installs as a plugin without a
|
|
149
|
+
separate packaging step:
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
/plugin marketplace add amiable-dev/skills-telemetry
|
|
153
|
+
/plugin install stdtel@amiable-standards
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
The shipped `hooks/hooks.json` carries the bare command name, because a distributed manifest cannot
|
|
157
|
+
know your install path — run `stdtel-install settings` afterwards to bind it to an absolute one.
|
|
158
|
+
|
|
159
|
+
#### Copilot
|
|
160
|
+
|
|
161
|
+
Copilot needs **no code from us**. It emits first-party OpenTelemetry with per-tool-call spans and
|
|
162
|
+
token counts; point it at the same collector (user settings, `COPILOT_OTEL_*` env vars, or the
|
|
163
|
+
enterprise `managed-settings.json` `telemetry` block for a fleet).
|
|
164
|
+
|
|
165
|
+
If you do run our hooks on Copilot as well, install them from `com.github.copilot/hooks/hooks.json`,
|
|
166
|
+
which sets `STDTEL_HARNESS` per hook. That is load-bearing: VS Code Copilot **reads
|
|
167
|
+
`~/.claude/settings.json`**, and its snake_case payload dialect is indistinguishable from Claude
|
|
168
|
+
Code's — without that env block, Copilot activity is recorded as `claude-code`.
|
|
169
|
+
|
|
170
|
+
### 2. Onboard a project
|
|
171
|
+
|
|
172
|
+
Per-project overrides go in `<project>/.claude/settings.json` — Claude Code merges them over the user
|
|
173
|
+
file, so repeat only what differs. Do **not** repeat the `hooks` block: it is already registered globally
|
|
174
|
+
and a second copy fires each hook twice.
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
cd ~/projects/payments-api
|
|
178
|
+
mkdir -p .claude && cp ~/projects/skills-telemetry/examples/settings.project.json .claude/settings.json
|
|
179
|
+
$EDITOR .claude/settings.json # STDTEL_TEAM is the one you must set
|
|
180
|
+
git checkout -b feature/PLAT-123-add-audit-log # ticket prefix -> std.ticket.id join key
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
| variable | where | meaning |
|
|
184
|
+
|---|---|---|
|
|
185
|
+
| `STDTEL_TEAM` | project | owning team on every span; `unknown` until you set it |
|
|
186
|
+
| `STDTEL_HARNESS_MODE` | project | `agent` / `interactive` — keeps the Claude Code vs Copilot split fair |
|
|
187
|
+
| `STDTEL_SKILLS_ROOT` | project | extra catalogue root(s), `os.pathsep`-separated. A relative path resolves against the project directory; `~/.claude/skills` is always searched last, and the earliest root wins a name collision |
|
|
188
|
+
| `STDTEL_HARNESS`, `OTEL_*` | global | harness label and collector endpoint |
|
|
189
|
+
|
|
190
|
+
Then verify the loop end to end:
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
claude # invoke a skill in the project
|
|
194
|
+
cat ~/.stdtel/sessions/*.json # a window with skill, version, tool_use_id
|
|
195
|
+
curl -s 'http://localhost:3200/api/search?tags=name%3Dstd.skill.invocation' | jq '.traces[0]'
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
A `std.skill.version` of `unversioned` means the name in the transcript matched no `SKILL.md` in any root
|
|
199
|
+
— check `STDTEL_SKILLS_ROOT` and that the skill's front-matter `name` matches what you invoked.
|
|
200
|
+
|
|
201
|
+
Copilot: enable managed OTel export (VS Code / CLI) pointing at the same collector with resource attributes
|
|
202
|
+
`std.harness=copilot-vscode`, `std.team=<team>`; `collector/otel-collector.yaml` maps catalogued skill
|
|
203
|
+
tool-calls onto `std.skill.*`.
|
|
204
|
+
|
|
205
|
+
## Span schema
|
|
206
|
+
|
|
207
|
+
Two span types, emitted at `Stop`. **They overlap by design and must never be summed:**
|
|
208
|
+
`std.session.cost` is the session's total spend, `std.skill.invocation` attributes a *share* of that
|
|
209
|
+
total to one skill. Use session cost as the denominator for cost-per-PR; use invocation tail tokens to
|
|
210
|
+
compare skills with each other.
|
|
211
|
+
|
|
212
|
+
### `std.session.cost`
|
|
213
|
+
|
|
214
|
+
Emitted once per turn that made any LLM request, **whether or not a skill was loaded** — a session that
|
|
215
|
+
never loads a skill is still spend, and excluding it would silently understate cost per PR.
|
|
216
|
+
|
|
217
|
+
| attribute | source |
|
|
218
|
+
|---|---|
|
|
219
|
+
| `gen_ai.usage.{input,output,cache_read_input,cache_creation_input}_tokens` | whole transcript slice |
|
|
220
|
+
| `std.session.llm_requests`, `gen_ai.request.model` | transcript |
|
|
221
|
+
| `std.session.tool_calls`, `std.session.tool_failures` | every tool call in the turn |
|
|
222
|
+
| `std.session.tool.<name>.{calls,failures}` | per tool; `failures` omitted when zero |
|
|
223
|
+
| `std.ticket.id`, `std.repo`, `std.team`, `std.harness` | resource (SessionStart) |
|
|
224
|
+
|
|
225
|
+
### `std.skill.invocation`
|
|
226
|
+
|
|
227
|
+
| attribute | source |
|
|
228
|
+
|---|---|
|
|
229
|
+
| `std.skill.name/version/trigger`, `std.standard_id`, `std.policy.ids` | hook + manifest |
|
|
230
|
+
| `std.skill.invoked_as`, `std.skill.plugin` | raw invocation string (plugin skills are namespaced) |
|
|
231
|
+
| `std.skill.load_tokens`, `std.skill.tail_tokens`, `std.skill.tail_tokens_first_only`, `std.skill.llm_requests` | transcript attribution |
|
|
232
|
+
| `gen_ai.usage.{input,output,cache_read_input,cache_creation_input}_tokens`, `gen_ai.request.model` | transcript |
|
|
233
|
+
| `std.ticket.id`, `std.repo`, `std.team`, `std.harness`, `std.harness.mode` | resource (SessionStart) |
|
|
234
|
+
| `std.user.hash` | collector (pseudonymised) |
|
|
235
|
+
|
|
236
|
+
## Known limitations
|
|
237
|
+
|
|
238
|
+
- Tool counts are counts only. `PostToolUse` fires for **every** tool, so `tool_input` and
|
|
239
|
+
`tool_response` carry commands, file contents and diffs — `scrub()` refuses those attribute names
|
|
240
|
+
outright and a test asserts nothing leaks.
|
|
241
|
+
- Skill name is parsed from the Skill tool input in hooks (the field is `skill`, verified against 120 real invocations). Parsing is isolated in `hooks/cli.py::_skill_from_payload`.
|
|
242
|
+
- `std.skill.trigger` reports the transcript's `caller.type` where present, else `unknown` — it is never guessed.
|
|
243
|
+
- Tail attribution splits by load order; when several skills load in one turn compare against `tail_tokens_first_only`.
|
|
244
|
+
- Copilot granularity is per turn; use Claude Code's finer data for within-harness tuning only.
|
|
245
|
+
- `load_tokens` uses a chars/4 heuristic on the Skill tool result.
|
|
246
|
+
- `gen_ai.*` conventions are still *Development* upstream; extend `transform/normalise` as names move.
|
stdtel-0.2.3/README.md
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
# standards-telemetry
|
|
2
|
+
|
|
3
|
+
Telemetry and metadata capture for **standards-as-skills** — attributes token cost and outcomes to
|
|
4
|
+
individual skills across Claude Code and GitHub Copilot, joined to policy (OPA/Rego), delivery
|
|
5
|
+
(Linear/GitHub) and quality data. Design rationale: [docs/design-proposal.md](docs/design-proposal.md).
|
|
6
|
+
|
|
7
|
+
Metadata only. No prompt, response or file content is ever emitted; the collector drops it again as
|
|
8
|
+
defence in depth. If it runs on your machine, [docs/for-developers.md](docs/for-developers.md) lists
|
|
9
|
+
every field that leaves it — and `export STDTEL_DISABLED=1` turns it off entirely.
|
|
10
|
+
|
|
11
|
+
## When can I trust these numbers?
|
|
12
|
+
|
|
13
|
+
Telemetry starts producing plausible-looking ratios on day one. Most of them mean nothing yet. The
|
|
14
|
+
phases below are set by **data volume, not elapsed time** — how long each takes depends entirely on
|
|
15
|
+
team size, and a small team may never leave the first one.
|
|
16
|
+
|
|
17
|
+
| phase | you have | what it supports | what it does not |
|
|
18
|
+
|---|---|---|---|
|
|
19
|
+
| **Descriptive** | anything below the floor | cost, usage, and finding data-quality faults — `unversioned` skills, `unattributed` branches | any comparison between skills, harnesses, or arms |
|
|
20
|
+
| **Directional** | 30+ merged PRs per arm, 5+ developers | spotting large effects (>40%) as a hypothesis, always with an interval | point estimates, or a keep/deprecate decision |
|
|
21
|
+
| **Inferential** | 150-400+ PRs per arm, depending on effect size | keep / refine / merge / deprecate decisions | detecting effects under 20%, which needs 900+ |
|
|
22
|
+
|
|
23
|
+
**The hard floor: below 30 merged PRs per arm, or fewer than 5 developers, report descriptively and make no comparative claim.**
|
|
24
|
+
|
|
25
|
+
Early on, the most valuable thing this data does is find its own faults. A high share of `unversioned`
|
|
26
|
+
skills or `unattributed` tickets bounds every later conclusion, and both are fixable now — see
|
|
27
|
+
[`stdtel-onboard`](skills/stdtel-onboard/SKILL.md) and ticket-prefixed branches.
|
|
28
|
+
|
|
29
|
+
The failure mode this exists to prevent: reading a scorecard after two weeks, seeing a skill
|
|
30
|
+
"underperform" across nine PRs, and deprecating it. Nine PRs cannot distinguish a bad skill from a
|
|
31
|
+
quiet fortnight. Full derivations and the assumptions behind every figure:
|
|
32
|
+
[docs/evaluation-power.md](docs/evaluation-power.md). Worked examples of asking these
|
|
33
|
+
questions, including the ones the data cannot answer:
|
|
34
|
+
[docs/insight-walkthroughs.md](docs/insight-walkthroughs.md).
|
|
35
|
+
|
|
36
|
+
## Documentation
|
|
37
|
+
|
|
38
|
+
| page | for |
|
|
39
|
+
|---|---|
|
|
40
|
+
| [docs/for-developers.md](docs/for-developers.md) | **if this runs on your machine**: exactly what is collected, and how to switch it off |
|
|
41
|
+
| [docs/reference.md](docs/reference.md) | every CLI, its flags and exit codes, and the authoritative `STDTEL_*` table |
|
|
42
|
+
| [docs/skills.md](docs/skills.md) | each skill and the agent — when to use, when not to, what it refuses |
|
|
43
|
+
| [docs/evaluation-power.md](docs/evaluation-power.md) | how much data before a comparison means anything |
|
|
44
|
+
| [docs/insight-walkthroughs.md](docs/insight-walkthroughs.md) | worked examples with real output, including the misreadings |
|
|
45
|
+
| [docs/local-stack.md](docs/local-stack.md) | endpoints, credentials, the verification ladder, and how to reset |
|
|
46
|
+
| [docs/adrs/](docs/adrs/) | why things are the way they are |
|
|
47
|
+
|
|
48
|
+
## Layout
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
skills/<name>/SKILL.md skill catalogue with validated front-matter (the contract)
|
|
52
|
+
stdtel/manifest.py front-matter parser + `stdtel-validate` CI gate
|
|
53
|
+
stdtel/hooks/cli.py Claude Code hooks: session-start | pre-tool-use | post-tool-use | stop
|
|
54
|
+
stdtel/transcript.py incremental JSONL reader + token attribution (tail rule, first-only sensitivity)
|
|
55
|
+
stdtel/exporter.py std.skill.invocation spans via OTLP/HTTP (content scrubbed)
|
|
56
|
+
stdtel/enrich.py join keys: std.ticket.id from branch, std.repo, std.team, std.harness
|
|
57
|
+
stdtel/skillmap.py generates collector/copilot-skill-map.yaml for Copilot tool-call mapping
|
|
58
|
+
collector/otel-collector.yaml drop content → normalise gen_ai.* → map Copilot skills → pseudonymise → spanmetrics
|
|
59
|
+
deploy/docker-compose.yml collector + Tempo + Prometheus + Grafana + Postgres; `langfuse` profile optional
|
|
60
|
+
deploy/smoke.sh eight-hop verification ladder (`mise run smoke`)
|
|
61
|
+
collector/overlay-*.yaml merged over the base config; `none` is the default no-op, `langfuse` adds an exporter
|
|
62
|
+
warehouse/schema.sql skill_invocation, session_cost, ticket, pull_request, policy_result, defect, skill_eval
|
|
63
|
+
warehouse/scorecard.sql weekly per-skill scorecard → keep / refine / review-merge / deprecate
|
|
64
|
+
warehouse/load_traces.py Tempo → Postgres loader
|
|
65
|
+
eval/run_eval.py offline with/without-skill eval, real OPA grading (`--dry-run` is a smoke test)
|
|
66
|
+
eval/power.py generates every table in docs/evaluation-power.md; CI checks it is current
|
|
67
|
+
policies/ Rego behind the primary metric: logging.*, telemetry.manifest_valid
|
|
68
|
+
agents/ skill-scorecard-analyst: keep / refine / merge / deprecate from the data
|
|
69
|
+
warehouse/load_delivery.py GitHub → ticket / pull_request / defect; policy_result from a CI artefact
|
|
70
|
+
examples/settings.*.json hook + OTel wiring: `global` installs once, `project` overrides per repo
|
|
71
|
+
docs/adrs/ six ADRs: distribution, delivery data, hook constraints, identity, integrity, Langfuse
|
|
72
|
+
plugin.json Agent Plugins v1 manifest (portable `skills/` is the shared half)
|
|
73
|
+
.claude-plugin/ Claude Code plugin + marketplace manifest
|
|
74
|
+
hooks/, com.github.copilot/ per-harness hook manifests, generated from stdtel/install.py::EVENTS
|
|
75
|
+
stdtel/install.py `stdtel-install`: absolute-path resolution + settings merge
|
|
76
|
+
mise.toml toolchain (Python 3.13) + `.venv` + tasks wrapping the Makefile
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Quick start
|
|
80
|
+
|
|
81
|
+
Two separate jobs: **install the harness wiring once for your user**, then **onboard each project** you
|
|
82
|
+
want attributed telemetry from. Skipping the second step still gives you spans — the skills just come
|
|
83
|
+
through as `unversioned`, with `std.team=unknown`.
|
|
84
|
+
|
|
85
|
+
### 1. Install once, globally
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
uv tool install stdtel # or: pipx install stdtel
|
|
89
|
+
stdtel-install settings # merges hooks into ~/.claude/settings.json
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
> **Not published yet.** `stdtel` is not on PyPI until the first release
|
|
93
|
+
> ([docs/releasing.md](docs/releasing.md)). Until then install from a checkout:
|
|
94
|
+
> `uv tool install /path/to/skills-telemetry`. This matters more than it looks: the plugin's hooks
|
|
95
|
+
> call a launcher that exits silently when it cannot find the CLI, so an uninstalled package produces
|
|
96
|
+
> **no data and no error**. `stdtel-doctor` says so explicitly.
|
|
97
|
+
|
|
98
|
+
`stdtel-install` resolves the **absolute path** of the `stdtel-hook` it was installed alongside and
|
|
99
|
+
writes that into the config. This is not cosmetic: hook processes get a non-login `sh -c` and inherit
|
|
100
|
+
whatever PATH launched the harness, so a bare `stdtel-hook` is unresolvable whenever a version manager
|
|
101
|
+
(mise, asdf, pyenv) or an activated venv is what put it there. The same reason pre-commit bakes
|
|
102
|
+
`sys.executable` into the git hook it generates. `stdtel-install where` prints the path it will use;
|
|
103
|
+
`--dry-run` shows the JSON without writing.
|
|
104
|
+
|
|
105
|
+
Claude Code also **strips `OTEL_*` from every subprocess it spawns**, so point the exporter at your
|
|
106
|
+
collector with the `STDTEL_`-namespaced variables, which survive:
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
export STDTEL_OTLP_ENDPOINT=http://collector.internal:4318 # default: http://localhost:4318
|
|
110
|
+
export STDTEL_OTLP_TIMEOUT=2 # seconds; bounds a dead-collector stall
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
With no `STDTEL_SKILLS_ROOT` set, the catalogue is read from `~/.claude/skills`. Symlink your
|
|
114
|
+
standards there and every project gets versioned spans:
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
ln -s "$PWD/skills/structured-logging" ~/.claude/skills/structured-logging
|
|
118
|
+
stdtel-validate ~/.claude/skills # same contract gate CI runs
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
#### As a plugin
|
|
122
|
+
|
|
123
|
+
The repo is laid out for three loaders at once (ADR-001), so it installs as a plugin without a
|
|
124
|
+
separate packaging step:
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
/plugin marketplace add amiable-dev/skills-telemetry
|
|
128
|
+
/plugin install stdtel@amiable-standards
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
The shipped `hooks/hooks.json` carries the bare command name, because a distributed manifest cannot
|
|
132
|
+
know your install path — run `stdtel-install settings` afterwards to bind it to an absolute one.
|
|
133
|
+
|
|
134
|
+
#### Copilot
|
|
135
|
+
|
|
136
|
+
Copilot needs **no code from us**. It emits first-party OpenTelemetry with per-tool-call spans and
|
|
137
|
+
token counts; point it at the same collector (user settings, `COPILOT_OTEL_*` env vars, or the
|
|
138
|
+
enterprise `managed-settings.json` `telemetry` block for a fleet).
|
|
139
|
+
|
|
140
|
+
If you do run our hooks on Copilot as well, install them from `com.github.copilot/hooks/hooks.json`,
|
|
141
|
+
which sets `STDTEL_HARNESS` per hook. That is load-bearing: VS Code Copilot **reads
|
|
142
|
+
`~/.claude/settings.json`**, and its snake_case payload dialect is indistinguishable from Claude
|
|
143
|
+
Code's — without that env block, Copilot activity is recorded as `claude-code`.
|
|
144
|
+
|
|
145
|
+
### 2. Onboard a project
|
|
146
|
+
|
|
147
|
+
Per-project overrides go in `<project>/.claude/settings.json` — Claude Code merges them over the user
|
|
148
|
+
file, so repeat only what differs. Do **not** repeat the `hooks` block: it is already registered globally
|
|
149
|
+
and a second copy fires each hook twice.
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
cd ~/projects/payments-api
|
|
153
|
+
mkdir -p .claude && cp ~/projects/skills-telemetry/examples/settings.project.json .claude/settings.json
|
|
154
|
+
$EDITOR .claude/settings.json # STDTEL_TEAM is the one you must set
|
|
155
|
+
git checkout -b feature/PLAT-123-add-audit-log # ticket prefix -> std.ticket.id join key
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
| variable | where | meaning |
|
|
159
|
+
|---|---|---|
|
|
160
|
+
| `STDTEL_TEAM` | project | owning team on every span; `unknown` until you set it |
|
|
161
|
+
| `STDTEL_HARNESS_MODE` | project | `agent` / `interactive` — keeps the Claude Code vs Copilot split fair |
|
|
162
|
+
| `STDTEL_SKILLS_ROOT` | project | extra catalogue root(s), `os.pathsep`-separated. A relative path resolves against the project directory; `~/.claude/skills` is always searched last, and the earliest root wins a name collision |
|
|
163
|
+
| `STDTEL_HARNESS`, `OTEL_*` | global | harness label and collector endpoint |
|
|
164
|
+
|
|
165
|
+
Then verify the loop end to end:
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
claude # invoke a skill in the project
|
|
169
|
+
cat ~/.stdtel/sessions/*.json # a window with skill, version, tool_use_id
|
|
170
|
+
curl -s 'http://localhost:3200/api/search?tags=name%3Dstd.skill.invocation' | jq '.traces[0]'
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
A `std.skill.version` of `unversioned` means the name in the transcript matched no `SKILL.md` in any root
|
|
174
|
+
— check `STDTEL_SKILLS_ROOT` and that the skill's front-matter `name` matches what you invoked.
|
|
175
|
+
|
|
176
|
+
Copilot: enable managed OTel export (VS Code / CLI) pointing at the same collector with resource attributes
|
|
177
|
+
`std.harness=copilot-vscode`, `std.team=<team>`; `collector/otel-collector.yaml` maps catalogued skill
|
|
178
|
+
tool-calls onto `std.skill.*`.
|
|
179
|
+
|
|
180
|
+
## Span schema
|
|
181
|
+
|
|
182
|
+
Two span types, emitted at `Stop`. **They overlap by design and must never be summed:**
|
|
183
|
+
`std.session.cost` is the session's total spend, `std.skill.invocation` attributes a *share* of that
|
|
184
|
+
total to one skill. Use session cost as the denominator for cost-per-PR; use invocation tail tokens to
|
|
185
|
+
compare skills with each other.
|
|
186
|
+
|
|
187
|
+
### `std.session.cost`
|
|
188
|
+
|
|
189
|
+
Emitted once per turn that made any LLM request, **whether or not a skill was loaded** — a session that
|
|
190
|
+
never loads a skill is still spend, and excluding it would silently understate cost per PR.
|
|
191
|
+
|
|
192
|
+
| attribute | source |
|
|
193
|
+
|---|---|
|
|
194
|
+
| `gen_ai.usage.{input,output,cache_read_input,cache_creation_input}_tokens` | whole transcript slice |
|
|
195
|
+
| `std.session.llm_requests`, `gen_ai.request.model` | transcript |
|
|
196
|
+
| `std.session.tool_calls`, `std.session.tool_failures` | every tool call in the turn |
|
|
197
|
+
| `std.session.tool.<name>.{calls,failures}` | per tool; `failures` omitted when zero |
|
|
198
|
+
| `std.ticket.id`, `std.repo`, `std.team`, `std.harness` | resource (SessionStart) |
|
|
199
|
+
|
|
200
|
+
### `std.skill.invocation`
|
|
201
|
+
|
|
202
|
+
| attribute | source |
|
|
203
|
+
|---|---|
|
|
204
|
+
| `std.skill.name/version/trigger`, `std.standard_id`, `std.policy.ids` | hook + manifest |
|
|
205
|
+
| `std.skill.invoked_as`, `std.skill.plugin` | raw invocation string (plugin skills are namespaced) |
|
|
206
|
+
| `std.skill.load_tokens`, `std.skill.tail_tokens`, `std.skill.tail_tokens_first_only`, `std.skill.llm_requests` | transcript attribution |
|
|
207
|
+
| `gen_ai.usage.{input,output,cache_read_input,cache_creation_input}_tokens`, `gen_ai.request.model` | transcript |
|
|
208
|
+
| `std.ticket.id`, `std.repo`, `std.team`, `std.harness`, `std.harness.mode` | resource (SessionStart) |
|
|
209
|
+
| `std.user.hash` | collector (pseudonymised) |
|
|
210
|
+
|
|
211
|
+
## Known limitations
|
|
212
|
+
|
|
213
|
+
- Tool counts are counts only. `PostToolUse` fires for **every** tool, so `tool_input` and
|
|
214
|
+
`tool_response` carry commands, file contents and diffs — `scrub()` refuses those attribute names
|
|
215
|
+
outright and a test asserts nothing leaks.
|
|
216
|
+
- Skill name is parsed from the Skill tool input in hooks (the field is `skill`, verified against 120 real invocations). Parsing is isolated in `hooks/cli.py::_skill_from_payload`.
|
|
217
|
+
- `std.skill.trigger` reports the transcript's `caller.type` where present, else `unknown` — it is never guessed.
|
|
218
|
+
- Tail attribution splits by load order; when several skills load in one turn compare against `tail_tokens_first_only`.
|
|
219
|
+
- Copilot granularity is per turn; use Claude Code's finer data for within-harness tuning only.
|
|
220
|
+
- `load_tokens` uses a chars/4 heuristic on the Skill tool result.
|
|
221
|
+
- `gen_ai.*` conventions are still *Development* upstream; extend `transform/normalise` as names move.
|
|
File without changes
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Minimal FastAPI app. The eval task asks for request logging to be added."""
|
|
2
|
+
from fastapi import FastAPI
|
|
3
|
+
|
|
4
|
+
app = FastAPI()
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@app.get("/health")
|
|
8
|
+
def health():
|
|
9
|
+
return {"status": "ok"}
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@app.post("/orders")
|
|
13
|
+
def create_order(order: dict):
|
|
14
|
+
return {"id": 1, "total": order.get("total", 0)}
|