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.
- {convertible_cli-0.5.0 → convertible_cli-0.7.0}/.markdownlint-cli2.yaml +3 -0
- {convertible_cli-0.5.0 → convertible_cli-0.7.0}/CHANGELOG.md +25 -0
- {convertible_cli-0.5.0 → convertible_cli-0.7.0}/CLAUDE.md +47 -5
- convertible_cli-0.5.0/README.md → convertible_cli-0.7.0/PKG-INFO +105 -0
- convertible_cli-0.5.0/PKG-INFO → convertible_cli-0.7.0/README.md +83 -17
- {convertible_cli-0.5.0 → convertible_cli-0.7.0}/convertible/cli/__init__.py +8 -0
- convertible_cli-0.7.0/convertible/cli/_commands/agents.py +109 -0
- {convertible_cli-0.5.0 → convertible_cli-0.7.0}/convertible/cli/_commands/drive.py +61 -30
- {convertible_cli-0.5.0 → convertible_cli-0.7.0}/convertible/cli/_commands/overview.py +2 -0
- convertible_cli-0.7.0/convertible/cli/_commands/skills.py +107 -0
- convertible_cli-0.7.0/convertible/cli/_commands/telemetry.py +115 -0
- {convertible_cli-0.5.0 → convertible_cli-0.7.0}/convertible/engine.py +18 -0
- {convertible_cli-0.5.0 → convertible_cli-0.7.0}/convertible/engines/mock.py +6 -1
- {convertible_cli-0.5.0 → convertible_cli-0.7.0}/convertible/engines/vllm_openai.py +6 -1
- {convertible_cli-0.5.0 → convertible_cli-0.7.0}/convertible/explain/catalog.py +119 -0
- convertible_cli-0.7.0/convertible/layers.py +288 -0
- {convertible_cli-0.5.0 → convertible_cli-0.7.0}/convertible/loop.py +77 -52
- convertible_cli-0.7.0/convertible/telemetry/__init__.py +237 -0
- convertible_cli-0.7.0/convertible/telemetry/_otel.py +325 -0
- convertible_cli-0.7.0/docs/plans/2026-05-28-convertible-gains-otel-gps-observability.md +73 -0
- convertible_cli-0.7.0/docs/specs/2026-05-28-convertible-gains-otel-gps-observability.md +54 -0
- {convertible_cli-0.5.0 → convertible_cli-0.7.0}/pyproject.toml +19 -1
- convertible_cli-0.7.0/tests/test_agents_cli.py +82 -0
- convertible_cli-0.7.0/tests/test_layers.py +294 -0
- convertible_cli-0.7.0/tests/test_layers_engine_parity.py +83 -0
- convertible_cli-0.7.0/tests/test_skills_cli.py +87 -0
- convertible_cli-0.7.0/tests/test_telemetry.py +270 -0
- convertible_cli-0.7.0/tests/test_telemetry_cli.py +88 -0
- {convertible_cli-0.5.0 → convertible_cli-0.7.0}/tests/test_zero_deps.py +10 -0
- {convertible_cli-0.5.0 → convertible_cli-0.7.0}/uv.lock +256 -1
- {convertible_cli-0.5.0 → convertible_cli-0.7.0}/.claude/skills/agent-config/SKILL.md +0 -0
- {convertible_cli-0.5.0 → convertible_cli-0.7.0}/.claude/skills/agent-config/data/backend-fingerprints.yaml +0 -0
- {convertible_cli-0.5.0 → convertible_cli-0.7.0}/.claude/skills/agent-config/scripts/show.sh +0 -0
- {convertible_cli-0.5.0 → convertible_cli-0.7.0}/.claude/skills/assign-to-workforce/SKILL.md +0 -0
- {convertible_cli-0.5.0 → convertible_cli-0.7.0}/.claude/skills/assign-to-workforce/scripts/assign-to-workforce.sh +0 -0
- {convertible_cli-0.5.0 → convertible_cli-0.7.0}/.claude/skills/cicd/SKILL.md +0 -0
- {convertible_cli-0.5.0 → convertible_cli-0.7.0}/.claude/skills/cicd/scripts/_resolve-nick.sh +0 -0
- {convertible_cli-0.5.0 → convertible_cli-0.7.0}/.claude/skills/cicd/scripts/portability-lint.sh +0 -0
- {convertible_cli-0.5.0 → convertible_cli-0.7.0}/.claude/skills/cicd/scripts/pr-reply.sh +0 -0
- {convertible_cli-0.5.0 → convertible_cli-0.7.0}/.claude/skills/cicd/scripts/pr-status.sh +0 -0
- {convertible_cli-0.5.0 → convertible_cli-0.7.0}/.claude/skills/cicd/scripts/workflow.sh +0 -0
- {convertible_cli-0.5.0 → convertible_cli-0.7.0}/.claude/skills/communicate/SKILL.md +0 -0
- {convertible_cli-0.5.0 → convertible_cli-0.7.0}/.claude/skills/communicate/scripts/fetch-issues.sh +0 -0
- {convertible_cli-0.5.0 → convertible_cli-0.7.0}/.claude/skills/communicate/scripts/mesh-message.sh +0 -0
- {convertible_cli-0.5.0 → convertible_cli-0.7.0}/.claude/skills/communicate/scripts/post-comment.sh +0 -0
- {convertible_cli-0.5.0 → convertible_cli-0.7.0}/.claude/skills/communicate/scripts/post-issue.sh +0 -0
- {convertible_cli-0.5.0 → convertible_cli-0.7.0}/.claude/skills/communicate/scripts/templates/skill-new-brief.md +0 -0
- {convertible_cli-0.5.0 → convertible_cli-0.7.0}/.claude/skills/communicate/scripts/templates/skill-update-brief.md +0 -0
- {convertible_cli-0.5.0 → convertible_cli-0.7.0}/.claude/skills/doc-test-alignment/SKILL.md +0 -0
- {convertible_cli-0.5.0 → convertible_cli-0.7.0}/.claude/skills/doc-test-alignment/scripts/check.sh +0 -0
- {convertible_cli-0.5.0 → convertible_cli-0.7.0}/.claude/skills/pypi-maintainer/SKILL.md +0 -0
- {convertible_cli-0.5.0 → convertible_cli-0.7.0}/.claude/skills/pypi-maintainer/scripts/switch-source.sh +0 -0
- {convertible_cli-0.5.0 → convertible_cli-0.7.0}/.claude/skills/run-tests/SKILL.md +0 -0
- {convertible_cli-0.5.0 → convertible_cli-0.7.0}/.claude/skills/run-tests/scripts/test.sh +0 -0
- {convertible_cli-0.5.0 → convertible_cli-0.7.0}/.claude/skills/sonarclaude/SKILL.md +0 -0
- {convertible_cli-0.5.0 → convertible_cli-0.7.0}/.claude/skills/sonarclaude/scripts/sonar.sh +0 -0
- {convertible_cli-0.5.0 → convertible_cli-0.7.0}/.claude/skills/spec-to-plan/SKILL.md +0 -0
- {convertible_cli-0.5.0 → convertible_cli-0.7.0}/.claude/skills/spec-to-plan/scripts/spec-to-plan.sh +0 -0
- {convertible_cli-0.5.0 → convertible_cli-0.7.0}/.claude/skills/think/SKILL.md +0 -0
- {convertible_cli-0.5.0 → convertible_cli-0.7.0}/.claude/skills/think/scripts/think.sh +0 -0
- {convertible_cli-0.5.0 → convertible_cli-0.7.0}/.claude/skills/version-bump/SKILL.md +0 -0
- {convertible_cli-0.5.0 → convertible_cli-0.7.0}/.claude/skills/version-bump/scripts/bump.py +0 -0
- {convertible_cli-0.5.0 → convertible_cli-0.7.0}/.claude/skills.local.yaml.example +0 -0
- {convertible_cli-0.5.0 → convertible_cli-0.7.0}/.devague/current +0 -0
- {convertible_cli-0.5.0 → convertible_cli-0.7.0}/.devague/current_plan +0 -0
- {convertible_cli-0.5.0 → convertible_cli-0.7.0}/.devague/frames/convertible-gains-an-extensibility-layer-like-clau.json +0 -0
- {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
- {convertible_cli-0.5.0 → convertible_cli-0.7.0}/.devague/plans/convertible-gains-an-extensibility-layer-like-clau.json +0 -0
- {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
- {convertible_cli-0.5.0 → convertible_cli-0.7.0}/.flake8 +0 -0
- {convertible_cli-0.5.0 → convertible_cli-0.7.0}/.github/workflows/publish.yml +0 -0
- {convertible_cli-0.5.0 → convertible_cli-0.7.0}/.github/workflows/tests.yml +0 -0
- {convertible_cli-0.5.0 → convertible_cli-0.7.0}/.gitignore +0 -0
- {convertible_cli-0.5.0 → convertible_cli-0.7.0}/LICENSE +0 -0
- {convertible_cli-0.5.0 → convertible_cli-0.7.0}/convertible/__init__.py +0 -0
- {convertible_cli-0.5.0 → convertible_cli-0.7.0}/convertible/__main__.py +0 -0
- {convertible_cli-0.5.0 → convertible_cli-0.7.0}/convertible/artifact.py +0 -0
- {convertible_cli-0.5.0 → convertible_cli-0.7.0}/convertible/cli/_banner-big.txt +0 -0
- {convertible_cli-0.5.0 → convertible_cli-0.7.0}/convertible/cli/_banner.py +0 -0
- {convertible_cli-0.5.0 → convertible_cli-0.7.0}/convertible/cli/_banner.txt +0 -0
- {convertible_cli-0.5.0 → convertible_cli-0.7.0}/convertible/cli/_commands/__init__.py +0 -0
- {convertible_cli-0.5.0 → convertible_cli-0.7.0}/convertible/cli/_commands/cli.py +0 -0
- {convertible_cli-0.5.0 → convertible_cli-0.7.0}/convertible/cli/_commands/commands.py +0 -0
- {convertible_cli-0.5.0 → convertible_cli-0.7.0}/convertible/cli/_commands/doctor.py +0 -0
- {convertible_cli-0.5.0 → convertible_cli-0.7.0}/convertible/cli/_commands/explain.py +0 -0
- {convertible_cli-0.5.0 → convertible_cli-0.7.0}/convertible/cli/_commands/hooks.py +0 -0
- {convertible_cli-0.5.0 → convertible_cli-0.7.0}/convertible/cli/_commands/learn.py +0 -0
- {convertible_cli-0.5.0 → convertible_cli-0.7.0}/convertible/cli/_commands/session.py +0 -0
- {convertible_cli-0.5.0 → convertible_cli-0.7.0}/convertible/cli/_commands/wheels.py +0 -0
- {convertible_cli-0.5.0 → convertible_cli-0.7.0}/convertible/cli/_commands/whoami.py +0 -0
- {convertible_cli-0.5.0 → convertible_cli-0.7.0}/convertible/cli/_errors.py +0 -0
- {convertible_cli-0.5.0 → convertible_cli-0.7.0}/convertible/cli/_output.py +0 -0
- {convertible_cli-0.5.0 → convertible_cli-0.7.0}/convertible/commands.py +0 -0
- {convertible_cli-0.5.0 → convertible_cli-0.7.0}/convertible/config.py +0 -0
- {convertible_cli-0.5.0 → convertible_cli-0.7.0}/convertible/configdir.py +0 -0
- {convertible_cli-0.5.0 → convertible_cli-0.7.0}/convertible/contract.py +0 -0
- {convertible_cli-0.5.0 → convertible_cli-0.7.0}/convertible/engines/__init__.py +0 -0
- {convertible_cli-0.5.0 → convertible_cli-0.7.0}/convertible/explain/__init__.py +0 -0
- {convertible_cli-0.5.0 → convertible_cli-0.7.0}/convertible/handoff.py +0 -0
- {convertible_cli-0.5.0 → convertible_cli-0.7.0}/convertible/hooks.py +0 -0
- {convertible_cli-0.5.0 → convertible_cli-0.7.0}/convertible/registry.py +0 -0
- {convertible_cli-0.5.0 → convertible_cli-0.7.0}/convertible/tools.py +0 -0
- {convertible_cli-0.5.0 → convertible_cli-0.7.0}/culture.yaml +0 -0
- {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
- {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
- {convertible_cli-0.5.0 → convertible_cli-0.7.0}/docs/skill-sources.md +0 -0
- {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
- {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
- {convertible_cli-0.5.0 → convertible_cli-0.7.0}/sonar-project.properties +0 -0
- {convertible_cli-0.5.0 → convertible_cli-0.7.0}/tests/__init__.py +0 -0
- {convertible_cli-0.5.0 → convertible_cli-0.7.0}/tests/test_artifact.py +0 -0
- {convertible_cli-0.5.0 → convertible_cli-0.7.0}/tests/test_banner.py +0 -0
- {convertible_cli-0.5.0 → convertible_cli-0.7.0}/tests/test_boundary.py +0 -0
- {convertible_cli-0.5.0 → convertible_cli-0.7.0}/tests/test_cli.py +0 -0
- {convertible_cli-0.5.0 → convertible_cli-0.7.0}/tests/test_cli_introspection.py +0 -0
- {convertible_cli-0.5.0 → convertible_cli-0.7.0}/tests/test_commands.py +0 -0
- {convertible_cli-0.5.0 → convertible_cli-0.7.0}/tests/test_commands_cli.py +0 -0
- {convertible_cli-0.5.0 → convertible_cli-0.7.0}/tests/test_config.py +0 -0
- {convertible_cli-0.5.0 → convertible_cli-0.7.0}/tests/test_configdir.py +0 -0
- {convertible_cli-0.5.0 → convertible_cli-0.7.0}/tests/test_contract.py +0 -0
- {convertible_cli-0.5.0 → convertible_cli-0.7.0}/tests/test_drive.py +0 -0
- {convertible_cli-0.5.0 → convertible_cli-0.7.0}/tests/test_e2e_extensibility.py +0 -0
- {convertible_cli-0.5.0 → convertible_cli-0.7.0}/tests/test_e2e_mock.py +0 -0
- {convertible_cli-0.5.0 → convertible_cli-0.7.0}/tests/test_engine.py +0 -0
- {convertible_cli-0.5.0 → convertible_cli-0.7.0}/tests/test_handoff.py +0 -0
- {convertible_cli-0.5.0 → convertible_cli-0.7.0}/tests/test_hooks.py +0 -0
- {convertible_cli-0.5.0 → convertible_cli-0.7.0}/tests/test_hooks_cli.py +0 -0
- {convertible_cli-0.5.0 → convertible_cli-0.7.0}/tests/test_loop.py +0 -0
- {convertible_cli-0.5.0 → convertible_cli-0.7.0}/tests/test_mock_engine.py +0 -0
- {convertible_cli-0.5.0 → convertible_cli-0.7.0}/tests/test_registry.py +0 -0
- {convertible_cli-0.5.0 → convertible_cli-0.7.0}/tests/test_review_fixes.py +0 -0
- {convertible_cli-0.5.0 → convertible_cli-0.7.0}/tests/test_session.py +0 -0
- {convertible_cli-0.5.0 → convertible_cli-0.7.0}/tests/test_tools.py +0 -0
- {convertible_cli-0.5.0 → convertible_cli-0.7.0}/tests/test_vllm_live.py +0 -0
- {convertible_cli-0.5.0 → convertible_cli-0.7.0}/tests/test_vllm_openai.py +0 -0
- {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,
|
|
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,
|
|
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)
|
|
53
|
-
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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.
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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",
|