convertible-cli 0.5.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.5.0 → convertible_cli-0.7.0}/.markdownlint-cli2.yaml +3 -0
  2. {convertible_cli-0.5.0 → convertible_cli-0.7.0}/CHANGELOG.md +25 -0
  3. {convertible_cli-0.5.0 → convertible_cli-0.7.0}/CLAUDE.md +47 -5
  4. convertible_cli-0.5.0/README.md → convertible_cli-0.7.0/PKG-INFO +105 -0
  5. convertible_cli-0.5.0/PKG-INFO → convertible_cli-0.7.0/README.md +83 -17
  6. {convertible_cli-0.5.0 → convertible_cli-0.7.0}/convertible/cli/__init__.py +8 -0
  7. convertible_cli-0.7.0/convertible/cli/_commands/agents.py +109 -0
  8. {convertible_cli-0.5.0 → convertible_cli-0.7.0}/convertible/cli/_commands/drive.py +61 -30
  9. {convertible_cli-0.5.0 → convertible_cli-0.7.0}/convertible/cli/_commands/overview.py +2 -0
  10. convertible_cli-0.7.0/convertible/cli/_commands/skills.py +107 -0
  11. convertible_cli-0.7.0/convertible/cli/_commands/telemetry.py +115 -0
  12. {convertible_cli-0.5.0 → convertible_cli-0.7.0}/convertible/engine.py +18 -0
  13. {convertible_cli-0.5.0 → convertible_cli-0.7.0}/convertible/engines/mock.py +6 -1
  14. {convertible_cli-0.5.0 → convertible_cli-0.7.0}/convertible/engines/vllm_openai.py +6 -1
  15. {convertible_cli-0.5.0 → convertible_cli-0.7.0}/convertible/explain/catalog.py +119 -0
  16. convertible_cli-0.7.0/convertible/layers.py +288 -0
  17. {convertible_cli-0.5.0 → convertible_cli-0.7.0}/convertible/loop.py +77 -52
  18. convertible_cli-0.7.0/convertible/telemetry/__init__.py +237 -0
  19. convertible_cli-0.7.0/convertible/telemetry/_otel.py +325 -0
  20. convertible_cli-0.7.0/docs/plans/2026-05-28-convertible-gains-otel-gps-observability.md +73 -0
  21. convertible_cli-0.7.0/docs/specs/2026-05-28-convertible-gains-otel-gps-observability.md +54 -0
  22. {convertible_cli-0.5.0 → convertible_cli-0.7.0}/pyproject.toml +19 -1
  23. convertible_cli-0.7.0/tests/test_agents_cli.py +82 -0
  24. convertible_cli-0.7.0/tests/test_layers.py +294 -0
  25. convertible_cli-0.7.0/tests/test_layers_engine_parity.py +83 -0
  26. convertible_cli-0.7.0/tests/test_skills_cli.py +87 -0
  27. convertible_cli-0.7.0/tests/test_telemetry.py +270 -0
  28. convertible_cli-0.7.0/tests/test_telemetry_cli.py +88 -0
  29. {convertible_cli-0.5.0 → convertible_cli-0.7.0}/tests/test_zero_deps.py +10 -0
  30. {convertible_cli-0.5.0 → convertible_cli-0.7.0}/uv.lock +256 -1
  31. {convertible_cli-0.5.0 → convertible_cli-0.7.0}/.claude/skills/agent-config/SKILL.md +0 -0
  32. {convertible_cli-0.5.0 → convertible_cli-0.7.0}/.claude/skills/agent-config/data/backend-fingerprints.yaml +0 -0
  33. {convertible_cli-0.5.0 → convertible_cli-0.7.0}/.claude/skills/agent-config/scripts/show.sh +0 -0
  34. {convertible_cli-0.5.0 → convertible_cli-0.7.0}/.claude/skills/assign-to-workforce/SKILL.md +0 -0
  35. {convertible_cli-0.5.0 → convertible_cli-0.7.0}/.claude/skills/assign-to-workforce/scripts/assign-to-workforce.sh +0 -0
  36. {convertible_cli-0.5.0 → convertible_cli-0.7.0}/.claude/skills/cicd/SKILL.md +0 -0
  37. {convertible_cli-0.5.0 → convertible_cli-0.7.0}/.claude/skills/cicd/scripts/_resolve-nick.sh +0 -0
  38. {convertible_cli-0.5.0 → convertible_cli-0.7.0}/.claude/skills/cicd/scripts/portability-lint.sh +0 -0
  39. {convertible_cli-0.5.0 → convertible_cli-0.7.0}/.claude/skills/cicd/scripts/pr-reply.sh +0 -0
  40. {convertible_cli-0.5.0 → convertible_cli-0.7.0}/.claude/skills/cicd/scripts/pr-status.sh +0 -0
  41. {convertible_cli-0.5.0 → convertible_cli-0.7.0}/.claude/skills/cicd/scripts/workflow.sh +0 -0
  42. {convertible_cli-0.5.0 → convertible_cli-0.7.0}/.claude/skills/communicate/SKILL.md +0 -0
  43. {convertible_cli-0.5.0 → convertible_cli-0.7.0}/.claude/skills/communicate/scripts/fetch-issues.sh +0 -0
  44. {convertible_cli-0.5.0 → convertible_cli-0.7.0}/.claude/skills/communicate/scripts/mesh-message.sh +0 -0
  45. {convertible_cli-0.5.0 → convertible_cli-0.7.0}/.claude/skills/communicate/scripts/post-comment.sh +0 -0
  46. {convertible_cli-0.5.0 → convertible_cli-0.7.0}/.claude/skills/communicate/scripts/post-issue.sh +0 -0
  47. {convertible_cli-0.5.0 → convertible_cli-0.7.0}/.claude/skills/communicate/scripts/templates/skill-new-brief.md +0 -0
  48. {convertible_cli-0.5.0 → convertible_cli-0.7.0}/.claude/skills/communicate/scripts/templates/skill-update-brief.md +0 -0
  49. {convertible_cli-0.5.0 → convertible_cli-0.7.0}/.claude/skills/doc-test-alignment/SKILL.md +0 -0
  50. {convertible_cli-0.5.0 → convertible_cli-0.7.0}/.claude/skills/doc-test-alignment/scripts/check.sh +0 -0
  51. {convertible_cli-0.5.0 → convertible_cli-0.7.0}/.claude/skills/pypi-maintainer/SKILL.md +0 -0
  52. {convertible_cli-0.5.0 → convertible_cli-0.7.0}/.claude/skills/pypi-maintainer/scripts/switch-source.sh +0 -0
  53. {convertible_cli-0.5.0 → convertible_cli-0.7.0}/.claude/skills/run-tests/SKILL.md +0 -0
  54. {convertible_cli-0.5.0 → convertible_cli-0.7.0}/.claude/skills/run-tests/scripts/test.sh +0 -0
  55. {convertible_cli-0.5.0 → convertible_cli-0.7.0}/.claude/skills/sonarclaude/SKILL.md +0 -0
  56. {convertible_cli-0.5.0 → convertible_cli-0.7.0}/.claude/skills/sonarclaude/scripts/sonar.sh +0 -0
  57. {convertible_cli-0.5.0 → convertible_cli-0.7.0}/.claude/skills/spec-to-plan/SKILL.md +0 -0
  58. {convertible_cli-0.5.0 → convertible_cli-0.7.0}/.claude/skills/spec-to-plan/scripts/spec-to-plan.sh +0 -0
  59. {convertible_cli-0.5.0 → convertible_cli-0.7.0}/.claude/skills/think/SKILL.md +0 -0
  60. {convertible_cli-0.5.0 → convertible_cli-0.7.0}/.claude/skills/think/scripts/think.sh +0 -0
  61. {convertible_cli-0.5.0 → convertible_cli-0.7.0}/.claude/skills/version-bump/SKILL.md +0 -0
  62. {convertible_cli-0.5.0 → convertible_cli-0.7.0}/.claude/skills/version-bump/scripts/bump.py +0 -0
  63. {convertible_cli-0.5.0 → convertible_cli-0.7.0}/.claude/skills.local.yaml.example +0 -0
  64. {convertible_cli-0.5.0 → convertible_cli-0.7.0}/.devague/current +0 -0
  65. {convertible_cli-0.5.0 → convertible_cli-0.7.0}/.devague/current_plan +0 -0
  66. {convertible_cli-0.5.0 → convertible_cli-0.7.0}/.devague/frames/convertible-gains-an-extensibility-layer-like-clau.json +0 -0
  67. {convertible_cli-0.5.0 → convertible_cli-0.7.0}/.devague/frames/convertible-v0-ships-point-it-at-a-repo-task-and-i.json +0 -0
  68. {convertible_cli-0.5.0 → convertible_cli-0.7.0}/.devague/plans/convertible-gains-an-extensibility-layer-like-clau.json +0 -0
  69. {convertible_cli-0.5.0 → convertible_cli-0.7.0}/.devague/plans/convertible-v0-ships-point-it-at-a-repo-task-and-i.json +0 -0
  70. {convertible_cli-0.5.0 → convertible_cli-0.7.0}/.flake8 +0 -0
  71. {convertible_cli-0.5.0 → convertible_cli-0.7.0}/.github/workflows/publish.yml +0 -0
  72. {convertible_cli-0.5.0 → convertible_cli-0.7.0}/.github/workflows/tests.yml +0 -0
  73. {convertible_cli-0.5.0 → convertible_cli-0.7.0}/.gitignore +0 -0
  74. {convertible_cli-0.5.0 → convertible_cli-0.7.0}/LICENSE +0 -0
  75. {convertible_cli-0.5.0 → convertible_cli-0.7.0}/convertible/__init__.py +0 -0
  76. {convertible_cli-0.5.0 → convertible_cli-0.7.0}/convertible/__main__.py +0 -0
  77. {convertible_cli-0.5.0 → convertible_cli-0.7.0}/convertible/artifact.py +0 -0
  78. {convertible_cli-0.5.0 → convertible_cli-0.7.0}/convertible/cli/_banner-big.txt +0 -0
  79. {convertible_cli-0.5.0 → convertible_cli-0.7.0}/convertible/cli/_banner.py +0 -0
  80. {convertible_cli-0.5.0 → convertible_cli-0.7.0}/convertible/cli/_banner.txt +0 -0
  81. {convertible_cli-0.5.0 → convertible_cli-0.7.0}/convertible/cli/_commands/__init__.py +0 -0
  82. {convertible_cli-0.5.0 → convertible_cli-0.7.0}/convertible/cli/_commands/cli.py +0 -0
  83. {convertible_cli-0.5.0 → convertible_cli-0.7.0}/convertible/cli/_commands/commands.py +0 -0
  84. {convertible_cli-0.5.0 → convertible_cli-0.7.0}/convertible/cli/_commands/doctor.py +0 -0
  85. {convertible_cli-0.5.0 → convertible_cli-0.7.0}/convertible/cli/_commands/explain.py +0 -0
  86. {convertible_cli-0.5.0 → convertible_cli-0.7.0}/convertible/cli/_commands/hooks.py +0 -0
  87. {convertible_cli-0.5.0 → convertible_cli-0.7.0}/convertible/cli/_commands/learn.py +0 -0
  88. {convertible_cli-0.5.0 → convertible_cli-0.7.0}/convertible/cli/_commands/session.py +0 -0
  89. {convertible_cli-0.5.0 → convertible_cli-0.7.0}/convertible/cli/_commands/wheels.py +0 -0
  90. {convertible_cli-0.5.0 → convertible_cli-0.7.0}/convertible/cli/_commands/whoami.py +0 -0
  91. {convertible_cli-0.5.0 → convertible_cli-0.7.0}/convertible/cli/_errors.py +0 -0
  92. {convertible_cli-0.5.0 → convertible_cli-0.7.0}/convertible/cli/_output.py +0 -0
  93. {convertible_cli-0.5.0 → convertible_cli-0.7.0}/convertible/commands.py +0 -0
  94. {convertible_cli-0.5.0 → convertible_cli-0.7.0}/convertible/config.py +0 -0
  95. {convertible_cli-0.5.0 → convertible_cli-0.7.0}/convertible/configdir.py +0 -0
  96. {convertible_cli-0.5.0 → convertible_cli-0.7.0}/convertible/contract.py +0 -0
  97. {convertible_cli-0.5.0 → convertible_cli-0.7.0}/convertible/engines/__init__.py +0 -0
  98. {convertible_cli-0.5.0 → convertible_cli-0.7.0}/convertible/explain/__init__.py +0 -0
  99. {convertible_cli-0.5.0 → convertible_cli-0.7.0}/convertible/handoff.py +0 -0
  100. {convertible_cli-0.5.0 → convertible_cli-0.7.0}/convertible/hooks.py +0 -0
  101. {convertible_cli-0.5.0 → convertible_cli-0.7.0}/convertible/registry.py +0 -0
  102. {convertible_cli-0.5.0 → convertible_cli-0.7.0}/convertible/tools.py +0 -0
  103. {convertible_cli-0.5.0 → convertible_cli-0.7.0}/culture.yaml +0 -0
  104. {convertible_cli-0.5.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
  105. {convertible_cli-0.5.0 → convertible_cli-0.7.0}/docs/plans/2026-05-27-convertible-gains-an-extensibility-layer-like-clau.md +0 -0
  106. {convertible_cli-0.5.0 → convertible_cli-0.7.0}/docs/skill-sources.md +0 -0
  107. {convertible_cli-0.5.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
  108. {convertible_cli-0.5.0 → convertible_cli-0.7.0}/docs/specs/2026-05-27-convertible-gains-an-extensibility-layer-like-clau.md +0 -0
  109. {convertible_cli-0.5.0 → convertible_cli-0.7.0}/sonar-project.properties +0 -0
  110. {convertible_cli-0.5.0 → convertible_cli-0.7.0}/tests/__init__.py +0 -0
  111. {convertible_cli-0.5.0 → convertible_cli-0.7.0}/tests/test_artifact.py +0 -0
  112. {convertible_cli-0.5.0 → convertible_cli-0.7.0}/tests/test_banner.py +0 -0
  113. {convertible_cli-0.5.0 → convertible_cli-0.7.0}/tests/test_boundary.py +0 -0
  114. {convertible_cli-0.5.0 → convertible_cli-0.7.0}/tests/test_cli.py +0 -0
  115. {convertible_cli-0.5.0 → convertible_cli-0.7.0}/tests/test_cli_introspection.py +0 -0
  116. {convertible_cli-0.5.0 → convertible_cli-0.7.0}/tests/test_commands.py +0 -0
  117. {convertible_cli-0.5.0 → convertible_cli-0.7.0}/tests/test_commands_cli.py +0 -0
  118. {convertible_cli-0.5.0 → convertible_cli-0.7.0}/tests/test_config.py +0 -0
  119. {convertible_cli-0.5.0 → convertible_cli-0.7.0}/tests/test_configdir.py +0 -0
  120. {convertible_cli-0.5.0 → convertible_cli-0.7.0}/tests/test_contract.py +0 -0
  121. {convertible_cli-0.5.0 → convertible_cli-0.7.0}/tests/test_drive.py +0 -0
  122. {convertible_cli-0.5.0 → convertible_cli-0.7.0}/tests/test_e2e_extensibility.py +0 -0
  123. {convertible_cli-0.5.0 → convertible_cli-0.7.0}/tests/test_e2e_mock.py +0 -0
  124. {convertible_cli-0.5.0 → convertible_cli-0.7.0}/tests/test_engine.py +0 -0
  125. {convertible_cli-0.5.0 → convertible_cli-0.7.0}/tests/test_handoff.py +0 -0
  126. {convertible_cli-0.5.0 → convertible_cli-0.7.0}/tests/test_hooks.py +0 -0
  127. {convertible_cli-0.5.0 → convertible_cli-0.7.0}/tests/test_hooks_cli.py +0 -0
  128. {convertible_cli-0.5.0 → convertible_cli-0.7.0}/tests/test_loop.py +0 -0
  129. {convertible_cli-0.5.0 → convertible_cli-0.7.0}/tests/test_mock_engine.py +0 -0
  130. {convertible_cli-0.5.0 → convertible_cli-0.7.0}/tests/test_registry.py +0 -0
  131. {convertible_cli-0.5.0 → convertible_cli-0.7.0}/tests/test_review_fixes.py +0 -0
  132. {convertible_cli-0.5.0 → convertible_cli-0.7.0}/tests/test_session.py +0 -0
  133. {convertible_cli-0.5.0 → convertible_cli-0.7.0}/tests/test_tools.py +0 -0
  134. {convertible_cli-0.5.0 → convertible_cli-0.7.0}/tests/test_vllm_live.py +0 -0
  135. {convertible_cli-0.5.0 → convertible_cli-0.7.0}/tests/test_vllm_openai.py +0 -0
  136. {convertible_cli-0.5.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,31 @@ 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
+
21
+ ## [0.6.0] - 2026-05-28
22
+
23
+ ### Added
24
+
25
+ - Layered per-model config: AGENTS instructions (`AGENTS.md` -> `AGENTS.convertible.md` -> `AGENTS.convertible.<model>.md` at the repo root, with a `~/.convertible/` fallback) and skills (`.convertible/skills/*.md` -> `.convertible/<model>/skills/*.md`) compose into the engine system prompt via `convertible/layers.py`
26
+ - `convertible agents` and `convertible skills` introspection nouns (list + overview, `--json`, `--model`)
27
+ - `Engine.system_prompt()` base-class helper injects the layered prompt for every engine (all-engines rule)
28
+
29
+ ### Changed
30
+
31
+ - Both engines (mock, vllm-openai) now pass a model-specific `system_prompt` to the loop; behavior is byte-identical when no AGENTS/skills files exist
32
+
8
33
  ## [0.5.0] - 2026-05-27
9
34
 
10
35
  ### Changed
@@ -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
@@ -34,6 +39,16 @@ The car metaphor *is* the architecture:
34
39
  code path, no daemon.
35
40
  - **Config resolution** — `convertible/configdir.py`: repo-level
36
41
  `.convertible/` overrides user-level `~/.convertible/`.
42
+ - **Layered per-model config** — `convertible/layers.py`: AGENTS instructions
43
+ (`AGENTS.md` → `AGENTS.convertible.md` → `AGENTS.convertible.<model>.md`, at
44
+ the repo root with a `~/.convertible/` fallback) and skills
45
+ (`.convertible/skills/*.md` → `.convertible/<model>/skills/*.md`) compose into
46
+ the engine system prompt. Resolution builds exact paths for the current model
47
+ and never globs sibling models — per-model isolation is structural. Injected
48
+ once on the `Engine` base class (`system_prompt()`), so every engine inherits
49
+ it (all-engines rule). Surfaced via the `agents` / `skills` introspection
50
+ nouns. **MCP layering is not built** — convertible reads no `mcp.json` and has
51
+ no `mcp` verb; a live MCP client is a re-spec (see scope below).
37
52
 
38
53
  The buildable spec and plan this implementation converged from live in
39
54
  [`docs/specs/`](docs/specs/) and [`docs/plans/`](docs/plans/) (authored via the
@@ -43,14 +58,21 @@ The buildable spec and plan this implementation converged from live in
43
58
 
44
59
  In scope: the chassis, the entry-point wheel contract, exactly two engines
45
60
  (`mock`, `vllm-openai`), the git/PR handoff, command templates, lifecycle
46
- hooks, and the foreground interactive palette.
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.
47
64
 
48
65
  **Out of scope for v0** — do not add without re-speccing: a multi-engine
49
66
  router/policy "gearbox", an execution sandbox, a daemon/server mode,
50
- Codex/Claude/Gemini drivers, and a per-repo hook trust gate / `--no-hooks`
67
+ Codex/Claude/Gemini drivers, a per-repo hook trust gate / `--no-hooks`
51
68
  escape hatch (planned follow-up hardening — not yet built; document this gap
52
- honestly, never invent a `--no-hooks` flag). Adding an excluded feature means
53
- scope crept.
69
+ honestly, never invent a `--no-hooks` flag), and an **MCP execution runtime**
70
+ (a live MCP client — stdio/socket transport, tool discovery, dynamic tool
71
+ registration). The layered config ships AGENTS + skills only; `mcp.json` is
72
+ **not** read and there is no `mcp` verb. A live MCP client would breach the
73
+ no-deps / no-socket / no-daemon conventions and needs its own spec — document
74
+ this gap honestly, never invent an `mcp` surface. Adding an excluded feature
75
+ means scope crept.
54
76
 
55
77
  ## The all-engines rule
56
78
 
@@ -65,7 +87,15 @@ test (`tests/test_e2e_mock.py`) is the guard.
65
87
  - **No runtime dependencies.** `pyproject.toml` keeps `dependencies = []`; the
66
88
  vLLM driver speaks the OpenAI wire format over stdlib `urllib`; commands and
67
89
  hooks use only stdlib (`json`, `subprocess`, `pathlib`). Don't add a runtime
68
- 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.
69
99
  - **Agent-first CLI.** New verbs are `convertible/cli/_commands/` modules with a
70
100
  `register(sub)`, wired in `convertible/cli/__init__.py`. Results to stdout,
71
101
  diagnostics/errors to stderr (never mixed); every command supports `--json`;
@@ -82,6 +112,11 @@ test (`tests/test_e2e_mock.py`) is the guard.
82
112
  hook firing — new engine wheels inherit the full lifecycle layer automatically
83
113
  and must not duplicate it. The all-engines rule applies: a hook config that
84
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.
85
120
  - **Repo-shipped hooks run by default (trusted-operator-env model D2).** There
86
121
  is no `--no-hooks` flag today. A per-repo trust gate is a tracked follow-up.
87
122
  Document this gap clearly; never document a non-existent flag.
@@ -102,6 +137,13 @@ uv run convertible hooks list --repo . # list configured hooks
102
137
  uv run convertible hooks overview # surface description
103
138
  uv run convertible session --repo . --engine mock # interactive palette
104
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
+
105
147
  # Lint + gates CI enforces:
106
148
  uv run black --check convertible tests
107
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,82 @@ 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
+
376
+ ## Per-model instructions & skills
377
+
378
+ Convertible composes a model-specific **system prompt** for every drive from two
379
+ layered families, resolved *relative to the model currently driving*. Strict
380
+ per-model isolation: driving model X reads only X's overlay plus the shared base
381
+ — it never even opens model Y's files (isolation is structural, built from exact
382
+ paths, not filtered).
383
+
384
+ **AGENTS instructions** cascade from the **repo root** (the cross-tool standard
385
+ location — sibling agent tools read `AGENTS.md` there too), general → specific,
386
+ with a `~/.convertible/` user-level fallback:
387
+
388
+ ```text
389
+ AGENTS.md # shared base
390
+ AGENTS.convertible.md # convertible overlay
391
+ AGENTS.convertible.<model>.md # model overlay
392
+ ```
393
+
394
+ **Skills** are markdown capability docs under `.convertible/`, folded into the
395
+ prompt as a compact name + one-line-summary catalog (a skill is instructional
396
+ text only — there is no skill *execution* in v0):
397
+
398
+ ```text
399
+ .convertible/skills/*.md # base
400
+ .convertible/<model>/skills/*.md # model overlay (shadows base by stem)
401
+ ```
402
+
403
+ `<model>` is sanitized to a filename-safe token (e.g. `Qwen/Qwen3-32B` →
404
+ `Qwen-Qwen3-32B`). Inspect what resolves for a model:
405
+
406
+ ```bash
407
+ uv run convertible agents list --model Qwen/Qwen3-32B --repo .
408
+ uv run convertible skills list --model Qwen/Qwen3-32B --repo .
409
+ ```
410
+
411
+ > **MCP layering is not built yet.** Convertible does not read `mcp.json` or
412
+ > connect to any MCP server today; a live MCP client needs its own spec. There
413
+ > is no `mcp` verb — don't rely on a non-existent surface.
414
+
316
415
  ## ⚠ Security: repo-shipped hooks run by default
317
416
 
318
417
  > **This is a code-execution risk. Read before driving an untrusted repo.**
@@ -351,6 +450,12 @@ rely on a non-existent flag.
351
450
  | `commands overview` | Describe the commands surface. |
352
451
  | `hooks list` | List configured hook entries for a repo. |
353
452
  | `hooks overview` | Describe the hooks surface. |
453
+ | `agents list` | List resolved AGENTS instruction layers for a model. |
454
+ | `agents overview` | Describe the agents surface. |
455
+ | `skills list` | List resolved skill docs for a model. |
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. |
354
459
  | `session` | Open a foreground interactive palette. |
355
460
  | `wheels list` | List discovered engine wheels (the garage). |
356
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.5.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,82 @@ 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
+
354
+ ## Per-model instructions & skills
355
+
356
+ Convertible composes a model-specific **system prompt** for every drive from two
357
+ layered families, resolved *relative to the model currently driving*. Strict
358
+ per-model isolation: driving model X reads only X's overlay plus the shared base
359
+ — it never even opens model Y's files (isolation is structural, built from exact
360
+ paths, not filtered).
361
+
362
+ **AGENTS instructions** cascade from the **repo root** (the cross-tool standard
363
+ location — sibling agent tools read `AGENTS.md` there too), general → specific,
364
+ with a `~/.convertible/` user-level fallback:
365
+
366
+ ```text
367
+ AGENTS.md # shared base
368
+ AGENTS.convertible.md # convertible overlay
369
+ AGENTS.convertible.<model>.md # model overlay
370
+ ```
371
+
372
+ **Skills** are markdown capability docs under `.convertible/`, folded into the
373
+ prompt as a compact name + one-line-summary catalog (a skill is instructional
374
+ text only — there is no skill *execution* in v0):
375
+
376
+ ```text
377
+ .convertible/skills/*.md # base
378
+ .convertible/<model>/skills/*.md # model overlay (shadows base by stem)
379
+ ```
380
+
381
+ `<model>` is sanitized to a filename-safe token (e.g. `Qwen/Qwen3-32B` →
382
+ `Qwen-Qwen3-32B`). Inspect what resolves for a model:
383
+
384
+ ```bash
385
+ uv run convertible agents list --model Qwen/Qwen3-32B --repo .
386
+ uv run convertible skills list --model Qwen/Qwen3-32B --repo .
387
+ ```
388
+
389
+ > **MCP layering is not built yet.** Convertible does not read `mcp.json` or
390
+ > connect to any MCP server today; a live MCP client needs its own spec. There
391
+ > is no `mcp` verb — don't rely on a non-existent surface.
392
+
333
393
  ## ⚠ Security: repo-shipped hooks run by default
334
394
 
335
395
  > **This is a code-execution risk. Read before driving an untrusted repo.**
@@ -368,6 +428,12 @@ rely on a non-existent flag.
368
428
  | `commands overview` | Describe the commands surface. |
369
429
  | `hooks list` | List configured hook entries for a repo. |
370
430
  | `hooks overview` | Describe the hooks surface. |
431
+ | `agents list` | List resolved AGENTS instruction layers for a model. |
432
+ | `agents overview` | Describe the agents surface. |
433
+ | `skills list` | List resolved skill docs for a model. |
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. |
371
437
  | `session` | Open a foreground interactive palette. |
372
438
  | `wheels list` | List discovered engine wheels (the garage). |
373
439
  | `whoami` | Report this agent's nick, version, backend, and model. |
@@ -73,6 +73,7 @@ def _stdio_is_interactive() -> bool:
73
73
 
74
74
 
75
75
  def _build_parser() -> argparse.ArgumentParser:
76
+ from convertible.cli._commands import agents as _agents_group
76
77
  from convertible.cli._commands import cli as _cli_group
77
78
  from convertible.cli._commands import commands as _commands_group
78
79
  from convertible.cli._commands import doctor as _doctor_cmd
@@ -82,6 +83,8 @@ def _build_parser() -> argparse.ArgumentParser:
82
83
  from convertible.cli._commands import learn as _learn_cmd
83
84
  from convertible.cli._commands import overview as _overview_cmd
84
85
  from convertible.cli._commands import session as _session_cmd
86
+ from convertible.cli._commands import skills as _skills_group
87
+ from convertible.cli._commands import telemetry as _telemetry_group
85
88
  from convertible.cli._commands import wheels as _wheels_group
86
89
  from convertible.cli._commands import whoami as _whoami_cmd
87
90
 
@@ -110,6 +113,11 @@ def _build_parser() -> argparse.ArgumentParser:
110
113
  # Extensibility layer: command templates + lifecycle hooks.
111
114
  _commands_group.register(sub)
112
115
  _hooks_group.register(sub)
116
+ # Layered per-model config: AGENTS instructions + skills.
117
+ _agents_group.register(sub)
118
+ _skills_group.register(sub)
119
+ # GPS: OpenTelemetry traces + metrics (opt-in, optional [otel] extra).
120
+ _telemetry_group.register(sub)
113
121
  # Interactive foreground palette (c28/R8).
114
122
  _session_cmd.register(sub)
115
123
 
@@ -0,0 +1,109 @@
1
+ """``convertible agents`` — inspect layered AGENTS instruction files.
2
+
3
+ ``agents list`` resolves the AGENTS instruction cascade for a model
4
+ (``AGENTS.md`` -> ``AGENTS.convertible.md`` -> ``AGENTS.convertible.<model>.md``;
5
+ repo root with a ``~/.convertible/`` fallback) and reports the layers that
6
+ exist, in general -> specific order. ``agents overview`` describes the noun
7
+ (satisfying the agent-first rubric: any noun with action-verbs must also expose
8
+ ``overview``).
9
+
10
+ These layers are composed (with the engine default and the skills catalog) into
11
+ the system prompt every drive sends — so what ``agents list`` reports for a model
12
+ is exactly what that model is instructed with. Per-model isolation is structural:
13
+ only the named model's overlay is read, never a sibling model's.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import argparse
19
+ from pathlib import Path
20
+
21
+ from convertible.cli._commands.overview import emit_overview
22
+ from convertible.cli._output import emit_result
23
+ from convertible.config import EngineConfig
24
+ from convertible.layers import resolve_agents
25
+
26
+
27
+ def _agents_sections() -> list[dict[str, object]]:
28
+ return [
29
+ {
30
+ "title": "What it does",
31
+ "items": [
32
+ "Resolves AGENTS instruction layers for the current model",
33
+ "Cascade (general -> specific): AGENTS.md, AGENTS.convertible.md, "
34
+ "AGENTS.convertible.<model>.md",
35
+ "Read from the repo root, with a ~/.convertible/ user-level fallback",
36
+ "Composed into the system prompt every drive sends to the engine",
37
+ ],
38
+ },
39
+ {
40
+ "title": "Per-model isolation",
41
+ "items": [
42
+ "<model> is sanitized (e.g. 'Qwen/Qwen3-32B' -> 'Qwen-Qwen3-32B')",
43
+ "Only the named model's overlay is read — never a sibling model's",
44
+ "MCP layering is not built yet (no mcp.json reader); tracked separately",
45
+ ],
46
+ },
47
+ {
48
+ "title": "Verbs",
49
+ "items": [
50
+ "agents list [--model M] [--repo PATH] — list resolved AGENTS layers",
51
+ "agents overview — describe the agents surface (this command)",
52
+ ],
53
+ },
54
+ ]
55
+
56
+
57
+ def cmd_agents_overview(args: argparse.Namespace) -> int:
58
+ emit_overview(
59
+ "convertible agents",
60
+ _agents_sections(),
61
+ json_mode=bool(getattr(args, "json", False)),
62
+ )
63
+ return 0
64
+
65
+
66
+ def cmd_agents_list(args: argparse.Namespace) -> int:
67
+ repo = Path(getattr(args, "repo", ".")).expanduser()
68
+ model = getattr(args, "model", None) or EngineConfig.resolve().model
69
+ json_mode = bool(getattr(args, "json", False))
70
+
71
+ layers = resolve_agents(repo, model)
72
+
73
+ if json_mode:
74
+ items = [{"scope": layer.scope, "path": str(layer.path)} for layer in layers]
75
+ emit_result({"model": model, "agents": items}, json_mode=True)
76
+ elif not layers:
77
+ emit_result("(no AGENTS layers found)", json_mode=False)
78
+ else:
79
+ lines = [f"{layer.scope}\t{layer.path}" for layer in layers]
80
+ emit_result("\n".join(lines), json_mode=False)
81
+ return 0
82
+
83
+
84
+ def _no_verb(args: argparse.Namespace) -> int:
85
+ return cmd_agents_overview(args)
86
+
87
+
88
+ def register(sub: argparse._SubParsersAction) -> None:
89
+ p = sub.add_parser(
90
+ "agents",
91
+ help="Inspect layered AGENTS instruction files (see 'convertible agents overview').",
92
+ )
93
+ p.add_argument("--json", action="store_true", help="Emit structured JSON.")
94
+ p.set_defaults(func=_no_verb, json=False)
95
+ noun_sub = p.add_subparsers(dest="agents_command", parser_class=type(p))
96
+
97
+ lst = noun_sub.add_parser("list", help="List resolved AGENTS instruction layers.")
98
+ lst.add_argument("--repo", default=".", help="Path to the target repository (default: cwd).")
99
+ lst.add_argument(
100
+ "--model",
101
+ default=None,
102
+ help="Model to resolve layers for (default: the resolved engine model).",
103
+ )
104
+ lst.add_argument("--json", action="store_true", help="Emit structured JSON.")
105
+ lst.set_defaults(func=cmd_agents_list)
106
+
107
+ ov = noun_sub.add_parser("overview", help="Describe the agents surface.")
108
+ ov.add_argument("--json", action="store_true", help="Emit structured JSON.")
109
+ ov.set_defaults(func=cmd_agents_overview)
@@ -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:
@@ -27,6 +27,8 @@ _ARTIFACTS = [
27
27
  _VERBS = [
28
28
  "drive <instruction> — run a repo task through a coder engine",
29
29
  "wheels list — list discovered engine wheels",
30
+ "agents list — inspect layered AGENTS instruction files for a model",
31
+ "skills list — inspect layered skill docs for a model",
30
32
  "whoami — identity probe (nick, version, backend, model)",
31
33
  "learn — structured self-teaching prompt",
32
34
  "explain <path> — markdown docs for a topic",