Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,40 @@ This project follows a Keep a Changelog-style format and uses version tags for r

## [Unreleased]

## [1.9.8] - 2026-08-13

**Skip 1.9.1 through 1.9.7 — install this one instead.** Those seven were cut in
rapid succession (six of them inside forty minutes) while the release pipeline
itself was being stabilized: each fixed the flow that had just shipped the one
before it, and none carried a user-visible change worth a release of its own.
1.9.8 supersedes all of them and is the first release in that line with a real
fix for users. `CLAUDE.md` now gates releases on user-visible change so this
does not recur; the intermediate tags stay published because PyPI artifacts
already point at them.

### Fixed

- **Narrow grid columns no longer clip the tail off metric rows.** At
`_GRID_MIN_WIDTH` (96 cols) each box gets ~44 content columns, and the longest
rows are wider than that: the P-CPU summary lost the `%` off
`avg 41% · max 41`, and every residency row lost `high37` outright (that one
overflowed up to ~110 cols). The rows are now width-adaptive instead of
hard-clipped — carried open since 1.5.0.
- Rolling context degrades in three tiers, each row choosing the widest that
fits its own measured width: `avg N% · max N%` → `⌀N ▲N%` → `▲N%` →
nothing. Applies to every labelled row (P-CPU/E-CPU, GPU, ANE, RAM, Mem BW,
the four I/O rates, CPU/GPU/Package power). The reading itself is never
shortened to make room for its annotation.
- Residency rows size their density bar to what the breakdown leaves (16 chars
down to 8, dropped below that), so the four percentages always survive. The
bar is budgeted against the widest the breakdown can ever be, so it does not
breathe a column every time a percentage crosses 10.
- The fit is against each row's high-water widths, not the current frame's, so
a row on a tier boundary holds its form instead of flipping between the
spelled-out and compact shapes as digits come and go (`avg 8%` → `avg 10%`,
`987MHz` → `1987MHz`).
- `?` help and `docs/SPEC-system.md` §6.3 document the ⌀/▲ compact form.

## [1.9.7] - 2026-08-13

### Fixed
Expand Down
28 changes: 26 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,34 @@ This file is the single source of truth for repository guidelines, used by Claud

**RULE — cutting a release REQUIRES a GitHub Release, not just a tag.** A release is cut with `scripts/tag_release.sh`, which pushes the git tag **and** creates the GitHub Release for it via `gh release create` (release notes pulled from the matching `CHANGELOG.md` section). A bare `git tag` + push is **not** a release: it never surfaces on the `/releases` page or as `Latest`, and leaves the Releases history silently behind the code. Never cut a release by hand-tagging alone — if the `gh release create` step fails, finish it manually (see playbooks below) before calling the release cut done.

### When to cut a release

**A release needs something that changed for users.** Repo-internal churn — CI
workflows, the release scripts themselves, docs, the cover page, agent skills,
test-only edits — lands under `CHANGELOG.md` `[Unreleased]` and ships with the
next release that carries a user-visible change. It is never a release on its
own.

**One release per change set, and version numbers are permanent.** PyPI burns a
version string on first upload: it can be yanked but never reused, so every
throwaway release permanently consumes a number and adds a row to `/releases`
that users have to reason about. On 2026-08-13, six releases (1.9.2–1.9.7)
landed inside forty minutes, every one of them a fix to the release pipeline
that had just shipped the previous one. That is the failure mode this rule
exists to stop: when the release flow itself is what is broken, fix it on a
branch and cut **one** release at the end.

The only reason to re-tag the same day is a release that is **broken for users**
— install fails, import fails, the TUI crashes on launch. Anything less waits.

If several unreleased changes have accumulated, a release PR rolls them into one
dated section; do not manufacture one section per merged PR.

### Release steps

```bash
# 1. Bump version and changelog via a PR (never commit the bump to main)
# 1. Bump version and changelog via a RELEASE PR (never commit the bump to main).
# Only do this when "When to cut a release" above says it is warranted.
git switch -c release-vX.Y.Z
# edit pyproject.toml ([project].version) and CHANGELOG.md (move Unreleased -> new version + date)
git commit -am "Release vX.Y.Z" && git push -u origin release-vX.Y.Z
Expand Down Expand Up @@ -236,7 +260,7 @@ a mount point, not a fake); faking the data or the logic under test is not.

## Commit & Pull Request Guidelines
- **Branch from `main`; PR strictly into `main`.** Every branch forks from `main` and targets `main`. **Never fork a feature branch off another feature branch** (no stacked PRs): if you need work that is still on an unmerged branch, wait for it to merge and re-branch from `main`. This holds especially for CI/CD and release changes — they land via a single PR into `main`, never a chained branch.
- **Bump the version in every PR.** Each PR updates `pyproject.toml` version + moves `CHANGELOG.md` `[Unreleased]` into a new dated section, in the same PR: **patch bump by default, minor only for a milestone PR** (major reserved for breaking API/CLI changes). Tagging (`scripts/tag_release.sh`) is a separate step after merge; publishing to PyPI is a third, manual step after that — see the Release Process section above.
- **A normal PR does NOT bump the version.** It adds its entry under `CHANGELOG.md` `[Unreleased]` and leaves `pyproject.toml` alone. Only a **release PR** moves `[Unreleased]` into a dated version section and bumps `[project].version` — **patch by default, minor for a milestone** (major reserved for breaking API/CLI changes) — and a release PR is the only kind followed by `scripts/tag_release.sh`. See **When to cut a release** in the Release Process section for when a release PR is warranted; publishing to PyPI is a third, manual step after the tag — see the Release Process section above.
- Use concise, imperative commit subjects (as seen in history), e.g. `Add support for M1 Ultra` or `actop/utils.py: add bandwidth of M2`.
- Keep commits scoped to one logical change.
- Before every commit and before every push, always run:
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ and thermals — plus a **Python API** (`Monitor` / `Profiler`, `to_pandas()`) s
can instrument your *own* local LLM / MLX / CoreML inference and training runs with
SoC-accurate power and energy context.

![actop live during a llama.cpp inference run: the single-column stack layout switching chart glyphs and revealing the process panel, with live E-CPU/P-CPU/GPU/ANE utilization, per-core frequency, memory bandwidth, power charts, and a watt-attributed PWR column on Apple Silicon](images/actop-demo.gif)
![actop live during a llama.cpp inference run: the two-column grid layout switching chart glyphs, revealing the per-core panels and the process table, and cycling themes, with live E-CPU/P-CPU/GPU/ANE utilization, per-core frequency, memory bandwidth, network and disk I/O, power charts, and a watt-attributed PWR column on Apple Silicon](images/actop-demo.gif)

*Above: actop live under a local LLM run — the `stack` layout with braille then block sparklines, then the process panel (`t`) with its watt-attributed `PWR` column. Below: the default two-column `grid` layout (`l` cycles between them).*
*Above: actop live under a local LLM run — the default `grid` layout with braille then block sparklines, the per-core panels (`c`), the process table (`p`) with its watt-attributed `PWR` column, and a pass through the themes (`t`). Below: the same grid layout as a still (`l` cycles to the single-column `stack`).*

![actop dashboard in the default two-column grid layout: E-CPU/P-CPU/GPU/ANE utilization, per-core frequency, memory bandwidth, and power charts on Apple Silicon](images/actop.png)

Expand Down
4 changes: 3 additions & 1 deletion actop/tui/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,9 @@ def _process_display_name(command, max_len=24):
account for vs total package CPU watts (a partition of it).

avg · max Rolling average (over the --avg window) and session peak,
shown next to each live reading.
shown next to each live reading. A column too narrow for the
spelled-out form compacts it to ⌀avg ▲peak, then to ▲peak
alone — the reading itself is never shortened.

[b]Status line[/b]

Expand Down
Loading