imbue-mngr-pi-coding-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_pi_coding_usage
2
+
3
+ A concise, human-friendly summary of changes for the `mngr_pi_coding_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-pi-coding-usage` providing cost/usage tracking for pi agents in `mngr usage`. pi reports per-message cost client-side, so it's REPORTED (not estimated) and aggregated session-incrementally. The per-message writer lives in `mngr_pi_coding`'s lifecycle extension (pi loads a single explicit extension); this package owns the reader (an `aggregate_usage_source` hookimpl claiming the `pi-coding` source) and provisions a `pi_emit_usage` gate marker so the extension only emits usage events when this package is installed.
@@ -0,0 +1,41 @@
1
+ Metadata-Version: 2.4
2
+ Name: imbue-mngr-pi-coding-usage
3
+ Version: 0.1.0
4
+ Summary: pi usage data provider for `mngr usage` (reader + writer gate; the per-message writer lives in mngr_pi_coding's lifecycle extension)
5
+ Requires-Python: >=3.12
6
+ Requires-Dist: imbue-mngr-pi-coding==0.1.13
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-pi-coding-usage
12
+
13
+ pi data provider for `mngr usage`. pi loads a single explicit extension
14
+ (mngr_pi_coding's lifecycle extension, via `pi -e`), so the per-message usage
15
+ **writer** lives there -- it already holds each assistant message's cost and
16
+ tokens. This package owns the two usage-specific pieces:
17
+
18
+ - **The reader**: an `aggregate_usage_source` hookimpl claiming the `pi-coding`
19
+ source, aggregated session-incrementally (pi reports cost per message, summed
20
+ per session).
21
+ - **The writer gate**: `on_after_provisioning` drops a `pi_emit_usage` marker in
22
+ each pi agent's state dir. The lifecycle extension only emits usage events when
23
+ that marker is present -- so the events are written exactly when their reader
24
+ (this package) is installed.
25
+
26
+ ## What gets captured
27
+
28
+ pi computes per-message cost client-side (`usage.cost.total`), so cost is
29
+ **REPORTED** (no estimation). Each assistant `message_end` appends one
30
+ `cost_snapshot` to `events/pi-coding/usage/events.jsonl` with the reported cost,
31
+ tokens (`input`/`output`/`cacheRead`/`cacheWrite`), the provider-qualified model
32
+ (`<provider>/<model>`), and the session id (the pi session file's basename).
33
+ `cost_mode` is `API_KEY` (pi bills a real provider key).
34
+
35
+ ## Why the writer is in the harness extension
36
+
37
+ Unlike Claude (statusline shim) or OpenCode (auto-loaded `plugin/*.ts`), pi loads
38
+ exactly one explicit extension owned by mngr_pi_coding. A standalone writer would
39
+ require a harness change either way, so the writer is folded into the lifecycle
40
+ extension (which already extracts the message usage), and this package stays the
41
+ reader + gate.
@@ -0,0 +1,31 @@
1
+ # imbue-mngr-pi-coding-usage
2
+
3
+ pi data provider for `mngr usage`. pi loads a single explicit extension
4
+ (mngr_pi_coding's lifecycle extension, via `pi -e`), so the per-message usage
5
+ **writer** lives there -- it already holds each assistant message's cost and
6
+ tokens. This package owns the two usage-specific pieces:
7
+
8
+ - **The reader**: an `aggregate_usage_source` hookimpl claiming the `pi-coding`
9
+ source, aggregated session-incrementally (pi reports cost per message, summed
10
+ per session).
11
+ - **The writer gate**: `on_after_provisioning` drops a `pi_emit_usage` marker in
12
+ each pi agent's state dir. The lifecycle extension only emits usage events when
13
+ that marker is present -- so the events are written exactly when their reader
14
+ (this package) is installed.
15
+
16
+ ## What gets captured
17
+
18
+ pi computes per-message cost client-side (`usage.cost.total`), so cost is
19
+ **REPORTED** (no estimation). Each assistant `message_end` appends one
20
+ `cost_snapshot` to `events/pi-coding/usage/events.jsonl` with the reported cost,
21
+ tokens (`input`/`output`/`cacheRead`/`cacheWrite`), the provider-qualified model
22
+ (`<provider>/<model>`), and the session id (the pi session file's basename).
23
+ `cost_mode` is `API_KEY` (pi bills a real provider key).
24
+
25
+ ## Why the writer is in the harness extension
26
+
27
+ Unlike Claude (statusline shim) or OpenCode (auto-loaded `plugin/*.ts`), pi loads
28
+ exactly one explicit extension owned by mngr_pi_coding. A standalone writer would
29
+ require a harness change either way, so the writer is folded into the lifecycle
30
+ extension (which already extracts the message usage), and this package stays the
31
+ reader + gate.
@@ -0,0 +1,13 @@
1
+ # Unabridged Changelog - mngr_pi_coding_usage
2
+
3
+ Full, unedited changelog entries consolidated nightly from individual files in `libs/mngr_pi_coding_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-pi-coding-usage`: cost/usage tracking for pi agents in `mngr usage`. pi reports per-message cost client-side, so it's REPORTED (not estimated) and aggregated session-incrementally. Because pi loads a single explicit extension, the per-message writer lives in mngr_pi_coding's lifecycle extension; this package owns the reader (an `aggregate_usage_source` hookimpl claiming the `pi-coding` source) and provisions a `pi_emit_usage` gate marker so the extension only emits usage events when this package (their reader) is installed.
File without changes
@@ -0,0 +1,7 @@
1
+ """Project-level conftest for mngr_pi_coding_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 pi ``aggregate_usage_source`` reader hookimpl."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from imbue.mngr_pi_coding_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, event_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
+ "event_id": event_id,
21
+ "cost": {"total_cost_usd": total_cost_usd},
22
+ "model": "anthropic/claude-opus-4-8",
23
+ }
24
+
25
+
26
+ def test_pi_hookimpl_sums_per_message_as_reported() -> None:
27
+ snapshot = aggregate_usage_source(
28
+ source_name="pi-coding",
29
+ agents_events={
30
+ "agent-1": parse_usage_events(
31
+ [
32
+ _message_event("s1", "evt-pi-usage-0", 1, 0.01),
33
+ _message_event("s1", "evt-pi-usage-1", 2, 0.02),
34
+ ],
35
+ "pi-coding",
36
+ )
37
+ },
38
+ since_seconds=_SINCE,
39
+ now=_NOW,
40
+ )
41
+ assert snapshot is not None
42
+ assert snapshot.source_name == "pi-coding"
43
+ record = snapshot.sessions[0]
44
+ assert record.cost.total_cost_usd == 0.03
45
+ assert record.cost_provenance == CostProvenance.REPORTED
46
+ assert record.cost_mode == CostMode.API_KEY
47
+
48
+
49
+ def test_pi_hookimpl_declines_non_pi_sources() -> None:
50
+ snapshot = aggregate_usage_source(
51
+ source_name="opencode",
52
+ agents_events={"agent-1": parse_usage_events([_message_event("s1", "evt-pi-usage-0", 1, 0.01)], "opencode")},
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,23 @@
1
+ """Guard the cross-package writer <-> reader seam.
2
+
3
+ pi's usage *writer* lives in mngr_pi_coding's lifecycle extension (pi loads one
4
+ explicit extension, owned by the harness), while the reader + gate live here.
5
+ The two packages agree only by hand-synced string literals across both a
6
+ language *and* a package boundary -- the gate filename and the source name. A
7
+ rename on either side silently disables usage tracking, so pin the invariant.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from imbue.mngr_pi_coding.plugin import _LIFECYCLE_EXTENSION_NAME
13
+ from imbue.mngr_pi_coding.plugin import _load_resource
14
+ from imbue.mngr_pi_coding_usage.plugin import _PI_USAGE_SOURCE_NAME
15
+ from imbue.mngr_pi_coding_usage.plugin import _USAGE_GATE_FILENAME
16
+
17
+
18
+ def test_lifecycle_extension_uses_the_gate_and_source_this_package_owns() -> None:
19
+ lifecycle_ts = _load_resource(_LIFECYCLE_EXTENSION_NAME)
20
+ # The extension only emits usage when it sees this package's gate marker...
21
+ assert f'"{_USAGE_GATE_FILENAME}"' in lifecycle_ts
22
+ # ...and emits under the source the reader here claims (`<name>/usage`).
23
+ assert f'"{_PI_USAGE_SOURCE_NAME}/usage"' in lifecycle_ts
@@ -0,0 +1,67 @@
1
+ """pi usage data provider for `mngr usage` (reader + writer gate).
2
+
3
+ pi loads exactly one explicit extension -- mngr_pi_coding's lifecycle extension,
4
+ launched with ``pi -e`` -- so the usage *writer* lives there (it already holds
5
+ each assistant message's cost + tokens). This package owns the two pieces that
6
+ are genuinely usage-specific:
7
+
8
+ 1. A gate marker, provisioned per pi agent, that tells the lifecycle extension to
9
+ emit usage events. Without it the writer stays inert -- so usage events are
10
+ only written when *this* package (which ships their reader) is installed.
11
+ Emitting them unread would let ``mngr usage`` fall back to the wrong
12
+ (process-cumulative) strategy and undercount pi's per-message events.
13
+ 2. The reader: an ``aggregate_usage_source`` hookimpl claiming the ``pi-coding``
14
+ source, aggregated session-incrementally (pi reports cost per message).
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ from imbue.mngr import hookimpl
20
+ from imbue.mngr.config.data_types import MngrContext
21
+ from imbue.mngr.hosts.host import get_agent_state_dir_path
22
+ from imbue.mngr.interfaces.agent import AgentInterface
23
+ from imbue.mngr.interfaces.host import OnlineHostInterface
24
+ from imbue.mngr_pi_coding.plugin import PiCodingAgent
25
+ from imbue.mngr_usage.api import aggregate_session_incremental
26
+ from imbue.mngr_usage.data_types import UsageEvent
27
+ from imbue.mngr_usage.data_types import UsageSnapshot
28
+
29
+ # Gate marker the lifecycle extension checks (existsSync) to decide whether to
30
+ # emit usage events. Kept in sync with mngr_pi_lifecycle.ts (literal "pi_emit_usage").
31
+ _USAGE_GATE_FILENAME = "pi_emit_usage"
32
+
33
+ # Source the pi writer emits under ($STATE_DIR/events/pi-coding/usage/...); the
34
+ # reader strips "/usage". A fixed harness id (not the agent subtype), so usage
35
+ # from any pi subtype lumps under one source. Kept in sync with mngr_pi_lifecycle.ts.
36
+ _PI_USAGE_SOURCE_NAME = "pi-coding"
37
+
38
+
39
+ @hookimpl
40
+ def on_after_provisioning(agent: AgentInterface, host: OnlineHostInterface, mngr_ctx: MngrContext) -> None:
41
+ """Install the usage gate marker for pi agents so the lifecycle extension emits usage events.
42
+
43
+ Skips non-pi agents. Written to the agent state dir, which the extension
44
+ reads (``$MNGR_AGENT_STATE_DIR/pi_emit_usage``) once at startup.
45
+ """
46
+ if not isinstance(agent, PiCodingAgent):
47
+ return
48
+ state_dir = get_agent_state_dir_path(host.host_dir, agent.id)
49
+ host.write_file(state_dir / _USAGE_GATE_FILENAME, b"1")
50
+
51
+
52
+ @hookimpl
53
+ def aggregate_usage_source(
54
+ source_name: str,
55
+ agents_events: dict[str, list[UsageEvent]],
56
+ since_seconds: int,
57
+ now: int,
58
+ ) -> UsageSnapshot | None:
59
+ """Aggregate the pi usage source; decline every other source.
60
+
61
+ pi reports each assistant message's own cost/tokens (not a cumulative total),
62
+ so this is the session-incremental strategy. Returning None for non-pi sources
63
+ lets the firstresult hook fall through.
64
+ """
65
+ if source_name != _PI_USAGE_SOURCE_NAME:
66
+ return None
67
+ return aggregate_session_incremental(source_name, agents_events, since_seconds=since_seconds, now=now)
@@ -0,0 +1,53 @@
1
+ """Unit tests for mngr_pi_coding_usage.plugin (gate provisioning + hookimpl filter)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ from imbue.mngr.agents.base_agent import BaseAgent
8
+ from imbue.mngr.cli.testing import create_test_agent
9
+ from imbue.mngr.config.data_types import MngrContext
10
+ from imbue.mngr.hosts.host import get_agent_state_dir_path
11
+ from imbue.mngr.primitives import AgentId
12
+ from imbue.mngr.providers.local.instance import LocalProviderInstance
13
+ from imbue.mngr_pi_coding.plugin import PiCodingAgent
14
+ from imbue.mngr_pi_coding.plugin import PiCodingAgentConfig
15
+ from imbue.mngr_pi_coding_usage.plugin import _USAGE_GATE_FILENAME
16
+ from imbue.mngr_pi_coding_usage.plugin import on_after_provisioning
17
+
18
+
19
+ def _gate_path(host_dir: Path, agent_id: AgentId) -> Path:
20
+ return get_agent_state_dir_path(host_dir, agent_id) / _USAGE_GATE_FILENAME
21
+
22
+
23
+ def test_provisions_usage_gate_for_pi_agent(
24
+ local_provider: LocalProviderInstance, temp_mngr_ctx: MngrContext, tmp_path: Path
25
+ ) -> None:
26
+ work_dir = tmp_path / "work"
27
+ work_dir.mkdir()
28
+ agent = create_test_agent(
29
+ local_provider,
30
+ work_dir,
31
+ agent_config=PiCodingAgentConfig(),
32
+ agent_type=None,
33
+ extra_data=None,
34
+ agent_class=PiCodingAgent,
35
+ )
36
+ on_after_provisioning(agent=agent, host=agent.host, mngr_ctx=temp_mngr_ctx)
37
+ gate = _gate_path(agent.host.host_dir, agent.id)
38
+ assert gate.read_text() == "1"
39
+
40
+
41
+ def test_skips_non_pi_agent(local_provider: LocalProviderInstance, temp_mngr_ctx: MngrContext, tmp_path: Path) -> None:
42
+ work_dir = tmp_path / "work"
43
+ work_dir.mkdir()
44
+ agent = create_test_agent(
45
+ local_provider,
46
+ work_dir,
47
+ agent_config=None,
48
+ agent_type=None,
49
+ extra_data=None,
50
+ agent_class=BaseAgent,
51
+ )
52
+ on_after_provisioning(agent=agent, host=agent.host, mngr_ctx=temp_mngr_ctx)
53
+ assert not _gate_path(agent.host.host_dir, agent.id).exists()
@@ -0,0 +1,278 @@
1
+ from pathlib import Path
2
+
3
+ import pytest
4
+ from inline_snapshot import snapshot
5
+
6
+ from imbue.imbue_common.ratchet_testing import standard_ratchet_checks as rc
7
+
8
+ _DIR = Path(__file__).parent.parent.parent
9
+
10
+ pytestmark = pytest.mark.xdist_group(name="ratchets")
11
+
12
+
13
+ # --- Code safety ---
14
+
15
+
16
+ def test_prevent_todos() -> None:
17
+ rc.check_todos(_DIR, snapshot(0))
18
+
19
+
20
+ def test_prevent_exec() -> None:
21
+ rc.check_exec(_DIR, snapshot(0))
22
+
23
+
24
+ def test_prevent_eval() -> None:
25
+ rc.check_eval(_DIR, snapshot(0))
26
+
27
+
28
+ def test_prevent_while_true() -> None:
29
+ rc.check_while_true(_DIR, snapshot(0))
30
+
31
+
32
+ def test_prevent_time_sleep() -> None:
33
+ rc.check_time_sleep(_DIR, snapshot(0))
34
+
35
+
36
+ def test_prevent_global_keyword() -> None:
37
+ rc.check_global_keyword(_DIR, snapshot(0))
38
+
39
+
40
+ def test_prevent_bare_print() -> None:
41
+ rc.check_bare_print(_DIR, snapshot(0))
42
+
43
+
44
+ # --- Exception handling ---
45
+
46
+
47
+ def test_prevent_bare_except() -> None:
48
+ rc.check_bare_except(_DIR, snapshot(0))
49
+
50
+
51
+ def test_prevent_broad_exception_catch() -> None:
52
+ rc.check_broad_exception_catch(_DIR, snapshot(0))
53
+
54
+
55
+ def test_prevent_base_exception_catch() -> None:
56
+ rc.check_base_exception_catch(_DIR, snapshot(0))
57
+
58
+
59
+ def test_prevent_builtin_exception_raises() -> None:
60
+ rc.check_builtin_exception_raises(_DIR, snapshot(0))
61
+
62
+
63
+ def test_prevent_silent_decode_error_catches() -> None:
64
+ rc.check_silent_decode_error_catches(_DIR, snapshot(0))
65
+
66
+
67
+ # --- Import style ---
68
+
69
+
70
+ def test_prevent_inline_imports() -> None:
71
+ rc.check_inline_imports(_DIR, snapshot(0))
72
+
73
+
74
+ def test_prevent_relative_imports() -> None:
75
+ rc.check_relative_imports(_DIR, snapshot(0))
76
+
77
+
78
+ def test_prevent_import_datetime() -> None:
79
+ rc.check_import_datetime(_DIR, snapshot(0))
80
+
81
+
82
+ def test_prevent_importlib_import_module() -> None:
83
+ rc.check_importlib_import_module(_DIR, snapshot(0))
84
+
85
+
86
+ def test_prevent_getattr() -> None:
87
+ rc.check_getattr(_DIR, snapshot(0))
88
+
89
+
90
+ def test_prevent_setattr() -> None:
91
+ rc.check_setattr(_DIR, snapshot(0))
92
+
93
+
94
+ # --- Banned libraries and patterns ---
95
+
96
+
97
+ def test_prevent_asyncio_import() -> None:
98
+ rc.check_asyncio_import(_DIR, snapshot(0))
99
+
100
+
101
+ def test_prevent_pandas_import() -> None:
102
+ rc.check_pandas_import(_DIR, snapshot(0))
103
+
104
+
105
+ def test_prevent_dataclasses_import() -> None:
106
+ rc.check_dataclasses_import(_DIR, snapshot(0))
107
+
108
+
109
+ def test_prevent_namedtuple() -> None:
110
+ rc.check_namedtuple(_DIR, snapshot(0))
111
+
112
+
113
+ def test_prevent_yaml_usage() -> None:
114
+ rc.check_yaml_usage(_DIR, snapshot(0))
115
+
116
+
117
+ def test_prevent_functools_partial() -> None:
118
+ rc.check_functools_partial(_DIR, snapshot(0))
119
+
120
+
121
+ def test_prevent_exit_stack() -> None:
122
+ rc.check_exit_stack(_DIR, snapshot(0))
123
+
124
+
125
+ # --- Hardcoded paths ---
126
+
127
+
128
+ def test_prevent_hardcoded_claude_dir() -> None:
129
+ rc.check_hardcoded_claude_dir(_DIR, snapshot(0))
130
+
131
+
132
+ def test_prevent_hardcoded_guarded_binary() -> None:
133
+ rc.check_hardcoded_guarded_binary(_DIR, snapshot(0))
134
+
135
+
136
+ # --- Naming conventions ---
137
+
138
+
139
+ def test_prevent_num_prefix() -> None:
140
+ rc.check_num_prefix(_DIR, snapshot(0))
141
+
142
+
143
+ # --- Documentation ---
144
+
145
+
146
+ def test_prevent_trailing_comments() -> None:
147
+ rc.check_trailing_comments(_DIR, snapshot(0))
148
+
149
+
150
+ def test_prevent_init_docstrings() -> None:
151
+ rc.check_init_docstrings(_DIR, snapshot(0))
152
+
153
+
154
+ @pytest.mark.timeout(10)
155
+ def test_prevent_args_in_docstrings() -> None:
156
+ rc.check_args_in_docstrings(_DIR, snapshot(0))
157
+
158
+
159
+ @pytest.mark.timeout(10)
160
+ def test_prevent_returns_in_docstrings() -> None:
161
+ rc.check_returns_in_docstrings(_DIR, snapshot(0))
162
+
163
+
164
+ # --- Type safety ---
165
+
166
+
167
+ def test_prevent_literal_with_multiple_options() -> None:
168
+ rc.check_literal_with_multiple_options(_DIR, snapshot(0))
169
+
170
+
171
+ def test_prevent_bare_generic_types() -> None:
172
+ rc.check_bare_generic_types(_DIR, snapshot(0))
173
+
174
+
175
+ def test_prevent_typing_builtin_imports() -> None:
176
+ rc.check_typing_builtin_imports(_DIR, snapshot(0))
177
+
178
+
179
+ def test_prevent_short_uuid_ids() -> None:
180
+ rc.check_short_uuid_ids(_DIR, snapshot(0))
181
+
182
+
183
+ # --- Pydantic / models ---
184
+
185
+
186
+ def test_prevent_model_copy() -> None:
187
+ rc.check_model_copy(_DIR, snapshot(0))
188
+
189
+
190
+ # --- Logging ---
191
+
192
+
193
+ def test_prevent_fstring_logging() -> None:
194
+ rc.check_fstring_logging(_DIR, snapshot(0))
195
+
196
+
197
+ def test_prevent_click_echo() -> None:
198
+ rc.check_click_echo(_DIR, snapshot(0))
199
+
200
+
201
+ def test_prevent_logger_exception() -> None:
202
+ rc.check_logger_exception(_DIR, snapshot(0))
203
+
204
+
205
+ # --- Testing conventions ---
206
+
207
+
208
+ def test_prevent_unittest_mock_imports() -> None:
209
+ rc.check_unittest_mock_imports(_DIR, snapshot(0))
210
+
211
+
212
+ def test_prevent_monkeypatch_setattr() -> None:
213
+ rc.check_monkeypatch_setattr(_DIR, snapshot(0))
214
+
215
+
216
+ def test_prevent_test_container_classes() -> None:
217
+ rc.check_test_container_classes(_DIR, snapshot(0))
218
+
219
+
220
+ def test_prevent_pytest_mark_integration() -> None:
221
+ rc.check_pytest_mark_integration(_DIR, snapshot(0))
222
+
223
+
224
+ # --- Process management ---
225
+
226
+
227
+ def test_prevent_os_fork() -> None:
228
+ rc.check_os_fork(_DIR, snapshot(0))
229
+
230
+
231
+ def test_prevent_bare_urwid_tty_signal_keys() -> None:
232
+ rc.check_bare_urwid_tty_signal_keys(_DIR, snapshot(0))
233
+
234
+
235
+ def test_prevent_direct_subprocess() -> None:
236
+ rc.check_direct_subprocess(_DIR, snapshot(0))
237
+
238
+
239
+ def test_prevent_bare_tmux_targets() -> None:
240
+ rc.check_bare_tmux_targets(_DIR, snapshot(0))
241
+
242
+
243
+ # --- AST-based ratchets ---
244
+
245
+
246
+ def test_prevent_if_elif_without_else() -> None:
247
+ rc.check_if_elif_without_else(_DIR, snapshot(0))
248
+
249
+
250
+ def test_prevent_inline_functions() -> None:
251
+ rc.check_inline_functions(_DIR, snapshot(0))
252
+
253
+
254
+ def test_prevent_underscore_imports() -> None:
255
+ rc.check_underscore_imports(_DIR, snapshot(0))
256
+
257
+
258
+ def test_prevent_init_methods_in_non_exception_classes() -> None:
259
+ rc.check_init_methods_in_non_exception_classes(_DIR, snapshot(0))
260
+
261
+
262
+ def test_prevent_cast_usage() -> None:
263
+ rc.check_cast_usage(_DIR, snapshot(0))
264
+
265
+
266
+ def test_prevent_assert_isinstance() -> None:
267
+ rc.check_assert_isinstance(_DIR, snapshot(0))
268
+
269
+
270
+ def test_prevent_per_file_host_upload() -> None:
271
+ rc.check_per_file_host_upload(_DIR, snapshot(0))
272
+
273
+
274
+ # --- Project-level checks ---
275
+
276
+
277
+ def test_prevent_code_in_init_files() -> None:
278
+ rc.check_code_in_init_files(_DIR, snapshot(0))
@@ -0,0 +1,68 @@
1
+ [project]
2
+ name = "imbue-mngr-pi-coding-usage"
3
+ version = "0.1.0"
4
+ description = "pi usage data provider for `mngr usage` (reader + writer gate; the per-message writer lives in mngr_pi_coding's lifecycle extension)"
5
+ readme = "README.md"
6
+ requires-python = ">=3.12"
7
+ dependencies = [
8
+ "imbue-mngr==0.2.16",
9
+ "imbue-mngr-pi-coding==0.1.13",
10
+ "imbue-mngr-usage==0.1.5",
11
+ ]
12
+
13
+ [project.entry-points.mngr]
14
+ pi_coding_usage = "imbue.mngr_pi_coding_usage.plugin"
15
+
16
+ [build-system]
17
+ requires = ["hatchling"]
18
+ build-backend = "hatchling.build"
19
+
20
+ [tool.hatch.build.targets.wheel]
21
+ packages = ["imbue"]
22
+ exclude = ["*_test.py", "test_*.py", "**/conftest.py", "**/testing.py"]
23
+
24
+ [tool.uv.sources]
25
+ imbue-mngr = { workspace = true }
26
+ imbue-mngr-pi-coding = { workspace = true }
27
+ imbue-mngr-usage = { workspace = true }
28
+
29
+ [tool.pytest.ini_options]
30
+ timeout_func_only = true
31
+ junit_family = "xunit1"
32
+ testpaths = ["."]
33
+ addopts = [
34
+ "--cov=imbue.mngr_pi_coding_usage",
35
+ "--cov-report=term-missing",
36
+ "--cov-report=xml",
37
+ "--durations=20",
38
+ "--timeout=10",
39
+ "--timeout-method=signal",
40
+ "--slow-tests-to-file",
41
+ "--coverage-to-file",
42
+ ]
43
+
44
+ [tool.coverage.run]
45
+ parallel = true
46
+ concurrency = ["multiprocessing", "thread"]
47
+ omit = [
48
+ "*_test.py",
49
+ "test_*.py",
50
+ "*/tests/*",
51
+ "*/conftest.py",
52
+ "*/utils/testing.py",
53
+ ]
54
+
55
+ [tool.inline-snapshot]
56
+ default-flags=["report"]
57
+ default-flags-tui=["report"]
58
+ default-flags-ide=["report"]
59
+ format-command="uv run ruff format --force-exclude --config pyproject.toml --stdin-filename {filename}"
60
+
61
+ [tool.pyright]
62
+ venvPath = "../.."
63
+ venv = ".venv"
64
+ pythonVersion = "3.12"
65
+ strict = ["**/*.py"]
66
+
67
+ [tool.coverage.report]
68
+ fail_under = 95