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.
Files changed (53) hide show
  1. stdtel-0.2.3/PKG-INFO +246 -0
  2. stdtel-0.2.3/README.md +221 -0
  3. stdtel-0.2.3/eval/__init__.py +0 -0
  4. stdtel-0.2.3/eval/fixtures/fastapi-min/app/main.py +14 -0
  5. stdtel-0.2.3/eval/power.py +199 -0
  6. stdtel-0.2.3/eval/run_eval.py +139 -0
  7. stdtel-0.2.3/pyproject.toml +49 -0
  8. stdtel-0.2.3/setup.cfg +4 -0
  9. stdtel-0.2.3/stdtel/__init__.py +2 -0
  10. stdtel-0.2.3/stdtel/doctor.py +211 -0
  11. stdtel-0.2.3/stdtel/enrich.py +59 -0
  12. stdtel-0.2.3/stdtel/exporter.py +113 -0
  13. stdtel-0.2.3/stdtel/hooks/__init__.py +0 -0
  14. stdtel-0.2.3/stdtel/hooks/cli.py +318 -0
  15. stdtel-0.2.3/stdtel/install.py +173 -0
  16. stdtel-0.2.3/stdtel/manifest.py +217 -0
  17. stdtel-0.2.3/stdtel/policy_report.py +126 -0
  18. stdtel-0.2.3/stdtel/skillmap.py +15 -0
  19. stdtel-0.2.3/stdtel/spool.py +124 -0
  20. stdtel-0.2.3/stdtel/spool_export.py +86 -0
  21. stdtel-0.2.3/stdtel/state.py +118 -0
  22. stdtel-0.2.3/stdtel/statusline.py +101 -0
  23. stdtel-0.2.3/stdtel/transcript.py +174 -0
  24. stdtel-0.2.3/stdtel.egg-info/PKG-INFO +246 -0
  25. stdtel-0.2.3/stdtel.egg-info/SOURCES.txt +51 -0
  26. stdtel-0.2.3/stdtel.egg-info/dependency_links.txt +1 -0
  27. stdtel-0.2.3/stdtel.egg-info/entry_points.txt +9 -0
  28. stdtel-0.2.3/stdtel.egg-info/requires.txt +10 -0
  29. stdtel-0.2.3/stdtel.egg-info/top_level.txt +2 -0
  30. stdtel-0.2.3/tests/test_adrs.py +69 -0
  31. stdtel-0.2.3/tests/test_dashboards.py +124 -0
  32. stdtel-0.2.3/tests/test_demo_seed.py +110 -0
  33. stdtel-0.2.3/tests/test_distribution.py +276 -0
  34. stdtel-0.2.3/tests/test_doc_links.py +25 -0
  35. stdtel-0.2.3/tests/test_docs_coverage.py +115 -0
  36. stdtel-0.2.3/tests/test_doctor.py +175 -0
  37. stdtel-0.2.3/tests/test_eval_and_map.py +14 -0
  38. stdtel-0.2.3/tests/test_hooks_e2e.py +47 -0
  39. stdtel-0.2.3/tests/test_live_payloads.py +69 -0
  40. stdtel-0.2.3/tests/test_load_delivery.py +140 -0
  41. stdtel-0.2.3/tests/test_manifest.py +112 -0
  42. stdtel-0.2.3/tests/test_policies.py +158 -0
  43. stdtel-0.2.3/tests/test_policy_report.py +156 -0
  44. stdtel-0.2.3/tests/test_power.py +143 -0
  45. stdtel-0.2.3/tests/test_publish_workflow.py +64 -0
  46. stdtel-0.2.3/tests/test_regressions.py +332 -0
  47. stdtel-0.2.3/tests/test_scorecard.py +107 -0
  48. stdtel-0.2.3/tests/test_skills_roots.py +72 -0
  49. stdtel-0.2.3/tests/test_spool.py +185 -0
  50. stdtel-0.2.3/tests/test_statusline.py +141 -0
  51. stdtel-0.2.3/tests/test_transcript.py +41 -0
  52. stdtel-0.2.3/tests/test_walkthroughs.py +75 -0
  53. 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)}