imbue-mngr-opencode-usage 0.1.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.
@@ -0,0 +1,364 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ **/__pycache__/
3
+ **/*.py[codz]
4
+ **/*$py.class
5
+
6
+ # macOS Finder metadata
7
+ **/.DS_Store
8
+
9
+ # C extensions
10
+ **/*.so
11
+
12
+ # Distribution / packaging
13
+ **/.Python
14
+ **/build/
15
+ **/develop-eggs/
16
+ **/dist/
17
+ **/downloads/
18
+ **/eggs/
19
+ **/.eggs/
20
+ **/lib/
21
+ **/lib64/
22
+ **/parts/
23
+ **/sdist/
24
+ **/var/
25
+ **/wheels/
26
+ **/share/python-wheels/
27
+ **/*.egg-info/
28
+ **/.installed.cfg
29
+ **/*.egg
30
+ **/MANIFEST
31
+
32
+ # PyInstaller
33
+ # Usually these files are written by a python script from a template
34
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
35
+ **/*.manifest
36
+ **/*.spec
37
+
38
+ # Installer logs
39
+ **/pip-log.txt
40
+ **/pip-delete-this-directory.txt
41
+
42
+ # Unit test / coverage reports
43
+ **/htmlcov/
44
+ **/.tox/
45
+ **/.nox/
46
+ **/.coverage
47
+ **/.coverage.*
48
+ **/.cache
49
+ **/nosetests.xml
50
+ **/coverage.xml
51
+ **/*.cover
52
+ **/*.py.cover
53
+ **/.hypothesis/
54
+ **/.pytest_cache/
55
+ **/cover/
56
+
57
+ # Translations
58
+ **/*.mo
59
+ **/*.pot
60
+
61
+ # Django stuff:
62
+ **/*.log
63
+ **/local_settings.py
64
+ **/db.sqlite3
65
+ **/db.sqlite3-journal
66
+
67
+ # Flask stuff:
68
+ **/instance/
69
+ **/.webassets-cache
70
+
71
+ # Scrapy stuff:
72
+ **/.scrapy
73
+
74
+ # Sphinx documentation
75
+ **/docs/_build/
76
+
77
+ # PyBuilder
78
+ **/.pybuilder/
79
+ **/target/
80
+
81
+ # Jupyter Notebook
82
+ **/.ipynb_checkpoints
83
+
84
+ # IPython
85
+ **/profile_default/
86
+ **/ipython_config.py
87
+
88
+ # pyenv
89
+ # For a library or package, you might want to ignore these files since the code is
90
+ # intended to run in multiple environments; otherwise, check them in:
91
+ # .python-version
92
+
93
+ # pipenv
94
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
95
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
96
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
97
+ # install all needed dependencies.
98
+ #Pipfile.lock
99
+
100
+ # UV
101
+ # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
102
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
103
+ # commonly ignored for libraries.
104
+ #uv.lock
105
+
106
+ # poetry
107
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
108
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
109
+ # commonly ignored for libraries.
110
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
111
+ #poetry.lock
112
+ #poetry.toml
113
+
114
+ # pdm
115
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
116
+ # pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
117
+ # https://pdm-project.org/en/latest/usage/project/#working-with-version-control
118
+ #pdm.lock
119
+ #pdm.toml
120
+ **/.pdm-python
121
+ **/.pdm-build/
122
+
123
+ # pixi
124
+ # Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
125
+ #pixi.lock
126
+ # Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
127
+ # in the .venv directory. It is recommended not to include this directory in version control.
128
+ **/.pixi
129
+
130
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
131
+ **/__pypackages__/
132
+
133
+ # Celery stuff
134
+ **/celerybeat-schedule
135
+ **/celerybeat.pid
136
+
137
+ # SageMath parsed files
138
+ **/*.sage.py
139
+
140
+ # Environments
141
+ **/.env
142
+ **/.envrc
143
+ **/.venv
144
+ **/.venv-*
145
+ **/env/
146
+ **/venv/
147
+ **/ENV/
148
+ **/env.bak/
149
+ **/venv.bak/
150
+
151
+ # Spyder project settings
152
+ **/.spyderproject
153
+ **/.spyproject
154
+
155
+ # Rope project settings
156
+ **/.ropeproject
157
+
158
+ # mkdocs documentation
159
+ **/site
160
+
161
+ # mypy
162
+ **/.mypy_cache/
163
+ **/.dmypy.json
164
+ **/dmypy.json
165
+
166
+ # Pyre type checker
167
+ **/.pyre/
168
+
169
+ # pytype static type analyzer
170
+ **/.pytype/
171
+
172
+ # Cython debug symbols
173
+ **/cython_debug/
174
+
175
+ # PyCharm
176
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
177
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
178
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
179
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
180
+ **/.idea/
181
+
182
+ # Abstra
183
+ # Abstra is an AI-powered process automation framework.
184
+ # Ignore directories containing user credentials, local state, and settings.
185
+ # Learn more at https://abstra.io/docs
186
+ **/.abstra/
187
+
188
+ # Visual Studio Code
189
+ # Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
190
+ # that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
191
+ # and can be added to the global gitignore or merged into this file. However, if you prefer,
192
+ # you could uncomment the following to ignore the entire vscode folder
193
+ # .vscode/
194
+
195
+ # Ruff stuff:
196
+ **/.ruff_cache/
197
+
198
+ # PyPI configuration file
199
+ **/.pypirc
200
+
201
+ # Cursor
202
+ # Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to
203
+ # exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
204
+ # refer to https://docs.cursor.com/context/ignore-files
205
+ **/.cursorignore
206
+ **/.cursorindexingignore
207
+
208
+ # Marimo
209
+ **/marimo/_static/
210
+ **/marimo/_lsp/
211
+ **/__marimo__/
212
+
213
+ # task folders for local custom claude workflow (at any depth, incl. the
214
+ # root-level `dev` project's `dev/_tasks/`, which `*/*/_tasks/` missed)
215
+ **/_tasks/
216
+ **/.sesskey
217
+
218
+ # Node.js
219
+ **/node_modules/
220
+
221
+ # Frontend build artifacts
222
+ **/frontend-dist/
223
+
224
+ # Claude Code local settings (session-specific permissions)
225
+ **/.claude/settings.local.json
226
+
227
+ # Local Claude settings backup files
228
+ **/.claude/*.bak
229
+
230
+ # Claude worktress
231
+ **/.claude/worktrees
232
+
233
+ # Agent definitions deployed by mngr_subagent_proxy at session start
234
+ **/.claude/agents/
235
+
236
+ # Local mngr settings (not committed)
237
+ **/.mngr/settings.local.toml
238
+
239
+ # Local working / scratch markdown (not committed)
240
+ **/*.local.md
241
+
242
+ # Local working / scratch shell scripts (not committed)
243
+ # (covers user notification scripts like scripts/notify_user.local.sh, too)
244
+ **/*.local.sh
245
+
246
+ # Local-only throwaway scripts (e.g. one-off test harnesses).
247
+ **/scripts/*.local.sh
248
+
249
+ # Local working / scratch Python + text (not committed; e.g. one-off validation
250
+ # harnesses or probe scripts). Mirrors the *.local.md / *.local.sh convention.
251
+ **/*.local.py
252
+ **/*.local.txt
253
+
254
+ # Test output files (slow tests, coverage summaries)
255
+ **/.test_output/
256
+
257
+ # TMR output directories (reports + test outputs)
258
+ **/tmr_*/
259
+ **/tmr-report/
260
+
261
+ # Active session marker file (used to detect interrupted sessions)
262
+ **/.claude/active
263
+
264
+ # we stick the image build artifacts here
265
+ **/.mngr/dev/build/
266
+ **/.mngr/dev/secrets/
267
+
268
+ # Offload caches and local files
269
+ **/.offload/**
270
+ **/test-results
271
+
272
+ # Minds deploy-time build artifacts.
273
+ # Ignore contents so we can un-ignore the committed `template/` subtree: git
274
+ # won't descend into a fully-ignored directory, so negations must live below
275
+ # the contents pattern rather than below the directory pattern.
276
+ **/.minds/*
277
+ # The template directory is committed: it defines the expected keys for each
278
+ # <service>.sh file, with empty values. `scripts/push_modal_secrets.py` uses
279
+ # it to check that per-env .sh files declare every template key.
280
+ !/.minds/template/
281
+ !/.minds/template/**
282
+ # Vault ACL policies for the minds tiers. The HCL is non-sensitive and is
283
+ # the source of truth for the policies pushed to Vault via
284
+ # `vault policy write <name> .minds/policies/<name>.hcl`.
285
+ !/.minds/policies/
286
+ !/.minds/policies/**
287
+
288
+ # Autofix session artifacts
289
+ **/.autofix/
290
+ **/.reviewer/logs/
291
+ **/.reviewer/outputs/
292
+ **/.reviewer/settings.local.json
293
+ **/.reviewer/.stop_hook_consecutive_blocks
294
+ **/.reviewer/conversation-issues.jsonl
295
+ **/.reviews/
296
+
297
+ # Stuck agent tracking (stop hook escape valve)
298
+ **/.claude/blocked_stop_commits
299
+
300
+ # Scheduled tasks lock file
301
+ **/.claude/scheduled_tasks.lock
302
+
303
+ # Demo recordings (asciinema .cast, .txt, and scripts)
304
+ **/.demos/
305
+
306
+ # for git worktrees from other repos
307
+ **/.external_worktrees/
308
+
309
+ # ignore coverage files generated within subprojects
310
+ **/cov.xml
311
+
312
+ # ignore demos
313
+ **/recordings
314
+ apps/minds_workspace_server/package-lock.json
315
+ **/*.local.md
316
+
317
+ # Generated by the _generate-dockerignore justfile recipe before each
318
+ # test-offload* run. The recipe strips this self-reference so offload's
319
+ # copy_untracked_files ships the generated file into the build context.
320
+ /.dockerignore
321
+
322
+ # Transient offload image-cache marker written by test-offload runs; not
323
+ # checked in (unlike the committed .offload-*-cache-key files which pin CI
324
+ # image versions).
325
+ /.offload-image-cache
326
+
327
+ # Generated by _generate-release-dockerfile from Dockerfile + .release.extras.
328
+ /libs/mngr/imbue/mngr/resources/Dockerfile.release
329
+ **/.reviewer/autofix/issues/
330
+ .reviewer/logs/
331
+
332
+ # Tailwind Play CDN JS for the minds desktop client. Fetched once via
333
+ # `just minds-tailwind` (plain curl, no build) and shipped with the
334
+ # distributable. Not checked in because it's a ~300KB blob we can always
335
+ # re-fetch.
336
+ /apps/minds/imbue/minds/desktop_client/static/tailwind.js
337
+
338
+ # macOS Finder metadata
339
+ **/.DS_Store
340
+ **/.env.swp
341
+ # Vim swap files for any file (`.foo.swp`, `.foo.swo`, etc.) -- generated
342
+ # while a file is open in vim. Never checked in.
343
+ **/.*.sw[a-p]
344
+
345
+ # `minds env deploy` writes a recover-target file at the monorepo root
346
+ # on failed deploys; `minds env recover` deletes it after rolling back.
347
+ # Never checked in. Per-env naming
348
+ # (``.minds-deploy-recover-target-<env-name>.json``) means each in-flight
349
+ # deploy has its own file, so concurrent deploys against different envs
350
+ # don't trip over each other.
351
+ **/.minds-deploy-*.json
352
+ # Per-env deploy lock files (kernel-level ``flock`` targets). Created
353
+ # by ``hold_deploy_lock`` in ``apps/minds/imbue/minds/envs/recover.py``
354
+ # and held for the entire duration of ``minds env deploy`` /
355
+ # ``minds env recover`` so two concurrent invocations against the same
356
+ # env serialize. The file itself is empty and cheap to leave around.
357
+ **/.minds-deploy-lock-*.lock
358
+ **/.antigravitycli/
359
+
360
+ # Playwright MCP scratch artifacts (mock screenshots, page snapshots)
361
+ **/.playwright-mcp/
362
+
363
+ # tmr (task-runner) orchestration scratch report
364
+ **/tmr-report/
@@ -0,0 +1,13 @@
1
+ # Changelog - mngr_opencode_usage
2
+
3
+ A concise, human-friendly summary of changes for the `mngr_opencode_usage` library. Entries are categorized using the [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) categories: Added, Changed, Deprecated, Removed, Fixed, Security.
4
+
5
+ For the full, unedited changelog entries, see [UNABRIDGED_CHANGELOG.md](UNABRIDGED_CHANGELOG.md).
6
+
7
+ ## [Unreleased]
8
+
9
+ ## [v0.1.0] - 2026-06-16
10
+
11
+ ### Added
12
+
13
+ - Added: New package `imbue-mngr-opencode-usage` providing cost/usage tracking for opencode agents in `mngr usage`. Installs a second in-process opencode plugin (alongside the lifecycle one) that appends one `cost_snapshot` event per assistant message (opencode's reported per-message cost, plus tokens and the provider-qualified model) to `events/opencode/usage/events.jsonl`. A reader hookimpl claims the `opencode` source and aggregates session-incrementally. After provisioning, an opencode agent's spend shows up in `mngr usage` like Claude's.
@@ -0,0 +1,61 @@
1
+ Metadata-Version: 2.4
2
+ Name: imbue-mngr-opencode-usage
3
+ Version: 0.1.0
4
+ Summary: OpenCode usage data provider for `mngr usage` (per-agent in-process plugin that captures per-message cost + tokens)
5
+ Requires-Python: >=3.12
6
+ Requires-Dist: imbue-mngr-opencode==0.2.16
7
+ Requires-Dist: imbue-mngr-usage==0.1.5
8
+ Requires-Dist: imbue-mngr==0.2.16
9
+ Description-Content-Type: text/markdown
10
+
11
+ # imbue-mngr-opencode-usage
12
+
13
+ OpenCode data provider for `mngr usage`. Single responsibility: install a second
14
+ in-process TypeScript plugin into each OpenCode agent so each assistant message
15
+ appends one event to `$MNGR_AGENT_STATE_DIR/events/opencode/usage/events.jsonl`.
16
+ The `mngr usage` CLI walks those events files itself (see `imbue-mngr-usage`).
17
+
18
+ ## How the pieces fit
19
+
20
+ ```
21
+ mngr_opencode_usage's on_after_provisioning hookimpl
22
+ └─→ installs <config_dir>/plugin/mngr_opencode_usage_plugin.ts
23
+ (alongside mngr_opencode's lifecycle plugin; OpenCode auto-loads plugin/*.ts)
24
+
25
+ OpenCode serve process, on each assistant message.updated
26
+ └─→ mngr_opencode_usage_plugin.ts
27
+ └─→ events/opencode/usage/events.jsonl (append one cost_snapshot event)
28
+
29
+ mngr usage
30
+ └─→ aggregate_usage_source hookimpl (claims the "opencode" source)
31
+ └─→ aggregate_session_incremental: sum each session's messages
32
+ ```
33
+
34
+ All file I/O goes through `host.write_text_file`, so provisioning works for local
35
+ **and** remote agents.
36
+
37
+ ## What gets captured
38
+
39
+ OpenCode computes cost and tokens per message and exposes them on each assistant
40
+ `message.updated` event. The writer emits, per message, a self-contained event
41
+ carrying:
42
+
43
+ - `cost.total_cost_usd` -- OpenCode's own per-message cost (provenance is
44
+ **REPORTED**; no estimation needed).
45
+ - `tokens` -- `{input, output, cache_read, cache_creation}` (reasoning folded
46
+ into output), for auditability.
47
+ - `model` -- the provider-qualified `<providerID>/<modelID>`.
48
+ - `session_id` + `message_id` -- the reader sums per session, keeping the freshest
49
+ event per `message_id` so a streaming message's re-fires collapse to its final
50
+ reading.
51
+
52
+ ## Why session-incremental
53
+
54
+ OpenCode reports each message's **own** cost, not a session running total. So the
55
+ writer is stateless -- it appends one self-contained event per message and keeps
56
+ nothing in memory. The reader (`aggregate_session_incremental`) recomputes each
57
+ session's total from the append-only log, which makes it robust across
58
+ `mngr stop`/`start` (a fresh serve process with empty memory loses no history).
59
+
60
+ `cost_mode` is `API_KEY`: OpenCode bills against a real provider key, so the cost
61
+ is real spend, never an imputed subscription value.
@@ -0,0 +1,51 @@
1
+ # imbue-mngr-opencode-usage
2
+
3
+ OpenCode data provider for `mngr usage`. Single responsibility: install a second
4
+ in-process TypeScript plugin into each OpenCode agent so each assistant message
5
+ appends one event to `$MNGR_AGENT_STATE_DIR/events/opencode/usage/events.jsonl`.
6
+ The `mngr usage` CLI walks those events files itself (see `imbue-mngr-usage`).
7
+
8
+ ## How the pieces fit
9
+
10
+ ```
11
+ mngr_opencode_usage's on_after_provisioning hookimpl
12
+ └─→ installs <config_dir>/plugin/mngr_opencode_usage_plugin.ts
13
+ (alongside mngr_opencode's lifecycle plugin; OpenCode auto-loads plugin/*.ts)
14
+
15
+ OpenCode serve process, on each assistant message.updated
16
+ └─→ mngr_opencode_usage_plugin.ts
17
+ └─→ events/opencode/usage/events.jsonl (append one cost_snapshot event)
18
+
19
+ mngr usage
20
+ └─→ aggregate_usage_source hookimpl (claims the "opencode" source)
21
+ └─→ aggregate_session_incremental: sum each session's messages
22
+ ```
23
+
24
+ All file I/O goes through `host.write_text_file`, so provisioning works for local
25
+ **and** remote agents.
26
+
27
+ ## What gets captured
28
+
29
+ OpenCode computes cost and tokens per message and exposes them on each assistant
30
+ `message.updated` event. The writer emits, per message, a self-contained event
31
+ carrying:
32
+
33
+ - `cost.total_cost_usd` -- OpenCode's own per-message cost (provenance is
34
+ **REPORTED**; no estimation needed).
35
+ - `tokens` -- `{input, output, cache_read, cache_creation}` (reasoning folded
36
+ into output), for auditability.
37
+ - `model` -- the provider-qualified `<providerID>/<modelID>`.
38
+ - `session_id` + `message_id` -- the reader sums per session, keeping the freshest
39
+ event per `message_id` so a streaming message's re-fires collapse to its final
40
+ reading.
41
+
42
+ ## Why session-incremental
43
+
44
+ OpenCode reports each message's **own** cost, not a session running total. So the
45
+ writer is stateless -- it appends one self-contained event per message and keeps
46
+ nothing in memory. The reader (`aggregate_session_incremental`) recomputes each
47
+ session's total from the append-only log, which makes it robust across
48
+ `mngr stop`/`start` (a fresh serve process with empty memory loses no history).
49
+
50
+ `cost_mode` is `API_KEY`: OpenCode bills against a real provider key, so the cost
51
+ is real spend, never an imputed subscription value.
@@ -0,0 +1,13 @@
1
+ # Unabridged Changelog - mngr_opencode_usage
2
+
3
+ Full, unedited changelog entries consolidated nightly from individual files in `libs/mngr_opencode_usage/changelog/`.
4
+
5
+ For a concise summary, see [CHANGELOG.md](CHANGELOG.md).
6
+
7
+ ## 2026-06-16
8
+
9
+ Corrected the package version back to 0.1.0 (it had been bumped to 0.1.1, but that version was never actually released).
10
+
11
+ ## 2026-06-16
12
+
13
+ New package `imbue-mngr-opencode-usage`: cost/usage tracking for OpenCode agents in `mngr usage`. It installs a second in-process OpenCode plugin (alongside the lifecycle one) that appends one `cost_snapshot` event per assistant message -- OpenCode's own per-message cost (reported, not estimated) plus tokens and the provider-qualified model -- to `events/opencode/usage/events.jsonl`. A reader hookimpl claims the `opencode` source and aggregates it with the session-incremental strategy (sum each session's messages). After provisioning an OpenCode agent, its spend now shows up in `mngr usage` like Claude's.
File without changes
@@ -0,0 +1,7 @@
1
+ """Project-level conftest for mngr_opencode_usage."""
2
+
3
+ from imbue.imbue_common.conftest_hooks import register_conftest_hooks
4
+ from imbue.mngr.utils.logging import suppress_warnings
5
+
6
+ suppress_warnings()
7
+ register_conftest_hooks(globals())
@@ -0,0 +1,56 @@
1
+ """Unit tests for the OpenCode ``aggregate_usage_source`` reader hookimpl."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from imbue.mngr_opencode_usage.plugin import aggregate_usage_source
8
+ from imbue.mngr_usage.api import parse_usage_events
9
+ from imbue.mngr_usage.data_types import CostMode
10
+ from imbue.mngr_usage.data_types import CostProvenance
11
+
12
+ _NOW = 2_800_000_000
13
+ _SINCE = 10**12
14
+
15
+
16
+ def _message_event(session_id: str, message_id: str, second: int, total_cost_usd: float) -> dict[str, Any]:
17
+ return {
18
+ "timestamp": f"2056-01-01T00:00:{second:02d}.000000000Z",
19
+ "session_id": session_id,
20
+ "message_id": message_id,
21
+ "event_id": f"evt-{message_id}-{second}",
22
+ "cost": {"total_cost_usd": total_cost_usd},
23
+ }
24
+
25
+
26
+ def test_opencode_hookimpl_sums_per_message_as_reported() -> None:
27
+ snapshot = aggregate_usage_source(
28
+ source_name="opencode",
29
+ agents_events={
30
+ "agent-1": parse_usage_events(
31
+ [
32
+ _message_event("s1", "m1", 1, 0.10),
33
+ _message_event("s1", "m2", 2, 0.25),
34
+ ],
35
+ "opencode",
36
+ )
37
+ },
38
+ since_seconds=_SINCE,
39
+ now=_NOW,
40
+ )
41
+ assert snapshot is not None
42
+ assert snapshot.source_name == "opencode"
43
+ record = snapshot.sessions[0]
44
+ assert record.cost.total_cost_usd == 0.35
45
+ assert record.cost_provenance == CostProvenance.REPORTED
46
+ assert record.cost_mode == CostMode.API_KEY
47
+
48
+
49
+ def test_opencode_hookimpl_declines_non_opencode_sources() -> None:
50
+ snapshot = aggregate_usage_source(
51
+ source_name="claude",
52
+ agents_events={"agent-1": parse_usage_events([_message_event("s1", "m1", 1, 0.10)], "claude")},
53
+ since_seconds=_SINCE,
54
+ now=_NOW,
55
+ )
56
+ assert snapshot is None
@@ -0,0 +1,3 @@
1
+ from imbue.mngr.utils.plugin_testing import register_plugin_test_fixtures
2
+
3
+ register_plugin_test_fixtures(globals())
@@ -0,0 +1,21 @@
1
+ """Guard the TS-writer <-> Python-reader source-name seam.
2
+
3
+ The writer (`.ts`) and the reader (`plugin.py`) agree on the source name only by
4
+ hand-synced string literals across the language boundary. A rename on one side
5
+ would silently disable usage tracking, so pin the invariant here.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import importlib.resources
11
+
12
+ from imbue.mngr_opencode_usage import resources as _resources
13
+ from imbue.mngr_opencode_usage.plugin import _OPENCODE_USAGE_SOURCE_NAME
14
+ from imbue.mngr_opencode_usage.plugin import _USAGE_PLUGIN_FILENAME
15
+
16
+
17
+ def test_writer_ts_emits_the_source_the_reader_claims() -> None:
18
+ writer_ts = importlib.resources.files(_resources).joinpath(_USAGE_PLUGIN_FILENAME).read_text()
19
+ # The writer emits `source: "<name>/usage"`; the reader strips "/usage" and
20
+ # claims "<name>". Assert the writer's literal matches the reader's constant.
21
+ assert f'"{_OPENCODE_USAGE_SOURCE_NAME}/usage"' in writer_ts