pi-quiver 3.0.0

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.
package/CHANGELOG.md ADDED
@@ -0,0 +1,92 @@
1
+ # Changelog
2
+
3
+ Format follows sibling pi packages (e.g. [`pi-context-prune`](https://github.com/jjuraszek/pi-context-prune/blob/main/CHANGELOG.md)):
4
+ one entry per `vX.Y.Z` tag, newest first, terse bullets, dated.
5
+
6
+ Published to npm as `pi-quiver` (`pi install npm:pi-quiver`). Pushing a
7
+ `vX.Y.Z` tag triggers `.github/workflows/release.yml`, which publishes to npm
8
+ via OIDC trusted publishing. The release helper at
9
+ `.agents/skills/release/scripts/release.sh` cuts the tag; CI publishes.
10
+
11
+ ## v3.0.0 - 2026-07-02
12
+
13
+ - **Distribution moved from git-tag pins to npm, and the package renamed
14
+ `pi-essentials` -> `pi-quiver`.** Installed with `pi install npm:pi-quiver`
15
+ instead of `git:github.com/jjuraszek/pi-essentials@<tag>`. This is a breaking
16
+ change to the install mechanism and package name only; extension behavior is
17
+ unchanged. Existing git-tag-pin consumers migrate their `settings.json` entry
18
+ to `npm:pi-quiver@<version>` (stale `pi-essentials` pins are flagged by
19
+ `release.sh sync-presets`).
20
+ - **Tag-triggered CI publish.** New `.github/workflows/release.yml` publishes
21
+ `npm publish --provenance --access public` via OIDC trusted publishing when a
22
+ `v[0-9]+.[0-9]+.[0-9]+` tag is pushed, gated on `tag == package.json` and
23
+ `npm run test:all`. New `.github/workflows/test.yml` runs unit tests +
24
+ typecheck on ubuntu + windows for every push and PR.
25
+ - **`release.sh` rewritten** to the shared pi-* skeleton (propose / current /
26
+ patch / minor / major / verify / sync-presets); only its CONFIG header is
27
+ repo-specific. It bumps + tags + pushes and lets CI publish; it never runs
28
+ `npm publish`. `sync-presets` reports old git-tag pins for manual migration
29
+ and bumps same-form `npm:` pins under `--apply`.
30
+ - **`package.json`:** added `author`, `engines.node >=20`, a `files` allowlist
31
+ (ships the runtime `.ts`, `scripts/pdf_to_md.py`, `types/`, docs), expanded
32
+ `keywords`, `peerDependenciesMeta` (peers optional), a `test:all` script, and
33
+ `devDependencies` for the peers + type packages so CI runs offline of a pi
34
+ host. `typecheck` now runs via `npx tsc` (was `bun x tsc`). Bundled runtime
35
+ deps (`jsdom`, `@mozilla/readability`, `turndown`, `turndown-plugin-gfm`,
36
+ `unpdf`) stay in `dependencies` and ship in the tarball.
37
+ - **Docs:** `README`, `AGENTS.md`, the release skill, and `/release` prompt
38
+ updated to the npm model; added README Development section. Removed the stale
39
+ `.npmignore` (superseded by the `files` allowlist).
40
+
41
+ ## v2.0.2 - 2026-06-28
42
+
43
+ - **`session-name`: fix auto-naming silently never running.** Two bugs compounded into zero auto-named sessions:
44
+ - **Env-key auth was rejected.** `generateName` bailed on `!auth.apiKey`, but `ModelRegistry.getApiKeyAndHeaders` resolves keys with `includeFallback: false` — so a key that lives only in the environment (e.g. `ANTHROPIC_API_KEY`, the common case with no stored provider credential) returns `ok: true` with `apiKey: undefined`. The bail discarded that path even though `complete()` resolves the env key itself via `withEnvApiKey`/`getEnvApiKey`. Now only bails on `!auth.ok`, and forwards `auth.env`.
45
+ - **Fragile `complete` import.** `complete` is re-exported from the pi-ai package index in older builds but only from the `/compat` subpath in newer ones; the static `import { complete }` aborted extension load entirely (taking the manual command with it) whenever the installed pi-ai used the other layout. Now resolved lazily at call time, trying the index then `/compat`.
46
+
47
+ ## v2.0.1 — 2026-06-18
48
+
49
+ - **`session-name`: keep the Ghostty tab in sync with the session name.** Pi owns the OS terminal title (OSC 0, `pi - <name> - <cwd>`) and rewrites it on every name change and session switch, clobbering our short OSC-2 tab label. The extension now re-asserts the tab label at the start of every turn (`turn_start`) — the only hook that fires after pi's writer on a session swap — so the tab and the session name move together. Self-heals when the name is changed outside the extension (re-derives the label from the new name) and reflects names on reload as well as resume. No behavior when OFF (default).
50
+
51
+ ## v2.0.0 — 2026-06-16
52
+
53
+ - **New `session-name` extension.** Names work sessions; **OFF by default**.
54
+ - **Manual `/session-name [name]`** sets or prints the session name; always available regardless of config. A manual name suppresses later auto-naming.
55
+ - **Automatic naming (opt-in):** after the first agent turn, asks the current model for a 3-6 word session title + 1-4 word tab label and applies both, once per session, never overwriting an existing name. Also re-applies the tab label when a named session is resumed.
56
+ - **Ghostty tab rename** via OSC 2, fired only when the active terminal is really Ghostty (`TERM_PROGRAM=ghostty` / `TERM=xterm-ghostty` / `GHOSTTY_*` dir env) and stdout is a TTY.
57
+ - **Config `sessionAutoName`** in `settings.json` (`{ "enabled": bool, "ghosttyTab": bool }` or boolean shorthand); project `.pi/settings.json` overrides the global layer. The global `settings.json` is located via pi's `getAgentDir()` (honours `PI_CODING_AGENT_DIR`), so it resolves correctly when installed as a git-tag-pinned package — replacing the previous `import.meta.url` heuristic that only worked for in-tree `extensions/` files.
58
+ - **Cost:** when enabled, one extra short LLM call per session (low reasoning effort), once. When OFF (default): no model calls, no terminal writes.
59
+ - **New `sword-header` extension.** Replaces the TUI startup logo with a theme-colored ASCII greatsword; **OFF by default**, installed only when enabled via `settings.json` (`swordHeader: true` or `{ "enabled": true }`). `/builtin-header` restores the built-in header at runtime. TUI-only (no-op under `-p`).
60
+ - **New runtime dependency:** `@earendil-works/pi-ai` (peer; matches the host pi runtime).
61
+ - **New shared module `extension-config.ts`:** `getAgentDir()`-based global + project `.pi/settings.json` layering (`resolveConfig`), used by both `session-name` and `sword-header`.
62
+
63
+ ## v1.0.0 — 2026-06-15
64
+
65
+ - **New `doc_to_md` extension.** Converts a local PDF/DOCX/PPTX to Markdown.
66
+ - **Primary engine `pymupdf4llm`** (high fidelity) run as an arms-length subprocess via `uv run --with pymupdf4llm==<pin> --python 3.14` — no project venv, wheel fetched into uv's cache on first use, Python pinned to 3.14. Warmed once per process (generous install budget, then a short per-document budget).
67
+ - **Fallback engine `unpdf`** (pure JS) when `uv` is absent, the warm probe fails, or a conversion times out. Degraded output is marked and carries a `Fallback-Reason:`.
68
+ - **DOCX/PPTX** convert to PDF via headless LibreOffice (`soffice`, isolated per-call profile) then through the same PDF pipeline; missing `soffice` errors office inputs only. Spreadsheets/other formats out of scope.
69
+ - **Size-gated** like `fetch` (≤ 32 KB and ≤ 1000 lines inline, else spill to `${TMPDIR}/pi-doc-to-md/` with a preview).
70
+ - **Config via env vars:** `PI_DOC_TO_MD_PYMUPDF_VERSION` (default `1.27.2.3`), `PI_DOC_TO_MD_WARM_TIMEOUT_MS` (120000), `PI_DOC_TO_MD_CONVERT_TIMEOUT_MS` (60000), `PI_DOC_TO_MD_SOFFICE_TIMEOUT_MS` (120000).
71
+ - **AGPL note:** PyMuPDF/pymupdf4llm are AGPL-3.0; no code is shipped (uv fetches the wheel at runtime) and it runs as a separate subprocess, keeping pi-quiver MIT.
72
+ - **`fetch`:** map OOXML content types (`...wordprocessingml.document`, `...presentationml.presentation`) to `.docx`/`.pptx` so fetched office docs are saved with the correct extension for the `fetch` → `doc_to_md` chain.
73
+ - **New runtime dependency:** `unpdf`. Optional system binaries `uv` and `soffice` are detected at runtime.
74
+
75
+ ## v0.2.0 — 2026-06-03
76
+
77
+ - **Content routing rewrite.** `fetch` now classifies responses by type and routes them:
78
+ - **HTML → Markdown:** Mozilla Readability extracts main content (strips nav/boilerplate), Turndown converts to Markdown with GFM plugin (pipe tables, fenced code, ATX headings). Page title becomes `#` heading.
79
+ - **Binary (images, PDFs, archives, fonts, audio/video):** Streamed untouched to `${TMPDIR}/pi-fetch/` without decoding. NUL-byte sniff in first ≤64 KB detects mislabeled payloads. Download cap raised to **50 MB**. Returns file path only, no preview.
80
+ - **Text / JSON:** Pretty-printed (JSON: 2-space indent). Inline gate tightened to **≤ 32 KB and ≤ 1000 lines**; larger content spills to file with preview + grep-able Markdown headings. Parsable download cap remains **1 MB**.
81
+ - **Truncation notes:** Parsable content over 1 MB notes truncation; binary over 50 MB notes truncation.
82
+ - **Parameters:** `raw=true` skips HTML→Markdown and JSON pretty-printing (still subject to size gate).
83
+ - **New runtime dependencies:** `jsdom`, `@mozilla/readability`, `turndown`, `turndown-plugin-gfm`. Pi installs them automatically via git tag pin.
84
+
85
+ ## v0.1.0 — 2026-06-02
86
+
87
+ - Initial release. Extracts the personal `fetch` extension out of the per-profile
88
+ `~/.pi/agent*/extensions/` dirs into a versioned, tag-pinned package.
89
+ - **`fetch` context hygiene:** bodies over 50 KB or 2000 lines are written to
90
+ `${TMPDIR}/pi-fetch/` and returned as a preview + file path instead of being
91
+ inlined whole. Small bodies are returned inline unchanged. Download stays
92
+ capped at 1 MB. Prevents a single fetch from flooding the context window.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jacek Juraszek
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,190 @@
1
+ # pi-quiver
2
+
3
+ A small pack of [Pi coding-agent](https://github.com/badlogic/pi-mono) extensions I keep across every pi profile. First-party-quality tools, published to npm like sibling packages ([`pi-cohort`](https://github.com/jjuraszek/pi-cohort), [`pi-superpowers`](https://github.com/jjuraszek/pi-superpowers)).
4
+
5
+ ## Extensions
6
+
7
+ | Extension | Tool | What it does |
8
+ |---|---|---|
9
+ | `fetch.ts` | `fetch` | Retrieve URLs over HTTP(S). HTML → Markdown (main-content extraction, stripped boilerplate). Binary content saved untouched to a temp file. **Context-safe:** output over 32 KB or 1000 lines is written to a temp file with a preview + file path. Prevents a single fetch from flooding the context window. |
10
+ | `doc_to_md.ts` | `doc_to_md` | Convert a local PDF/DOCX/PPTX to Markdown. High-fidelity via `pymupdf4llm` (run through `uv`, fetched on first use); degraded pure-JS fallback (`unpdf`) when `uv`/Python is unavailable or conversion times out. DOCX/PPTX convert via LibreOffice (`soffice`) to PDF first. Same 32 KB / 1000-line size gate as `fetch`. |
11
+ | `session-name.ts` | `/session-name` | Name work sessions. Manual `/session-name [name]` always works. **OFF by default:** when opted in via `settings.json`, after the first agent turn it asks the current model for a concise session name + short tab label and applies them, and renames the **Ghostty** tab via OSC 2 (only when the active terminal is really Ghostty), re-asserting it each turn so the tab tracks the session name. |
12
+ | `sword-header.ts` | `/builtin-header` | Replace the TUI startup logo with a theme-colored ASCII greatsword (hilt = accent, blade = text). **OFF by default:** only installs the header when opted in via `settings.json`. `/builtin-header` restores the built-in header at runtime. |
13
+
14
+ ### fetch — content routing & context hygiene
15
+
16
+ `fetch` is the main way an agent pulls external bytes into context. This extension routes responses by type to keep context tight:
17
+
18
+ **HTML → Markdown:**
19
+ - Mozilla Readability extracts main content, strips navigation/chrome/boilerplate
20
+ - Turndown converts to Markdown with GFM support (pipe tables, fenced code blocks, ATX headings)
21
+ - Page title becomes a top-level `#` heading
22
+ - Download cap: **1 MB**
23
+
24
+ **Binary (images, PDFs, archives, fonts, audio/video) → temp file:**
25
+ - Streamed untouched to `${TMPDIR}/pi-fetch/<stamp>-<host>-<hash>.<ext>` without decoding
26
+ - Detection: content-type check + NUL-byte sniff in first ≤64 KB (catches mislabeled payloads)
27
+ - Returns: status, content-type, size, file path — **no preview**
28
+ - Download cap: **50 MB**
29
+
30
+ **Text / Markdown / JSON size gate:**
31
+ - Inline when **≤ 32 KB AND ≤ 1000 lines** (converted output size)
32
+ - Otherwise **spills to file** with:
33
+ - HTTP status, content-type, charset, byte/line counts
34
+ - File path (`Saved-To:`)
35
+ - 60-line preview
36
+ - Instruction to `grep` (Markdown is grep-able by heading: `^#`) or `read` slices
37
+
38
+ **JSON:** Pretty-printed with 2-space indent before the gate.
39
+
40
+ **Parameters:**
41
+ - `raw=true`: Skip HTML→Markdown and JSON pretty-printing; return decoded body as-is (still subject to the size gate).
42
+
43
+ **Truncation:** Parsable content over 1 MB is truncated with a `(truncated to 1MB)` note; binary over 50 MB notes `(truncated to 50MB)`.
44
+
45
+ **Runtime dependencies:** `jsdom`, `@mozilla/readability`, `turndown`, `turndown-plugin-gfm`. Shipped in the npm package and installed automatically on `pi install` - no manual setup needed.
46
+
47
+ ### doc_to_md — local document → Markdown
48
+
49
+ `doc_to_md` takes a **local file path** (`.pdf`, `.docx`, `.pptx`) and returns Markdown. For remote documents, `fetch` the URL first (it saves binaries to a temp path), then pass that path here.
50
+
51
+ **Two engines, auto-selected:**
52
+
53
+ - **Primary — `pymupdf4llm`** (high fidelity: headings, tables, reading order). Runs as an arms-length subprocess via `uv run --with pymupdf4llm==<pin> --python 3.14`. `uv` fetches the wheel into its own cache on first use (one-time download); Python 3.14 is fixed. Warmed once per process: the first call probes/installs (generous budget), later calls reuse the warm cache with a shorter per-document budget.
54
+ - **Fallback — `unpdf`** (pure JS, bundled PDF.js). Used when `uv` is not on `PATH`, the warm probe fails, or a conversion times out. Output is plain text with page breaks — **no faithful tables/headings**. Degraded results are marked in the output (`[Note: degraded extraction via unpdf ...]`) and carry a `Fallback-Reason:` line.
55
+
56
+ **Office documents (`.docx`, `.pptx`):** converted to PDF by headless LibreOffice (`soffice`, isolated per-call profile), then fed through the same PDF pipeline. `soffice` must be on `PATH` for office inputs — otherwise the tool errors (there is no JS fallback for office→PDF). Spreadsheets and other formats are out of scope (spreadsheets paginate badly via PDF).
57
+
58
+ **Size gate:** identical to `fetch` — Markdown ≤ 32 KB and ≤ 1000 lines is inlined; larger output spills to `${TMPDIR}/pi-doc-to-md/<stamp>-<basename>-<hash>.md` with a 60-line preview + a grep/read-slice hint.
59
+
60
+ **Configuration (environment variables):**
61
+
62
+ | Variable | Default | Meaning |
63
+ |---|---|---|
64
+ | `PI_DOC_TO_MD_PYMUPDF_VERSION` | `1.27.2.3` | `pymupdf4llm` version pin passed to `uv --with` (digits/dots only) |
65
+ | `PI_DOC_TO_MD_WARM_TIMEOUT_MS` | `120000` | Warm/install call budget — covers the cold wheel (+ managed Python) download |
66
+ | `PI_DOC_TO_MD_CONVERT_TIMEOUT_MS` | `60000` | Per-document conversion budget (also bounds the `unpdf` fallback) |
67
+ | `PI_DOC_TO_MD_SOFFICE_TIMEOUT_MS` | `120000` | LibreOffice `.docx`/`.pptx` → PDF budget |
68
+
69
+ Python is pinned to **3.14** and is not configurable.
70
+
71
+ **Runtime dependencies:** `unpdf` (shipped in the npm package, installed automatically on `pi install`). `uv` and LibreOffice (`soffice`) are optional system binaries detected at runtime: without `uv`, PDFs still convert via the `unpdf` fallback; without `soffice`, office inputs error while PDFs are unaffected.
72
+
73
+ **Licensing note:** `pymupdf4llm`/PyMuPDF are **AGPL-3.0**. This package ships none of their code — `uv` downloads the wheel from PyPI onto your machine at runtime, and it runs as a **separate subprocess** (never imported or linked into this TypeScript). The arms-length process boundary keeps pi-quiver' MIT license intact; the AGPL governs PyMuPDF itself, whose source is public. This holds only while the boundary stays subprocess-only (no vendoring/importing the wheel).
74
+
75
+ ### session-name — manual + opt-in automatic session naming
76
+
77
+ Names work sessions so the session selector (and optionally the Ghostty tab) shows what each one is about.
78
+
79
+ **Behaviors:**
80
+
81
+ - **Manual `/session-name [name]`** - set the session name, or, with no argument, print the current one. Always available, regardless of config. A manual name wins: it suppresses later auto-naming for the session.
82
+ - **Automatic naming (opt-in).** After the first agent turn completes, if no name is set yet, the extension asks the **current model** for a 3-6 word session title plus a 1-4 word tab label and applies both. It only runs once per session and never overwrites an existing name.
83
+ - **Resume reflection (opt-in).** When a session that already carries a name is loaded/resumed/reloaded, its tab label is re-applied so the Ghostty tab matches.
84
+ - **Per-turn re-assert (opt-in).** The tab is re-pinned to the session name at the start of every turn. Pi owns the OS terminal title (OSC 0, `pi - <name> - <cwd>`) and overwrites it on every name change and session switch; the re-assert is the only hook that fires *after* pi's writer on a session swap, so the Ghostty tab and the session name stay in sync instead of drifting. It self-heals: if the name was changed outside this extension, the tab label is re-derived from the new name.
85
+ - **Ghostty tab rename.** The short label is written via OSC 2 (`ESC ] 2 ; <label> BEL`) **only when the active terminal is really Ghostty** (`TERM_PROGRAM=ghostty`, `TERM=xterm-ghostty`, or a `GHOSTTY_*` dir env) **and** stdout is a TTY. Other terminals are never touched. Auto-naming keeps its curated short label; re-derived labels (resume/external rename) are the first words of the session name.
86
+
87
+ **OFF by default.** All automatic behavior (auto-naming + resume reflection) is inert until explicitly enabled. The manual command is unaffected.
88
+
89
+ **Configuration** (`settings.json`, **project `.pi/settings.json` overrides the global agent-dir `settings.json`**). The global path is resolved via pi's own `getAgentDir()` (honours `PI_CODING_AGENT_DIR`, else `~/.pi/agent`), so it stays correct however this package is installed.
90
+
91
+ ```jsonc
92
+ {
93
+ // full form, defaults shown
94
+ "sessionAutoName": { "enabled": false, "ghosttyTab": true }
95
+ }
96
+ ```
97
+
98
+ | Key | Default | Meaning |
99
+ |---|---|---|
100
+ | `enabled` | `false` | Master switch for automatic naming + resume reflection. |
101
+ | `ghosttyTab` | `true` | Whether to rename the Ghostty tab (only ever fires when the terminal is actually Ghostty). |
102
+
103
+ Boolean shorthand: `"sessionAutoName": true` enables everything (equivalent to `{ "enabled": true, "ghosttyTab": true }`); `false` disables everything.
104
+
105
+ **Cost note:** when enabled, automatic naming makes **one extra short LLM call** per session (low reasoning effort, current model, a few-thousand-char conversation digest), once, after the first turn. When OFF (the default) it makes **no** model calls and writes **nothing** to the terminal.
106
+
107
+ **Runtime dependency:** `@earendil-works/pi-ai` (the unified LLM API provided by the pi runtime; a peer dependency, no separate install).
108
+
109
+ ### sword-header — themed ASCII startup header
110
+
111
+ Replaces pi's built-in startup logo with a hero's greatsword (Michael J. Penick longsword, asciiart.eu). The ASCII art is verbatim; only the coloring is ours - hilt/grip/pommel use the `accent` token, the blade uses `text`, so it tracks whatever theme is active.
112
+
113
+ **Behaviors:**
114
+
115
+ - **TUI only.** Installs a custom header on `session_start` when `ctx.mode === "tui"`. In print/non-interactive mode (`-p`) it does nothing.
116
+ - **`/builtin-header`** restores the built-in pi header at runtime (always available).
117
+
118
+ **OFF by default.** The header is only installed when explicitly enabled via `settings.json`.
119
+
120
+ **Configuration** (`settings.json`, project `.pi/settings.json` overrides the global agent-dir layer; same resolution as `session-name`):
121
+
122
+ ```jsonc
123
+ {
124
+ "swordHeader": false // default; true installs the header
125
+ // object form also accepted: "swordHeader": { "enabled": true }
126
+ }
127
+ ```
128
+
129
+ ## Install
130
+
131
+ Published to npm as the unscoped `pi-quiver` package.
132
+
133
+ **User scope** (all repos under your pi profile):
134
+
135
+ ```bash
136
+ pi install npm:pi-quiver
137
+ ```
138
+
139
+ **Project scope** (current repo only, committable via `.pi/settings.json`):
140
+
141
+ ```bash
142
+ pi install -l npm:pi-quiver
143
+ ```
144
+
145
+ **Try without installing**:
146
+
147
+ ```bash
148
+ pi -e npm:pi-quiver
149
+ ```
150
+
151
+ **From a local checkout** (for hacking on the extensions):
152
+
153
+ ```bash
154
+ git clone git@github.com:jjuraszek/pi-quiver.git ~/repos/pi-quiver
155
+ pi -e ~/repos/pi-quiver/fetch.ts
156
+ ```
157
+
158
+ ## Development
159
+
160
+ Deps are peers (`@earendil-works/*`, `@sinclair/typebox`) plus the bundled
161
+ runtime deps; install them transiently and run the full check:
162
+
163
+ ```bash
164
+ npm install
165
+ npm run test:all # node --test *.test.ts + tsc --noEmit typecheck
166
+ ```
167
+
168
+ `npm test` runs the unit tests alone; `npm run typecheck` runs the type pass.
169
+ Both run in CI on ubuntu + windows (`.github/workflows/test.yml`).
170
+
171
+ ## Release
172
+
173
+ Published to npm by CI. Pushing a `vX.Y.Z` tag triggers
174
+ `.github/workflows/release.yml`, which gates on `tag == package.json`, runs
175
+ `npm run test:all`, and publishes with `npm publish --provenance --access
176
+ public` via OIDC trusted publishing. **Never run `npm publish` by hand.**
177
+
178
+ Cut a release with the helper script (also exposed as the `/release` prompt +
179
+ the `release` skill at `.agents/skills/release/`):
180
+
181
+ ```bash
182
+ bash .agents/skills/release/scripts/release.sh propose # suggest a level
183
+ bash .agents/skills/release/scripts/release.sh patch # or minor / major
184
+ bash .agents/skills/release/scripts/release.sh --dry-run patch
185
+ ```
186
+
187
+ It bumps `package.json`, commits `Release <version>`, runs the tests, creates
188
+ and pushes the `vX.Y.Z` tag, then monitors the publish. See
189
+ `.agents/skills/release/SKILL.md` for the full flow (`sync-presets` migrates
190
+ old git-tag pins to `npm:pi-quiver@<version>`).