convertible-cli 0.6.0__tar.gz → 0.7.0__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 (136) hide show
  1. {convertible_cli-0.6.0 → convertible_cli-0.7.0}/.markdownlint-cli2.yaml +3 -0
  2. {convertible_cli-0.6.0 → convertible_cli-0.7.0}/CHANGELOG.md +13 -0
  3. {convertible_cli-0.6.0 → convertible_cli-0.7.0}/CLAUDE.md +29 -3
  4. convertible_cli-0.6.0/README.md → convertible_cli-0.7.0/PKG-INFO +62 -0
  5. convertible_cli-0.6.0/PKG-INFO → convertible_cli-0.7.0/README.md +40 -17
  6. {convertible_cli-0.6.0 → convertible_cli-0.7.0}/convertible/cli/__init__.py +3 -0
  7. {convertible_cli-0.6.0 → convertible_cli-0.7.0}/convertible/cli/_commands/drive.py +61 -30
  8. convertible_cli-0.7.0/convertible/cli/_commands/telemetry.py +115 -0
  9. {convertible_cli-0.6.0 → convertible_cli-0.7.0}/convertible/explain/catalog.py +53 -0
  10. {convertible_cli-0.6.0 → convertible_cli-0.7.0}/convertible/loop.py +77 -52
  11. convertible_cli-0.7.0/convertible/telemetry/__init__.py +237 -0
  12. convertible_cli-0.7.0/convertible/telemetry/_otel.py +325 -0
  13. convertible_cli-0.7.0/docs/plans/2026-05-28-convertible-gains-otel-gps-observability.md +73 -0
  14. convertible_cli-0.7.0/docs/specs/2026-05-28-convertible-gains-otel-gps-observability.md +54 -0
  15. {convertible_cli-0.6.0 → convertible_cli-0.7.0}/pyproject.toml +19 -1
  16. convertible_cli-0.7.0/tests/test_telemetry.py +270 -0
  17. convertible_cli-0.7.0/tests/test_telemetry_cli.py +88 -0
  18. {convertible_cli-0.6.0 → convertible_cli-0.7.0}/tests/test_zero_deps.py +9 -0
  19. {convertible_cli-0.6.0 → convertible_cli-0.7.0}/uv.lock +256 -1
  20. {convertible_cli-0.6.0 → convertible_cli-0.7.0}/.claude/skills/agent-config/SKILL.md +0 -0
  21. {convertible_cli-0.6.0 → convertible_cli-0.7.0}/.claude/skills/agent-config/data/backend-fingerprints.yaml +0 -0
  22. {convertible_cli-0.6.0 → convertible_cli-0.7.0}/.claude/skills/agent-config/scripts/show.sh +0 -0
  23. {convertible_cli-0.6.0 → convertible_cli-0.7.0}/.claude/skills/assign-to-workforce/SKILL.md +0 -0
  24. {convertible_cli-0.6.0 → convertible_cli-0.7.0}/.claude/skills/assign-to-workforce/scripts/assign-to-workforce.sh +0 -0
  25. {convertible_cli-0.6.0 → convertible_cli-0.7.0}/.claude/skills/cicd/SKILL.md +0 -0
  26. {convertible_cli-0.6.0 → convertible_cli-0.7.0}/.claude/skills/cicd/scripts/_resolve-nick.sh +0 -0
  27. {convertible_cli-0.6.0 → convertible_cli-0.7.0}/.claude/skills/cicd/scripts/portability-lint.sh +0 -0
  28. {convertible_cli-0.6.0 → convertible_cli-0.7.0}/.claude/skills/cicd/scripts/pr-reply.sh +0 -0
  29. {convertible_cli-0.6.0 → convertible_cli-0.7.0}/.claude/skills/cicd/scripts/pr-status.sh +0 -0
  30. {convertible_cli-0.6.0 → convertible_cli-0.7.0}/.claude/skills/cicd/scripts/workflow.sh +0 -0
  31. {convertible_cli-0.6.0 → convertible_cli-0.7.0}/.claude/skills/communicate/SKILL.md +0 -0
  32. {convertible_cli-0.6.0 → convertible_cli-0.7.0}/.claude/skills/communicate/scripts/fetch-issues.sh +0 -0
  33. {convertible_cli-0.6.0 → convertible_cli-0.7.0}/.claude/skills/communicate/scripts/mesh-message.sh +0 -0
  34. {convertible_cli-0.6.0 → convertible_cli-0.7.0}/.claude/skills/communicate/scripts/post-comment.sh +0 -0
  35. {convertible_cli-0.6.0 → convertible_cli-0.7.0}/.claude/skills/communicate/scripts/post-issue.sh +0 -0
  36. {convertible_cli-0.6.0 → convertible_cli-0.7.0}/.claude/skills/communicate/scripts/templates/skill-new-brief.md +0 -0
  37. {convertible_cli-0.6.0 → convertible_cli-0.7.0}/.claude/skills/communicate/scripts/templates/skill-update-brief.md +0 -0
  38. {convertible_cli-0.6.0 → convertible_cli-0.7.0}/.claude/skills/doc-test-alignment/SKILL.md +0 -0
  39. {convertible_cli-0.6.0 → convertible_cli-0.7.0}/.claude/skills/doc-test-alignment/scripts/check.sh +0 -0
  40. {convertible_cli-0.6.0 → convertible_cli-0.7.0}/.claude/skills/pypi-maintainer/SKILL.md +0 -0
  41. {convertible_cli-0.6.0 → convertible_cli-0.7.0}/.claude/skills/pypi-maintainer/scripts/switch-source.sh +0 -0
  42. {convertible_cli-0.6.0 → convertible_cli-0.7.0}/.claude/skills/run-tests/SKILL.md +0 -0
  43. {convertible_cli-0.6.0 → convertible_cli-0.7.0}/.claude/skills/run-tests/scripts/test.sh +0 -0
  44. {convertible_cli-0.6.0 → convertible_cli-0.7.0}/.claude/skills/sonarclaude/SKILL.md +0 -0
  45. {convertible_cli-0.6.0 → convertible_cli-0.7.0}/.claude/skills/sonarclaude/scripts/sonar.sh +0 -0
  46. {convertible_cli-0.6.0 → convertible_cli-0.7.0}/.claude/skills/spec-to-plan/SKILL.md +0 -0
  47. {convertible_cli-0.6.0 → convertible_cli-0.7.0}/.claude/skills/spec-to-plan/scripts/spec-to-plan.sh +0 -0
  48. {convertible_cli-0.6.0 → convertible_cli-0.7.0}/.claude/skills/think/SKILL.md +0 -0
  49. {convertible_cli-0.6.0 → convertible_cli-0.7.0}/.claude/skills/think/scripts/think.sh +0 -0
  50. {convertible_cli-0.6.0 → convertible_cli-0.7.0}/.claude/skills/version-bump/SKILL.md +0 -0
  51. {convertible_cli-0.6.0 → convertible_cli-0.7.0}/.claude/skills/version-bump/scripts/bump.py +0 -0
  52. {convertible_cli-0.6.0 → convertible_cli-0.7.0}/.claude/skills.local.yaml.example +0 -0
  53. {convertible_cli-0.6.0 → convertible_cli-0.7.0}/.devague/current +0 -0
  54. {convertible_cli-0.6.0 → convertible_cli-0.7.0}/.devague/current_plan +0 -0
  55. {convertible_cli-0.6.0 → convertible_cli-0.7.0}/.devague/frames/convertible-gains-an-extensibility-layer-like-clau.json +0 -0
  56. {convertible_cli-0.6.0 → convertible_cli-0.7.0}/.devague/frames/convertible-v0-ships-point-it-at-a-repo-task-and-i.json +0 -0
  57. {convertible_cli-0.6.0 → convertible_cli-0.7.0}/.devague/plans/convertible-gains-an-extensibility-layer-like-clau.json +0 -0
  58. {convertible_cli-0.6.0 → convertible_cli-0.7.0}/.devague/plans/convertible-v0-ships-point-it-at-a-repo-task-and-i.json +0 -0
  59. {convertible_cli-0.6.0 → convertible_cli-0.7.0}/.flake8 +0 -0
  60. {convertible_cli-0.6.0 → convertible_cli-0.7.0}/.github/workflows/publish.yml +0 -0
  61. {convertible_cli-0.6.0 → convertible_cli-0.7.0}/.github/workflows/tests.yml +0 -0
  62. {convertible_cli-0.6.0 → convertible_cli-0.7.0}/.gitignore +0 -0
  63. {convertible_cli-0.6.0 → convertible_cli-0.7.0}/LICENSE +0 -0
  64. {convertible_cli-0.6.0 → convertible_cli-0.7.0}/convertible/__init__.py +0 -0
  65. {convertible_cli-0.6.0 → convertible_cli-0.7.0}/convertible/__main__.py +0 -0
  66. {convertible_cli-0.6.0 → convertible_cli-0.7.0}/convertible/artifact.py +0 -0
  67. {convertible_cli-0.6.0 → convertible_cli-0.7.0}/convertible/cli/_banner-big.txt +0 -0
  68. {convertible_cli-0.6.0 → convertible_cli-0.7.0}/convertible/cli/_banner.py +0 -0
  69. {convertible_cli-0.6.0 → convertible_cli-0.7.0}/convertible/cli/_banner.txt +0 -0
  70. {convertible_cli-0.6.0 → convertible_cli-0.7.0}/convertible/cli/_commands/__init__.py +0 -0
  71. {convertible_cli-0.6.0 → convertible_cli-0.7.0}/convertible/cli/_commands/agents.py +0 -0
  72. {convertible_cli-0.6.0 → convertible_cli-0.7.0}/convertible/cli/_commands/cli.py +0 -0
  73. {convertible_cli-0.6.0 → convertible_cli-0.7.0}/convertible/cli/_commands/commands.py +0 -0
  74. {convertible_cli-0.6.0 → convertible_cli-0.7.0}/convertible/cli/_commands/doctor.py +0 -0
  75. {convertible_cli-0.6.0 → convertible_cli-0.7.0}/convertible/cli/_commands/explain.py +0 -0
  76. {convertible_cli-0.6.0 → convertible_cli-0.7.0}/convertible/cli/_commands/hooks.py +0 -0
  77. {convertible_cli-0.6.0 → convertible_cli-0.7.0}/convertible/cli/_commands/learn.py +0 -0
  78. {convertible_cli-0.6.0 → convertible_cli-0.7.0}/convertible/cli/_commands/overview.py +0 -0
  79. {convertible_cli-0.6.0 → convertible_cli-0.7.0}/convertible/cli/_commands/session.py +0 -0
  80. {convertible_cli-0.6.0 → convertible_cli-0.7.0}/convertible/cli/_commands/skills.py +0 -0
  81. {convertible_cli-0.6.0 → convertible_cli-0.7.0}/convertible/cli/_commands/wheels.py +0 -0
  82. {convertible_cli-0.6.0 → convertible_cli-0.7.0}/convertible/cli/_commands/whoami.py +0 -0
  83. {convertible_cli-0.6.0 → convertible_cli-0.7.0}/convertible/cli/_errors.py +0 -0
  84. {convertible_cli-0.6.0 → convertible_cli-0.7.0}/convertible/cli/_output.py +0 -0
  85. {convertible_cli-0.6.0 → convertible_cli-0.7.0}/convertible/commands.py +0 -0
  86. {convertible_cli-0.6.0 → convertible_cli-0.7.0}/convertible/config.py +0 -0
  87. {convertible_cli-0.6.0 → convertible_cli-0.7.0}/convertible/configdir.py +0 -0
  88. {convertible_cli-0.6.0 → convertible_cli-0.7.0}/convertible/contract.py +0 -0
  89. {convertible_cli-0.6.0 → convertible_cli-0.7.0}/convertible/engine.py +0 -0
  90. {convertible_cli-0.6.0 → convertible_cli-0.7.0}/convertible/engines/__init__.py +0 -0
  91. {convertible_cli-0.6.0 → convertible_cli-0.7.0}/convertible/engines/mock.py +0 -0
  92. {convertible_cli-0.6.0 → convertible_cli-0.7.0}/convertible/engines/vllm_openai.py +0 -0
  93. {convertible_cli-0.6.0 → convertible_cli-0.7.0}/convertible/explain/__init__.py +0 -0
  94. {convertible_cli-0.6.0 → convertible_cli-0.7.0}/convertible/handoff.py +0 -0
  95. {convertible_cli-0.6.0 → convertible_cli-0.7.0}/convertible/hooks.py +0 -0
  96. {convertible_cli-0.6.0 → convertible_cli-0.7.0}/convertible/layers.py +0 -0
  97. {convertible_cli-0.6.0 → convertible_cli-0.7.0}/convertible/registry.py +0 -0
  98. {convertible_cli-0.6.0 → convertible_cli-0.7.0}/convertible/tools.py +0 -0
  99. {convertible_cli-0.6.0 → convertible_cli-0.7.0}/culture.yaml +0 -0
  100. {convertible_cli-0.6.0 → convertible_cli-0.7.0}/docs/plans/2026-05-26-convertible-v0-ships-point-it-at-a-repo-task-and-i.md +0 -0
  101. {convertible_cli-0.6.0 → convertible_cli-0.7.0}/docs/plans/2026-05-27-convertible-gains-an-extensibility-layer-like-clau.md +0 -0
  102. {convertible_cli-0.6.0 → convertible_cli-0.7.0}/docs/skill-sources.md +0 -0
  103. {convertible_cli-0.6.0 → convertible_cli-0.7.0}/docs/specs/2026-05-26-convertible-v0-ships-point-it-at-a-repo-task-and-i.md +0 -0
  104. {convertible_cli-0.6.0 → convertible_cli-0.7.0}/docs/specs/2026-05-27-convertible-gains-an-extensibility-layer-like-clau.md +0 -0
  105. {convertible_cli-0.6.0 → convertible_cli-0.7.0}/sonar-project.properties +0 -0
  106. {convertible_cli-0.6.0 → convertible_cli-0.7.0}/tests/__init__.py +0 -0
  107. {convertible_cli-0.6.0 → convertible_cli-0.7.0}/tests/test_agents_cli.py +0 -0
  108. {convertible_cli-0.6.0 → convertible_cli-0.7.0}/tests/test_artifact.py +0 -0
  109. {convertible_cli-0.6.0 → convertible_cli-0.7.0}/tests/test_banner.py +0 -0
  110. {convertible_cli-0.6.0 → convertible_cli-0.7.0}/tests/test_boundary.py +0 -0
  111. {convertible_cli-0.6.0 → convertible_cli-0.7.0}/tests/test_cli.py +0 -0
  112. {convertible_cli-0.6.0 → convertible_cli-0.7.0}/tests/test_cli_introspection.py +0 -0
  113. {convertible_cli-0.6.0 → convertible_cli-0.7.0}/tests/test_commands.py +0 -0
  114. {convertible_cli-0.6.0 → convertible_cli-0.7.0}/tests/test_commands_cli.py +0 -0
  115. {convertible_cli-0.6.0 → convertible_cli-0.7.0}/tests/test_config.py +0 -0
  116. {convertible_cli-0.6.0 → convertible_cli-0.7.0}/tests/test_configdir.py +0 -0
  117. {convertible_cli-0.6.0 → convertible_cli-0.7.0}/tests/test_contract.py +0 -0
  118. {convertible_cli-0.6.0 → convertible_cli-0.7.0}/tests/test_drive.py +0 -0
  119. {convertible_cli-0.6.0 → convertible_cli-0.7.0}/tests/test_e2e_extensibility.py +0 -0
  120. {convertible_cli-0.6.0 → convertible_cli-0.7.0}/tests/test_e2e_mock.py +0 -0
  121. {convertible_cli-0.6.0 → convertible_cli-0.7.0}/tests/test_engine.py +0 -0
  122. {convertible_cli-0.6.0 → convertible_cli-0.7.0}/tests/test_handoff.py +0 -0
  123. {convertible_cli-0.6.0 → convertible_cli-0.7.0}/tests/test_hooks.py +0 -0
  124. {convertible_cli-0.6.0 → convertible_cli-0.7.0}/tests/test_hooks_cli.py +0 -0
  125. {convertible_cli-0.6.0 → convertible_cli-0.7.0}/tests/test_layers.py +0 -0
  126. {convertible_cli-0.6.0 → convertible_cli-0.7.0}/tests/test_layers_engine_parity.py +0 -0
  127. {convertible_cli-0.6.0 → convertible_cli-0.7.0}/tests/test_loop.py +0 -0
  128. {convertible_cli-0.6.0 → convertible_cli-0.7.0}/tests/test_mock_engine.py +0 -0
  129. {convertible_cli-0.6.0 → convertible_cli-0.7.0}/tests/test_registry.py +0 -0
  130. {convertible_cli-0.6.0 → convertible_cli-0.7.0}/tests/test_review_fixes.py +0 -0
  131. {convertible_cli-0.6.0 → convertible_cli-0.7.0}/tests/test_session.py +0 -0
  132. {convertible_cli-0.6.0 → convertible_cli-0.7.0}/tests/test_skills_cli.py +0 -0
  133. {convertible_cli-0.6.0 → convertible_cli-0.7.0}/tests/test_tools.py +0 -0
  134. {convertible_cli-0.6.0 → convertible_cli-0.7.0}/tests/test_vllm_live.py +0 -0
  135. {convertible_cli-0.6.0 → convertible_cli-0.7.0}/tests/test_vllm_openai.py +0 -0
  136. {convertible_cli-0.6.0 → convertible_cli-0.7.0}/tests/test_wheels.py +0 -0
@@ -19,6 +19,9 @@ ignores:
19
19
  - ".local/**"
20
20
  - ".afi/**"
21
21
  - ".teken/**"
22
+ # The virtualenv — installed packages (e.g. the [otel] extra's opentelemetry,
23
+ # idna) ship their own non-conformant Markdown; never lint dependencies.
24
+ - ".venv/**"
22
25
  # Vendored skills are cited verbatim from guildmaster — do not reformat them.
23
26
  - ".claude/skills/**"
24
27
  # devague artifacts (frames, exported specs/plans) are generated verbatim from
@@ -5,6 +5,19 @@ All notable changes to this project will be documented in this file.
5
5
  Format follows [Keep a Changelog](https://keepachangelog.com/). This project
6
6
  adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.7.0] - 2026-05-28
9
+
10
+ ### Added
11
+
12
+ - GPS: opt-in OpenTelemetry traces + metrics for a drive (issue #22). Spans (`convertible.drive` -> `convertible.tool.*` -> `convertible.handoff`) and metrics (steps, tokens, tool latency, tool calls, hook denials, drive duration) emit over OTLP from the loop + the shared drive path, so every engine is instrumented identically (all-engines rule).
13
+ - `convertible telemetry status` / `overview` introspection noun, plus an explain catalog entry.
14
+ - `TelemetryConfig` resolved from `CONVERTIBLE_OTEL_*` / standard `OTEL_*` env vars (`OTEL_SDK_DISABLED` honored as a kill-switch).
15
+ - Optional `[otel]` extra (opentelemetry SDK + OTLP/HTTP exporter); install with `pip install "convertible-cli[otel]"`.
16
+
17
+ ### Changed
18
+
19
+ - `loop.run()` and `execute_drive` accept/own telemetry, defaulting to a no-op resolved from the environment (mirrors the hooks pattern). Off by default it is a strict no-op: no spans, no SDK import, `TaskResult` unchanged.
20
+
8
21
  ## [0.6.0] - 2026-05-28
9
22
 
10
23
  ### Added
@@ -21,6 +21,11 @@ The car metaphor *is* the architecture:
21
21
  - **Wheels** — engines are plugins discovered via the `convertible.engines`
22
22
  Python entry-point group (`convertible/registry.py`).
23
23
  - **Dashboard** — the JSON result artifact + step trace (`convertible/artifact.py`).
24
+ - **GPS** — opt-in OpenTelemetry traces + metrics (`convertible/telemetry/`).
25
+ Instrumented in the loop + the shared drive path so every engine emits it
26
+ (all-engines rule), exactly like hooks. Off by default; the OpenTelemetry SDK
27
+ is an optional `[otel]` extra, imported lazily, so the base install stays
28
+ dep-free. Surfaced via the `telemetry` introspection noun.
24
29
  - **Handoff** — branch/commit/push + `gh pr create`, gated for offline/CI
25
30
  (`convertible/handoff.py`).
26
31
  - **Command templates** — named, parameterized task recipes in
@@ -53,8 +58,9 @@ The buildable spec and plan this implementation converged from live in
53
58
 
54
59
  In scope: the chassis, the entry-point wheel contract, exactly two engines
55
60
  (`mock`, `vllm-openai`), the git/PR handoff, command templates, lifecycle
56
- hooks, the foreground interactive palette, and layered per-model AGENTS/skills
57
- config (`convertible/layers.py`).
61
+ hooks, the foreground interactive palette, layered per-model AGENTS/skills
62
+ config (`convertible/layers.py`), and GPS — opt-in OpenTelemetry traces +
63
+ metrics (`convertible/telemetry/`), with the SDK as an optional `[otel]` extra.
58
64
 
59
65
  **Out of scope for v0** — do not add without re-speccing: a multi-engine
60
66
  router/policy "gearbox", an execution sandbox, a daemon/server mode,
@@ -81,7 +87,15 @@ test (`tests/test_e2e_mock.py`) is the guard.
81
87
  - **No runtime dependencies.** `pyproject.toml` keeps `dependencies = []`; the
82
88
  vLLM driver speaks the OpenAI wire format over stdlib `urllib`; commands and
83
89
  hooks use only stdlib (`json`, `subprocess`, `pathlib`). Don't add a runtime
84
- dep without a strong reason — dev-only deps go in the `dev` group.
90
+ dep without a strong reason — dev-only deps go in the `dev` group. The one
91
+ documented exception is **GPS**: the OpenTelemetry SDK ships as an optional
92
+ `[project.optional-dependencies] otel` extra, never a base dependency. It is
93
+ imported **lazily** inside `convertible/telemetry/_otel.py` (only when
94
+ telemetry is enabled), so `dependencies = []` and the zero-deps guard
95
+ (`tests/test_zero_deps.py`) still hold — the guard imports `convertible.loop`
96
+ / `convertible.telemetry` / `convertible.cli` and asserts no third-party leak
97
+ even with the extra installed. Keep the SDK confined to `_otel.py`; never
98
+ import `opentelemetry` from any other convertible module.
85
99
  - **Agent-first CLI.** New verbs are `convertible/cli/_commands/` modules with a
86
100
  `register(sub)`, wired in `convertible/cli/__init__.py`. Results to stdout,
87
101
  diagnostics/errors to stderr (never mixed); every command supports `--json`;
@@ -98,6 +112,11 @@ test (`tests/test_e2e_mock.py`) is the guard.
98
112
  hook firing — new engine wheels inherit the full lifecycle layer automatically
99
113
  and must not duplicate it. The all-engines rule applies: a hook config that
100
114
  fires on `mock` must fire identically on `vllm-openai`.
115
+ - **Telemetry belongs to the chassis too.** `convertible/loop.py` (per tool
116
+ call) and the shared `execute_drive` path (root + handoff spans) own all
117
+ telemetry; no engine module touches the `telemetry` package. Off by default it
118
+ is a strict no-op (no spans, no SDK import, `TaskResult` unchanged) — protect
119
+ that so the e2e shape test and zero-deps guard keep passing.
101
120
  - **Repo-shipped hooks run by default (trusted-operator-env model D2).** There
102
121
  is no `--no-hooks` flag today. A per-repo trust gate is a tracked follow-up.
103
122
  Document this gap clearly; never document a non-existent flag.
@@ -118,6 +137,13 @@ uv run convertible hooks list --repo . # list configured hooks
118
137
  uv run convertible hooks overview # surface description
119
138
  uv run convertible session --repo . --engine mock # interactive palette
120
139
 
140
+ # GPS / telemetry (opt-in; needs the [otel] extra):
141
+ uv run convertible telemetry status # resolved telemetry config
142
+ uv run convertible telemetry overview # surface description
143
+ uv sync --extra otel # install the OpenTelemetry SDK
144
+ CONVERTIBLE_OTEL_ENABLED=1 OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318 \
145
+ uv run convertible drive "<task>" --repo . --engine mock --no-pr # emits a trace
146
+
121
147
  # Lint + gates CI enforces:
122
148
  uv run black --check convertible tests
123
149
  uv run isort --check-only convertible tests
@@ -1,3 +1,25 @@
1
+ Metadata-Version: 2.4
2
+ Name: convertible-cli
3
+ Version: 0.7.0
4
+ Summary: Convertible CLI is a swappable coder-agent harness that turns different models into repo workers behind one shared task contract.
5
+ Project-URL: Homepage, https://github.com/agentculture/convertible
6
+ Project-URL: Issues, https://github.com/agentculture/convertible/issues
7
+ Author: AgentCulture
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Topic :: Software Development
15
+ Requires-Python: >=3.12
16
+ Provides-Extra: otel
17
+ Requires-Dist: opentelemetry-api>=1.25; extra == 'otel'
18
+ Requires-Dist: opentelemetry-exporter-otlp-proto-http>=1.25; extra == 'otel'
19
+ Requires-Dist: opentelemetry-sdk>=1.25; extra == 'otel'
20
+ Requires-Dist: opentelemetry-semantic-conventions>=0.46b0; extra == 'otel'
21
+ Description-Content-Type: text/markdown
22
+
1
23
  # convertible
2
24
 
3
25
  ```text
@@ -42,6 +64,7 @@ which one ran.
42
64
  | **Tool-loop** | the bounded agentic loop the engine drives the repo through |
43
65
  | **Wheels** | replaceable engine plugins, discovered via Python entry points |
44
66
  | **Dashboard** | the JSON result artifact + step trace each run writes |
67
+ | **GPS** | opt-in OpenTelemetry traces + metrics (`convertible/telemetry/`) |
45
68
  | **Garage** | `convertible wheels list` — the engines installed in this env |
46
69
 
47
70
  ## What ships in v0
@@ -313,6 +336,43 @@ The session loops until the user enters `q`, `quit`, or an empty line. Any
313
336
  driver flags accepted by `drive` (`--engine`, `--no-pr`, `--base-url`, etc.)
314
337
  are also accepted by `session`.
315
338
 
339
+ ## GPS: OpenTelemetry observability
340
+
341
+ A drive can emit **OpenTelemetry traces + metrics** so it's observable against an
342
+ OTLP collector — not just the per-run JSON artifact. Telemetry lives in the
343
+ chassis (the loop + the shared drive path), so **every engine** emits it
344
+ identically, exactly like lifecycle hooks.
345
+
346
+ It is **off by default** and a strict no-op when off (no spans, no SDK import,
347
+ the result artifact unchanged). The OpenTelemetry SDK is an **optional extra** —
348
+ the base install keeps zero runtime dependencies:
349
+
350
+ ```bash
351
+ pip install 'convertible-cli[otel]' # or: uv sync --extra otel
352
+ export CONVERTIBLE_OTEL_ENABLED=1
353
+ export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318 # OTLP/HTTP collector
354
+ uv run convertible drive "<task>" --repo . --engine mock --no-pr
355
+ # -> stderr prints "trace: <id>"; the collector receives the spans + metrics
356
+ ```
357
+
358
+ Requested without the extra installed, convertible degrades to a no-op with a
359
+ one-line stderr notice — it never fails the drive.
360
+
361
+ **Signals.** Spans: `convertible.drive` (root) → `convertible.tool.*` (per tool
362
+ call) → `convertible.handoff`. Metrics: `convertible.steps`, `convertible.tokens`,
363
+ `convertible.tool.latency`, `convertible.tool.calls`, `convertible.hook.denials`,
364
+ `convertible.drive.duration`.
365
+
366
+ **Config** (precedence: explicit > `CONVERTIBLE_OTEL_*` > standard `OTEL_*` >
367
+ default): `CONVERTIBLE_OTEL_ENABLED`, `CONVERTIBLE_OTEL_ENDPOINT` /
368
+ `OTEL_EXPORTER_OTLP_ENDPOINT`, `CONVERTIBLE_OTEL_SERVICE_NAME` /
369
+ `OTEL_SERVICE_NAME`. `OTEL_SDK_DISABLED=true` is honored as a kill-switch.
370
+
371
+ ```bash
372
+ uv run convertible telemetry status # resolved config + whether the SDK is installed
373
+ uv run convertible telemetry overview # describe the surface
374
+ ```
375
+
316
376
  ## Per-model instructions & skills
317
377
 
318
378
  Convertible composes a model-specific **system prompt** for every drive from two
@@ -394,6 +454,8 @@ rely on a non-existent flag.
394
454
  | `agents overview` | Describe the agents surface. |
395
455
  | `skills list` | List resolved skill docs for a model. |
396
456
  | `skills overview` | Describe the skills surface. |
457
+ | `telemetry status` | Show the resolved GPS / OpenTelemetry config + whether the SDK is installed. |
458
+ | `telemetry overview` | Describe the telemetry surface. |
397
459
  | `session` | Open a foreground interactive palette. |
398
460
  | `wheels list` | List discovered engine wheels (the garage). |
399
461
  | `whoami` | Report this agent's nick, version, backend, and model. |
@@ -1,20 +1,3 @@
1
- Metadata-Version: 2.4
2
- Name: convertible-cli
3
- Version: 0.6.0
4
- Summary: Convertible CLI is a swappable coder-agent harness that turns different models into repo workers behind one shared task contract.
5
- Project-URL: Homepage, https://github.com/agentculture/convertible
6
- Project-URL: Issues, https://github.com/agentculture/convertible/issues
7
- Author: AgentCulture
8
- License-Expression: MIT
9
- License-File: LICENSE
10
- Classifier: Development Status :: 3 - Alpha
11
- Classifier: Intended Audience :: Developers
12
- Classifier: License :: OSI Approved :: MIT License
13
- Classifier: Programming Language :: Python :: 3.12
14
- Classifier: Topic :: Software Development
15
- Requires-Python: >=3.12
16
- Description-Content-Type: text/markdown
17
-
18
1
  # convertible
19
2
 
20
3
  ```text
@@ -59,6 +42,7 @@ which one ran.
59
42
  | **Tool-loop** | the bounded agentic loop the engine drives the repo through |
60
43
  | **Wheels** | replaceable engine plugins, discovered via Python entry points |
61
44
  | **Dashboard** | the JSON result artifact + step trace each run writes |
45
+ | **GPS** | opt-in OpenTelemetry traces + metrics (`convertible/telemetry/`) |
62
46
  | **Garage** | `convertible wheels list` — the engines installed in this env |
63
47
 
64
48
  ## What ships in v0
@@ -330,6 +314,43 @@ The session loops until the user enters `q`, `quit`, or an empty line. Any
330
314
  driver flags accepted by `drive` (`--engine`, `--no-pr`, `--base-url`, etc.)
331
315
  are also accepted by `session`.
332
316
 
317
+ ## GPS: OpenTelemetry observability
318
+
319
+ A drive can emit **OpenTelemetry traces + metrics** so it's observable against an
320
+ OTLP collector — not just the per-run JSON artifact. Telemetry lives in the
321
+ chassis (the loop + the shared drive path), so **every engine** emits it
322
+ identically, exactly like lifecycle hooks.
323
+
324
+ It is **off by default** and a strict no-op when off (no spans, no SDK import,
325
+ the result artifact unchanged). The OpenTelemetry SDK is an **optional extra** —
326
+ the base install keeps zero runtime dependencies:
327
+
328
+ ```bash
329
+ pip install 'convertible-cli[otel]' # or: uv sync --extra otel
330
+ export CONVERTIBLE_OTEL_ENABLED=1
331
+ export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318 # OTLP/HTTP collector
332
+ uv run convertible drive "<task>" --repo . --engine mock --no-pr
333
+ # -> stderr prints "trace: <id>"; the collector receives the spans + metrics
334
+ ```
335
+
336
+ Requested without the extra installed, convertible degrades to a no-op with a
337
+ one-line stderr notice — it never fails the drive.
338
+
339
+ **Signals.** Spans: `convertible.drive` (root) → `convertible.tool.*` (per tool
340
+ call) → `convertible.handoff`. Metrics: `convertible.steps`, `convertible.tokens`,
341
+ `convertible.tool.latency`, `convertible.tool.calls`, `convertible.hook.denials`,
342
+ `convertible.drive.duration`.
343
+
344
+ **Config** (precedence: explicit > `CONVERTIBLE_OTEL_*` > standard `OTEL_*` >
345
+ default): `CONVERTIBLE_OTEL_ENABLED`, `CONVERTIBLE_OTEL_ENDPOINT` /
346
+ `OTEL_EXPORTER_OTLP_ENDPOINT`, `CONVERTIBLE_OTEL_SERVICE_NAME` /
347
+ `OTEL_SERVICE_NAME`. `OTEL_SDK_DISABLED=true` is honored as a kill-switch.
348
+
349
+ ```bash
350
+ uv run convertible telemetry status # resolved config + whether the SDK is installed
351
+ uv run convertible telemetry overview # describe the surface
352
+ ```
353
+
333
354
  ## Per-model instructions & skills
334
355
 
335
356
  Convertible composes a model-specific **system prompt** for every drive from two
@@ -411,6 +432,8 @@ rely on a non-existent flag.
411
432
  | `agents overview` | Describe the agents surface. |
412
433
  | `skills list` | List resolved skill docs for a model. |
413
434
  | `skills overview` | Describe the skills surface. |
435
+ | `telemetry status` | Show the resolved GPS / OpenTelemetry config + whether the SDK is installed. |
436
+ | `telemetry overview` | Describe the telemetry surface. |
414
437
  | `session` | Open a foreground interactive palette. |
415
438
  | `wheels list` | List discovered engine wheels (the garage). |
416
439
  | `whoami` | Report this agent's nick, version, backend, and model. |
@@ -84,6 +84,7 @@ def _build_parser() -> argparse.ArgumentParser:
84
84
  from convertible.cli._commands import overview as _overview_cmd
85
85
  from convertible.cli._commands import session as _session_cmd
86
86
  from convertible.cli._commands import skills as _skills_group
87
+ from convertible.cli._commands import telemetry as _telemetry_group
87
88
  from convertible.cli._commands import wheels as _wheels_group
88
89
  from convertible.cli._commands import whoami as _whoami_cmd
89
90
 
@@ -115,6 +116,8 @@ def _build_parser() -> argparse.ArgumentParser:
115
116
  # Layered per-model config: AGENTS instructions + skills.
116
117
  _agents_group.register(sub)
117
118
  _skills_group.register(sub)
119
+ # GPS: OpenTelemetry traces + metrics (opt-in, optional [otel] extra).
120
+ _telemetry_group.register(sub)
118
121
  # Interactive foreground palette (c28/R8).
119
122
  _session_cmd.register(sub)
120
123
 
@@ -33,6 +33,7 @@ from convertible.commands import CommandError, expand_command
33
33
  from convertible.config import EngineConfig
34
34
  from convertible.contract import OK, Task, TaskResult
35
35
  from convertible.handoff import HandoffError, handoff
36
+ from convertible.telemetry import load_telemetry
36
37
 
37
38
 
38
39
  def _render(result: TaskResult, engine: str, artifact_path: Path) -> str:
@@ -103,39 +104,69 @@ def execute_drive(
103
104
  EXIT_USER_ERROR, str(exc), "list engines with: convertible wheels list"
104
105
  ) from exc
105
106
 
107
+ # GPS: the root span wraps engine.drive() + handoff() + the artifact write, so
108
+ # the loop's tool spans nest under it. A no-op unless telemetry is enabled.
109
+ # The same shared path serves `drive` and `session`, so both are instrumented.
110
+ telemetry = load_telemetry()
106
111
  try:
107
- result = engine.drive(task, config)
108
- except Exception as exc: # noqa: BLE001 - any failure still writes an artifact (h5)
109
- result = failed_result(task.id, f"{type(exc).__name__}: {exc}")
110
- result.command = command_name
111
- write(result, artifact_dir(repo))
112
- raise CliError(
113
- EXIT_ENV_ERROR,
114
- f"engine '{engine_name}' failed: {exc}",
115
- "check the engine config / vLLM server; a result artifact was still written",
116
- ) from exc
112
+ with telemetry.drive_span(
113
+ task_id=task.id,
114
+ engine=engine_name,
115
+ model=config.model,
116
+ max_steps=config.max_steps,
117
+ ) as drive_span:
118
+ trace_id = telemetry.trace_id_hex()
119
+ if trace_id:
120
+ emit_diagnostic(f"trace: {trace_id}")
117
121
 
118
- if result.status == OK:
119
- try:
120
- outcome = handoff(
121
- repo,
122
- task.id,
123
- instruction=task.instruction,
124
- open_pr=open_pr,
125
- base_branch=base,
122
+ try:
123
+ result = engine.drive(task, config)
124
+ except Exception as exc: # noqa: BLE001 - any failure still writes an artifact (h5)
125
+ result = failed_result(task.id, f"{type(exc).__name__}: {exc}")
126
+ result.command = command_name
127
+ drive_span.set(status=result.status)
128
+ write(result, artifact_dir(repo))
129
+ raise CliError(
130
+ EXIT_ENV_ERROR,
131
+ f"engine '{engine_name}' failed: {exc}",
132
+ "check the engine config / vLLM server; a result artifact was still written",
133
+ ) from exc
134
+
135
+ if result.status == OK:
136
+ with telemetry.handoff_span() as handoff_span:
137
+ try:
138
+ outcome = handoff(
139
+ repo,
140
+ task.id,
141
+ instruction=task.instruction,
142
+ open_pr=open_pr,
143
+ base_branch=base,
144
+ )
145
+ result.branch = outcome.branch
146
+ result.pr_url = outcome.pr_url
147
+ if not result.changed_files:
148
+ result.changed_files = outcome.changed_files
149
+ handoff_span.set(
150
+ branch=outcome.branch,
151
+ committed=outcome.committed,
152
+ pushed=outcome.pushed,
153
+ pr_url=outcome.pr_url,
154
+ )
155
+ if outcome.note:
156
+ emit_diagnostic(f"handoff: {outcome.note}")
157
+ except HandoffError as exc:
158
+ emit_diagnostic(f"handoff skipped: {exc}")
159
+
160
+ drive_span.set(
161
+ status=result.status,
162
+ step_count=len(result.steps),
163
+ pr_url=result.pr_url,
126
164
  )
127
- result.branch = outcome.branch
128
- result.pr_url = outcome.pr_url
129
- if not result.changed_files:
130
- result.changed_files = outcome.changed_files
131
- if outcome.note:
132
- emit_diagnostic(f"handoff: {outcome.note}")
133
- except HandoffError as exc:
134
- emit_diagnostic(f"handoff skipped: {exc}")
135
-
136
- result.command = command_name
137
- artifact_path = write(result, artifact_dir(repo))
138
- return result, artifact_path
165
+ result.command = command_name
166
+ artifact_path = write(result, artifact_dir(repo))
167
+ return result, artifact_path
168
+ finally:
169
+ telemetry.flush()
139
170
 
140
171
 
141
172
  def cmd_drive(args: argparse.Namespace) -> int:
@@ -0,0 +1,115 @@
1
+ """``convertible telemetry`` — inspect the GPS (OpenTelemetry) configuration.
2
+
3
+ ``telemetry status`` reports the resolved :class:`~convertible.telemetry.TelemetryConfig`
4
+ (enabled flag, OTLP endpoint/protocol, service name, traces/metrics toggles) and
5
+ whether the optional ``[otel]`` extra is installed; ``telemetry overview``
6
+ describes the noun (satisfying the agent-first rubric: any noun with
7
+ action-verbs must also expose ``overview``).
8
+
9
+ This module imports only the **stdlib-clean** telemetry facade — never the SDK —
10
+ so ``convertible telemetry`` works (reporting ``sdk_installed: false``) even when
11
+ the extra is not installed.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import argparse
17
+
18
+ from convertible.cli._commands.overview import emit_overview
19
+ from convertible.cli._output import emit_result
20
+ from convertible.telemetry import TelemetryConfig, sdk_available
21
+
22
+
23
+ def _telemetry_sections() -> list[dict[str, object]]:
24
+ return [
25
+ {
26
+ "title": "What it does",
27
+ "items": [
28
+ "GPS for a drive: OpenTelemetry traces + metrics over OTLP",
29
+ "Off by default; opt in with CONVERTIBLE_OTEL_ENABLED=1",
30
+ "Needs the optional extra: pip install 'convertible-cli[otel]'",
31
+ "Instrumented in the loop + shared drive path, so every engine emits it",
32
+ ],
33
+ },
34
+ {
35
+ "title": "Signals",
36
+ "items": [
37
+ "spans: convertible.drive -> convertible.tool.* (+ convertible.handoff)",
38
+ "metrics: convertible.steps, convertible.tokens, convertible.tool.latency,"
39
+ " convertible.tool.calls, convertible.hook.denials, convertible.drive.duration",
40
+ ],
41
+ },
42
+ {
43
+ "title": "Configuration (precedence: explicit > CONVERTIBLE_OTEL_* > OTEL_* > default)",
44
+ "items": [
45
+ "CONVERTIBLE_OTEL_ENABLED — turn telemetry on (default: off)",
46
+ "CONVERTIBLE_OTEL_ENDPOINT / OTEL_EXPORTER_OTLP_ENDPOINT — collector URL",
47
+ "CONVERTIBLE_OTEL_SERVICE_NAME / OTEL_SERVICE_NAME — resource service.name",
48
+ "OTEL_SDK_DISABLED=true — standard kill-switch, forces telemetry off",
49
+ ],
50
+ },
51
+ {
52
+ "title": "Verbs",
53
+ "items": [
54
+ "telemetry status [--json] — show the resolved telemetry config",
55
+ "telemetry overview — describe the telemetry surface (this command)",
56
+ ],
57
+ },
58
+ ]
59
+
60
+
61
+ def cmd_telemetry_overview(args: argparse.Namespace) -> int:
62
+ emit_overview(
63
+ "convertible telemetry",
64
+ _telemetry_sections(),
65
+ json_mode=bool(getattr(args, "json", False)),
66
+ )
67
+ return 0
68
+
69
+
70
+ def cmd_telemetry_status(args: argparse.Namespace) -> int:
71
+ json_mode = bool(getattr(args, "json", False))
72
+ cfg = TelemetryConfig.resolve()
73
+ installed = sdk_available()
74
+
75
+ if json_mode:
76
+ payload = cfg.to_dict()
77
+ payload["sdk_installed"] = installed
78
+ emit_result(payload, json_mode=True)
79
+ return 0
80
+
81
+ lines = [
82
+ f"enabled: {cfg.enabled}",
83
+ f"sdk_installed: {installed}",
84
+ f"service_name: {cfg.service_name}",
85
+ f"otlp_endpoint: {cfg.otlp_endpoint}",
86
+ f"otlp_protocol: {cfg.otlp_protocol}",
87
+ f"traces_enabled: {cfg.traces_enabled}",
88
+ f"metrics_enabled:{cfg.metrics_enabled}",
89
+ ]
90
+ if cfg.enabled and not installed:
91
+ lines.append("note: enabled but the [otel] extra is not installed (no-op)")
92
+ emit_result("\n".join(lines), json_mode=False)
93
+ return 0
94
+
95
+
96
+ def _no_verb(args: argparse.Namespace) -> int:
97
+ return cmd_telemetry_overview(args)
98
+
99
+
100
+ def register(sub: argparse._SubParsersAction) -> None:
101
+ p = sub.add_parser(
102
+ "telemetry",
103
+ help="Inspect the GPS / OpenTelemetry config (see 'convertible telemetry overview').",
104
+ )
105
+ p.add_argument("--json", action="store_true", help="Emit structured JSON.")
106
+ p.set_defaults(func=_no_verb, json=False)
107
+ noun_sub = p.add_subparsers(dest="telemetry_command", parser_class=type(p))
108
+
109
+ st = noun_sub.add_parser("status", help="Show the resolved telemetry configuration.")
110
+ st.add_argument("--json", action="store_true", help="Emit structured JSON.")
111
+ st.set_defaults(func=cmd_telemetry_status)
112
+
113
+ ov = noun_sub.add_parser("overview", help="Describe the telemetry surface.")
114
+ ov.add_argument("--json", action="store_true", help="Emit structured JSON.")
115
+ ov.set_defaults(func=cmd_telemetry_overview)
@@ -326,6 +326,56 @@ scope); invokable skills are a tracked follow-up.
326
326
  """
327
327
 
328
328
 
329
+ _TELEMETRY = """\
330
+ # convertible telemetry
331
+
332
+ GPS for a drive: opt-in OpenTelemetry **traces + metrics** over OTLP. Telemetry
333
+ belongs to the chassis — it is instrumented once in the loop and the shared drive
334
+ path, so *every* engine emits identical signals (the all-engines rule), exactly
335
+ like lifecycle hooks.
336
+
337
+ Off by default. The OpenTelemetry SDK is an **optional extra** (the base install
338
+ keeps zero runtime dependencies); enable it with the env var and install the
339
+ extra:
340
+
341
+ pip install 'convertible-cli[otel]'
342
+ export CONVERTIBLE_OTEL_ENABLED=1
343
+ export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318 # OTLP/HTTP collector
344
+
345
+ When requested without the extra installed, convertible degrades to a no-op with
346
+ a one-line stderr notice — it never fails the drive.
347
+
348
+ ## Signals
349
+
350
+ - spans: `convertible.drive` (root) -> `convertible.tool.*` (per tool call) plus
351
+ `convertible.handoff`.
352
+ - metrics: `convertible.steps`, `convertible.tokens` (attr `kind`),
353
+ `convertible.tool.latency`, `convertible.tool.calls`, `convertible.hook.denials`,
354
+ `convertible.drive.duration` (attr `status`).
355
+
356
+ ## Configuration
357
+
358
+ Precedence (highest first): explicit > `CONVERTIBLE_OTEL_*` > standard `OTEL_*` >
359
+ default. `OTEL_SDK_DISABLED=true` is honored as a kill-switch.
360
+
361
+ - `CONVERTIBLE_OTEL_ENABLED` — turn telemetry on (default: off).
362
+ - `CONVERTIBLE_OTEL_ENDPOINT` / `OTEL_EXPORTER_OTLP_ENDPOINT` — collector URL.
363
+ - `CONVERTIBLE_OTEL_SERVICE_NAME` / `OTEL_SERVICE_NAME` — resource `service.name`.
364
+ - `CONVERTIBLE_OTEL_METRICS_ENABLED` — toggle metric emission (default: on).
365
+
366
+ ## Usage
367
+
368
+ convertible telemetry status
369
+ convertible telemetry status --json
370
+ convertible telemetry overview
371
+
372
+ ## See also
373
+
374
+ - `convertible explain drive`
375
+ - `convertible explain hooks`
376
+ """
377
+
378
+
329
379
  ENTRIES: dict[tuple[str, ...], str] = {
330
380
  (): _ROOT,
331
381
  ("convertible",): _ROOT,
@@ -353,4 +403,7 @@ ENTRIES: dict[tuple[str, ...], str] = {
353
403
  ("skills",): _SKILLS,
354
404
  ("skills", "list"): _SKILLS,
355
405
  ("skills", "overview"): _SKILLS,
406
+ ("telemetry",): _TELEMETRY,
407
+ ("telemetry", "status"): _TELEMETRY,
408
+ ("telemetry", "overview"): _TELEMETRY,
356
409
  }