@groeponline/pi-wishcraft 1.17.0 → 1.23.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/AGENTS.md +42 -22
- package/CHANGELOG.md +58 -0
- package/CONTRIBUTING.md +12 -1
- package/RELEASE.md +142 -49
- package/bash-mode/completion.ts +2 -2
- package/build-stamp.json +1 -1
- package/docs/configuration.md +6 -1
- package/docs/design/regression-testing.md +1 -1
- package/docs/testing.md +81 -0
- package/package.json +17 -10
- package/src/config/settings-registry.ts +2 -1
- package/src/config/structural-preset-table.ts +0 -1
- package/src/extension/commands/commands.ts +0 -1
- package/src/extension/core/peer-guard.ts +5 -0
- package/src/extension/core/segment-context.ts +139 -44
- package/src/extension/core/state.ts +1 -0
- package/src/extension/core/types.ts +5 -0
- package/src/extension/queue/queue-integration.ts +20 -2
- package/src/extension/session/session-lifecycle.ts +24 -3
- package/src/extension/skills/inline-invocation.ts +1 -1
- package/src/extension/ui/custom-editor.ts +1 -5
- package/src/extension/ui/deck/config-diagnostic-lines.ts +0 -6
- package/src/extension/ui/ports-panel.ts +0 -1
- package/src/extension/ui/token-overlays.ts +1 -1
- package/src/extension/welcome/welcome-integration.ts +65 -5
- package/src/i18n/nl.ts +3 -0
- package/src/segments/core.ts +0 -1
- package/src/segments/system.ts +12 -3
- package/src/studio/component.ts +0 -1
- package/src/theme/theme.ts +1 -1
- package/src/tools/graph.ts +0 -3
- package/src/usage/tps-ring.ts +172 -32
- package/src/welcome/banner.ts +14 -0
- package/src/welcome/overlay.ts +139 -7
- package/src/welcome/quick-launch.ts +44 -0
- package/src/welcome/types.ts +13 -0
- package/src/welcome/widgets/sessions-widget.ts +13 -9
- package/src/welcome/widgets/shortcuts-widget.ts +13 -5
- package/src/welcome/widgets/whats-new-widget.ts +16 -3
- package/src/working-vibes/frames.ts +81 -5
- package/src/working-vibes/index.ts +6 -1
- package/src/working-vibes/native-indicator.ts +71 -0
- package/src/working-vibes/storage.ts +5 -3
package/AGENTS.md
CHANGED
|
@@ -44,7 +44,7 @@ Each domain operates through `RuntimeState` callbacks:
|
|
|
44
44
|
Domain modules must not import back into the hub for callbacks. Shared types and constants live in `src/extension/core/`. Verify no circular imports:
|
|
45
45
|
|
|
46
46
|
```bash
|
|
47
|
-
|
|
47
|
+
bun run circular
|
|
48
48
|
# → madge --circular src index.ts bash-mode queue
|
|
49
49
|
```
|
|
50
50
|
|
|
@@ -71,29 +71,43 @@ npm run circular
|
|
|
71
71
|
|
|
72
72
|
## Development Commands
|
|
73
73
|
|
|
74
|
+
Bun is the package manager and script runner (`packageManager: bun@1.4.2`);
|
|
75
|
+
the test suite still runs on Node's own runner because two suites depend on
|
|
76
|
+
`node:test` timer/PTY semantics that `bun test` does not reproduce (see
|
|
77
|
+
`docs/testing.md`).
|
|
78
|
+
|
|
74
79
|
```bash
|
|
75
|
-
# Install dependencies
|
|
76
|
-
|
|
80
|
+
# Install dependencies (frozen lockfile)
|
|
81
|
+
bun install --frozen-lockfile
|
|
77
82
|
|
|
78
83
|
# Type check (strict, no emit)
|
|
79
|
-
|
|
84
|
+
bun run typecheck
|
|
85
|
+
|
|
86
|
+
# Lint (oxlint; warnings are errors)
|
|
87
|
+
bun run lint
|
|
88
|
+
|
|
89
|
+
# Run all tests (node:test, coverage thresholds)
|
|
90
|
+
bun run test
|
|
80
91
|
|
|
81
|
-
#
|
|
82
|
-
|
|
92
|
+
# Typecheck + lint + tests in one go
|
|
93
|
+
bun run check
|
|
83
94
|
|
|
84
95
|
# Check for circular imports
|
|
85
|
-
|
|
96
|
+
bun run circular
|
|
86
97
|
|
|
87
98
|
# Full package contract verification (for publish preparation)
|
|
88
|
-
|
|
99
|
+
bun run verify:package
|
|
89
100
|
|
|
90
101
|
# Local preview server (Deck/Signal/motion surfaces)
|
|
91
|
-
|
|
102
|
+
bun run preview
|
|
103
|
+
|
|
104
|
+
# Enable the pre-commit hook (typecheck + lint + staged tests)
|
|
105
|
+
bun run hooks:install
|
|
92
106
|
|
|
93
107
|
# Dockerized parallel testing (typecheck + test + circular)
|
|
94
108
|
scripts/docker-test.sh [-n N]
|
|
95
109
|
|
|
96
|
-
# Cloud agent bootstrap (Node 24, pi CLI install)
|
|
110
|
+
# Cloud agent bootstrap (bun, Node 24, pi CLI install)
|
|
97
111
|
scripts/cloud-agent-install.sh
|
|
98
112
|
scripts/cloud-agent-start.sh
|
|
99
113
|
```
|
|
@@ -104,7 +118,7 @@ Releases are CI-driven via GitHub Actions:
|
|
|
104
118
|
|
|
105
119
|
```bash
|
|
106
120
|
# Locally: bump version, rewrite CHANGELOG, tag, optional push
|
|
107
|
-
|
|
121
|
+
bun scripts/release.mjs # or: bun run release
|
|
108
122
|
|
|
109
123
|
# Publish to npm (CI handles auth)
|
|
110
124
|
scripts/npm-publish.sh
|
|
@@ -114,7 +128,7 @@ scripts/github-release.sh
|
|
|
114
128
|
```
|
|
115
129
|
|
|
116
130
|
CI workflows (`.github/workflows/`):
|
|
117
|
-
- `test.yml` —
|
|
131
|
+
- `test.yml` — parallel lanes (whitespace, typecheck, lint, test, circular, package-contract, audit) with a `verify` aggregator that keeps the required check name
|
|
118
132
|
- `release.yml` — reuses test.yml, prepares release-candidate branch, publishes on tag, attaches `npm pack` + `SHA256SUMS`
|
|
119
133
|
- `promote-release-candidate.yml` — promotes verified SHA to main, tags, dispatches publish
|
|
120
134
|
|
|
@@ -246,14 +260,16 @@ function createRuntimeState(hooks: {
|
|
|
246
260
|
| Requirement | Detail |
|
|
247
261
|
|---|---|
|
|
248
262
|
| **Node** | ≥22.19 required (pi CLI floor, pinned via `engines`); Node 24 via nvm for CI and pi CLI |
|
|
249
|
-
| **Package manager** |
|
|
263
|
+
| **Package manager** | Bun 1.4.2 (`bun.lock`, `bun install --frozen-lockfile`); the npm CLI is used only for registry operations (`npm pack`, `npm publish`, `npm dist-tag`) |
|
|
264
|
+
| **Lint** | oxlint (`bun run lint`, `--deny-warnings`); config in `.oxlintrc.json`, `no-control-regex` off because ANSI SGR matching is the product |
|
|
265
|
+
| **Pre-commit** | `.githooks/pre-commit` (typecheck + oxlint + staged test files), enabled with `bun run hooks:install` |
|
|
250
266
|
| **TypeScript** | 5.9.3, strict mode, `NodeNext` module/resolution, `allowImportingTsExtensions`, no build step |
|
|
251
267
|
| **Test runner** | Node built-in `node:test` with `--experimental-strip-types` type stripping |
|
|
252
|
-
| **Circular import check** | `madge --circular src index.ts bash-mode queue` (via `
|
|
268
|
+
| **Circular import check** | `madge --circular src index.ts bash-mode queue` (via `bun run circular`) |
|
|
253
269
|
| **Import style** | `.ts` extensions on all imports, `node:`-prefixed builtins |
|
|
254
270
|
| **Pi CLI** | Installed at `^0.99.2` (`pi-coding-agent`, `pi-ai`, `pi-tui` move together — the agent depends on matching `pi-ai`/`pi-tui`, so bumping one alone splits the tree); wrapped to run under Node 24 via nvm |
|
|
255
271
|
| **Peer floor** | `peerDependencies` are `"*"` for the three Pi packages because Pi's own docs require it ("declare the host-provided packages listed above in peerDependencies with a `*` range"), and `scripts/verify-pi-package-contract.mjs` enforces it. `"*"` therefore cannot express a floor, so the real one is only enforced by the code using the SDK: `normalizeContext` (added in `pi-ai` 0.86.0, used by `src/working-vibes/provider.ts`) is the binding symbol — probed across every SDK release, all 12 runtime symbols resolve from 0.86.0 up. A consumer on 0.84.3 installs without warning, but the activation-time peer guard (`src/extension/core/peer-guard.ts`) names every missing symbol with its feature and floor, notifies at session start, and turns the `/vibe` failure into an actionable error; `tests/peer-guard.test.ts` keeps the guarded catalog in sync with the published import surface in both directions. |
|
|
256
|
-
| **Dependency audit** | `
|
|
272
|
+
| **Dependency audit** | `bun run audit` (`scripts/audit-gate.mjs`) is the gate. `audit-baseline.json` lists accepted advisories, each pinned to its GHSA id **and** the install path(s) it covers, with a required written reason; new high/critical advisories fail even on an already-accepted package, a second vulnerable copy of an accepted package fails, a baselined advisory that stops reporting as high/critical also fails, an `npm audit` that errors out fails the gate instead of reading as "zero findings", and the baseline is excluded from the published tarball. |
|
|
257
273
|
| **Docker** | `fuse-overlayfs` storage driver for nested VM testing |
|
|
258
274
|
| **Compound Engineering** | `.compound-engineering/` overlay; portable skills via `~/.agents/skills/ce-*`; native Cursor plugin disabled |
|
|
259
275
|
|
|
@@ -264,7 +280,7 @@ function createRuntimeState(hooks: {
|
|
|
264
280
|
- **Runner**: Node's built-in `node:test` (flat per-file suites)
|
|
265
281
|
- **Assertions**: `node:assert/strict`
|
|
266
282
|
- **Type stripping**: `--experimental-strip-types` (no `tsc` compilation in tests)
|
|
267
|
-
- **Invocation**: `
|
|
283
|
+
- **Invocation**: `bun run test` runs `node --experimental-strip-types --test tests/**/*.test.ts` (bun owns install and script invocation; `bun test` itself is not used — see `docs/testing.md`)
|
|
268
284
|
|
|
269
285
|
### Test Organization
|
|
270
286
|
|
|
@@ -298,19 +314,23 @@ import { powerlineFooter } from "./index.ts";
|
|
|
298
314
|
The `.github/workflows/test.yml` workflow enforces the full verification chain:
|
|
299
315
|
|
|
300
316
|
```bash
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
317
|
+
bun run typecheck # tsc --noEmit
|
|
318
|
+
bun run lint # oxlint --deny-warnings
|
|
319
|
+
bun run test # node --test with coverage thresholds
|
|
320
|
+
bun run circular # madge --circular
|
|
321
|
+
bun run verify:package # verify-package.mjs + verify-pi-package-contract.mjs
|
|
322
|
+
bun run audit # security audit gate
|
|
306
323
|
```
|
|
307
324
|
|
|
325
|
+
Each gate is its own parallel job; the `verify` job aggregates them and stays
|
|
326
|
+
the single required status check.
|
|
327
|
+
|
|
308
328
|
### Release Verification
|
|
309
329
|
|
|
310
330
|
Before any publish, `prepublishOnly` runs the full chain:
|
|
311
331
|
|
|
312
332
|
```bash
|
|
313
|
-
tsc --noEmit && verify-package.mjs && verify-pi-package-contract.mjs
|
|
333
|
+
tsc --noEmit && oxlint --deny-warnings ... && verify-package.mjs && verify-pi-package-contract.mjs
|
|
314
334
|
```
|
|
315
335
|
|
|
316
336
|
### Dockerized Parallel Testing
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,64 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [1.23.0] - 2026-11-08
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
- **Releases reach npm again, with the token npm actually accepts.** Five tags — v1.18.0 through v1.22.0 — were cut but never published, so `npm install @groeponline/pi-wishcraft` still handed out 1.17.0 while the repository was at 1.22.0; the publish job of the v1.22.0 tag died with `ENEEDAUTH`. The pipeline assumed npm trusted publishing, and a probe run from this repository shows why that assumption was wrong: `id-token: write` is granted and the id token is present, but npm answers the OIDC token exchange for `@groeponline/pi-wishcraft` with **401**, so npmjs.com has no matching trusted publisher and npm fell back to "no credential at all". The `publish` job now carries the organisation `NPM_TOKEN` into `setup-node`'s `.npmrc` as `NODE_AUTH_TOKEN` (the probe confirms it authenticates as `chefgroeponline`), keeps `id-token: write` for the day the publisher is configured, and `RELEASE.md` records the measured state with the one-time npmjs.com steps to switch over. The version holes stay: npm versions need not be contiguous, so this is the first version on the registry since 1.17.0.
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
- **CI can start again: every third-party action is pinned to a commit SHA.** The org policy *all actions must be pinned to a full-length commit SHA* rejects `actions/checkout@v7` at job setup, so typecheck, lint, test, circular, package-contract, whitespace and the `verify` aggregator all failed in about three seconds with `steps: 0` — which reads like broken code, not a policy. `checkout`, `setup-node`, `setup-bun` and `cache` are pinned in `test.yml`, `preview.yml`, `deprecate-old-name.yml` and the `bun-setup` composite action, and `tests/workflow-guards.test.ts` gained a third guard that fails on any third-party `uses:` without a 40-hex ref, so the next `@v7` is caught locally. The throwaway `npm-auth-probe.yml` is gone: its question is answered above. (#139)
|
|
12
|
+
|
|
13
|
+
## [1.22.0] - 2026-11-05
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
- **The workflow ordering rules are tested, not remembered.** Two release failures had the same shape — a job used a tool that was not set up yet, or that could not know its own context: `publish` called `bun` before `bun-setup` ran (`bun: command not found`), and `verify` called `gh workflow run` with no checkout to infer the repository from (`failed to run git: fatal: not a git repository`). `tests/workflow-guards.test.ts` now checks every workflow: a job that runs `bun` must install it first, and a job that calls `gh` without a checkout must pin `GH_REPO`. Both assertions were verified to fail against the pre-fix files. `RELEASE.md` also records that a stranded tag can only be replayed with the workflow file it was tagged with.
|
|
17
|
+
|
|
18
|
+
## [1.21.0] - 2026-11-04
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
- **The publish job died with `bun: command not found` before it reached npm.** The bun migration (#128) rewrote `node scripts/verify-release-tag.mjs` to `bun scripts/...` in the `publish` job but left the `bun-setup` step below it, so v1.20.0's publish failed in its second step and never touched the registry. `bun-setup` now runs right after `setup-node` — which also satisfies the `prepublishOnly` gate `npm publish` triggers — and a test pins the order.
|
|
22
|
+
|
|
23
|
+
## [1.20.0] - 2026-11-04
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
- **A verified release candidate can be promoted again.** The `verify` aggregator in `test.yml` has no checkout, so `gh workflow run promote-release-candidate.yml` could not resolve the repository and every dispatch died with `failed to run git: fatal: not a git repository` — since the parallel-CI rewrite (#128) no candidate was promoted at all, which is why `main` sat at 1.19.0 with two tags and no release. The step now pins `GH_REPO: ${{ github.repository }}`, and a test holds it there.
|
|
27
|
+
- **Releases publish through npm trusted publishing, so no credential can expire.** v1.18.0 and v1.19.0 were tagged but never reached npm: the org `NPM_TOKEN` was a revoked 40-character classic token, and npm answers a PUT for a scoped package with 404 (not 403) when the caller may not publish, so the failure read like "package not found". `release.yml`'s `publish` job now grants `id-token: write` and lets the npm CLI exchange that GitHub OIDC identity for a short-lived publish token; `NPM_TOKEN` is no longer read, `scripts/npm-token-check.sh` and the `npm whoami` preflight are gone, and the manual `diagnose-npm.yml` workflow is deleted. `npm-publish.sh` drops the marker-tag + `npm dist-tag add latest` dance — it only existed to republish a version *below* `latest`, which OIDC does not cover; plain `npm publish` sets `latest` itself. Provenance attestations now come for free. `RELEASE.md` documents the one-time trusted-publisher configuration and replaces the token-rotation runbook.
|
|
28
|
+
- **`RELEASE.md` describes the chain that actually runs.** The old text still promised `release.mjs auto --push` bumping and publishing in one job, a `package-lock.json` in the candidate and `npm ci` CI steps; it now documents verify → prepare candidate → Verify on the candidate ref → Promote → publish on the tag ref, with the two-file candidate (`package.json`, `CHANGELOG.md`).
|
|
29
|
+
|
|
30
|
+
### Changed
|
|
31
|
+
- **Bun replaces npm for the development loop.** `bun.lock` (with `packageManager: bun@1.4.2`) replaces `package-lock.json`, `bun install --frozen-lockfile` replaces `npm ci` in every workflow and bootstrap script, and the package scripts run through `bun run` (`scripts/*.mjs` execute directly, no `npx`). The npm CLI stays where the npm *registry* is the interface: `npm pack`, `npm publish`, `npm dist-tag`. The test runner stays `node:test`: measured on 1.4.2 against the 1037-test suite of that moment, `bun test` reported 1035 pass / 2 fail (fake-timer and PTY-timing divergence in two suites) while `node --test` reported 1037 pass / 0 fail, so `bun run test` invokes Node's runner and `docs/testing.md` records the measurement and the condition for revisiting it.
|
|
32
|
+
- **oxlint is the lint gate.** The repository had no ESLint configuration, so this is a gate added rather than a linter swapped: `.oxlintrc.json` (correctness = error), `bun run lint` runs with `--deny-warnings`, CI fails on a single warning, and `bun run lint:fix` applies the automatic fixes. `no-control-regex` is off repo-wide because matching ANSI SGR sequences is the product. Getting to zero warnings removed 23 real findings: 21 unused imports/locals/parameters, one dead `readSkillBody()` call whose result nothing read, and one unsafe optional chain in a test assertion.
|
|
33
|
+
- **CI runs in parallel.** `test.yml` is now seven independent lanes — `whitespace`, `typecheck`, `lint`, `test`, `circular`, `package-contract`, `audit` — each installing from the frozen lockfile via the `.github/actions/bun-setup` composite action, with a `verify` aggregator job that keeps the required check name and the release-candidate promotion dispatch. Pull-request runs cancel superseded pushes; release-driven candidate runs never do.
|
|
34
|
+
- **Pre-commit hook.** `.githooks/pre-commit` (enable with `bun run hooks:install`) runs typecheck, oxlint and the test files the commit touches. The full suite stays CI's job.
|
|
35
|
+
- **The dependency audit gate is bun-native.** `npm audit` needs `package-lock.json`, which no longer exists, so `scripts/audit-gate.mjs` reads `bun audit --json` plus `bun pm ls --all`. The guarantees are restated in bun's terms and pinned by tests: new high/critical advisories fail by GHSA id and package name, a baselined advisory whose vulnerable range moved fails, an installed version the baseline does not list fails (the "second vulnerable copy" case, now version-based instead of path-based), a baselined advisory that stops reporting as high/critical fails, and an audit that cannot run exits 2.
|
|
36
|
+
- **Release candidates carry two files.** `bun.lock` stores no root version, so a bump changes exactly `package.json` and `CHANGELOG.md`; `release.mjs`, `promote-release-candidate.yml` and their tests no longer expect a lockfile rewrite. `bun install --frozen-lockfile` in the publish job proves the lock still matches the manifest.
|
|
37
|
+
- `audit-baseline.json` and `.oxlintrc.json` are excluded from the published tarball; `bun.lock` never ships because `files` does not match it.
|
|
38
|
+
|
|
39
|
+
## [1.19.0] - 2026-11-04
|
|
40
|
+
|
|
41
|
+
### Fixed
|
|
42
|
+
- **`/tps` reported near-zero averages and thousand-TPS peaks.** `pairwiseRates()` and `ratesFromRing()` treated `delta >= 0` as a rate, so every idle repaint pushed a `0` and dragged `avg` down; two samples a few ms apart straddling one token burst reported that burst as a spike. Idle deltas are now skipped (`> 0`), buckets under `TPS_MIN_PAIRWISE_DT_MS` (80 ms) are folded into the next one, and duplicate counts inside 40 ms are dropped at the door.
|
|
43
|
+
- **`/tps` was empty unless the `tps` segment was in the active preset.** The ring was only filled while that segment rendered. `message_update` now samples it (`sampleTpsFromSession`, throttled to 30 ms); the segment keeps its passive sample as a fallback.
|
|
44
|
+
- **Pi's own working spinner ignored `wishcraft.workingIndicatorStyle` and the motion policy.** The style was only used by status renderers. `before_agent_start` now hands Pi `ctx.ui.setWorkingIndicator({ frames, intervalMs })` once per run (Pi owns the timer, so there is no second clock), and `agent_end` restores the default. Reduced/off motion holds one frame, functional alternates two, ASCII glyph mode forces the ASCII set.
|
|
45
|
+
- **Welcome overlay**: the boot reveal repainted once a second because only the countdown timer drove it; it now repaints on the same bounded heartbeat as the header, and keeps repainting while the lantern animates instead of freezing on the last frame once the reveal window closes (`revealElapsedForArt()`). The countdown timer is `unref`'d.
|
|
46
|
+
- **`/tps` averaged cached prefill into the input rate and smoothed nothing.** Each turn is now measured from `agent_start` to its first new token (TTFT), the input rate counts only uncached tokens (`input - cacheRead`), and the output rate carries an EMA (α 0.32) next to the raw window rate. The `ttft` and `ema` overlay lines appear only when a turn could actually be measured; a turn whose prompt was fully cached reports `0`, not a division artefact.
|
|
47
|
+
- Idea capture strips spinner noise for **every** indicator frame, not just braille, so the new native styles cannot leak into captured text. The prefix is now derived from the built-in frames *and* every Motion Catalog frame (whitespace-only rows included) at import time, so a new style cannot silently bypass it.
|
|
48
|
+
- `segment-context.ts` lost its last `any`: `buildSegmentContext()` and `sampleTpsFromSession()` take a structural `SegmentSourceContext` instead of `ctx: any`, and the builder is split into `resolveContextMetrics()` / `resolveCustomItemState()` so its complexity drops below the CodeFactor threshold.
|
|
49
|
+
|
|
50
|
+
### Added
|
|
51
|
+
- Working-indicator styles `lantern` and `braille` (settings registry choices are now derived from `workingIndicatorStyles()` so the two cannot drift).
|
|
52
|
+
- Working-indicator style `motion`: Pi's native spinner is fed from the Motion Catalog (`framesOf()` plus each definition's `generator.intervalMs`) instead of a fixed glyph set, so the working indicator moves like the motion assigned to `thinking`. It falls back to `lantern` for an unknown motion id, and reduced/off motion, functional motion and ASCII glyph mode still win over the catalog.
|
|
53
|
+
- Welcome cockpit: `Tab`/`Shift+Tab` step between `What's new`, `Recent Crafts` and `Quick Launch`, ↑/↓ move inside the focused section and pause the countdown, and `Enter` (only once navigation has paused it) primes the prompt for the focused quick-launch row (`# `, `! `). Sessions stay read-only because Pi exposes no resume handle. Printable keys are still never captured: any letter dismisses, exactly as before.
|
|
54
|
+
|
|
55
|
+
## [1.18.0] - 2026-11-03
|
|
56
|
+
|
|
57
|
+
### Security
|
|
58
|
+
- Pi peers bumped to 1.5.0 (`@earendil-works/pi-coding-agent`, `pi-ai`, `pi-tui` move together): undici is now 8.12.3, clearing GHSA-xjj6-f33q-929c, and the peer dropped its `npm-shrinkwrap.json`, so the two inherited brace-expansion advisories (GHSA-qhr7-859c-m2p7, GHSA-6j4f-fj2g-mc7p) resolve to the fixed 5.0.12 as well. `audit-baseline.json` is back to zero accepted entries.
|
|
59
|
+
|
|
60
|
+
### Fixed
|
|
61
|
+
- `before_agent_start` is a union in pi-coding-agent 1.5.0: the working-vibes prompt text now comes from `event.prompt` on user runs and from displayable custom-message content otherwise.
|
|
62
|
+
|
|
5
63
|
## [1.17.0] - 2026-10-27
|
|
6
64
|
|
|
7
65
|
### Fixed
|
package/CONTRIBUTING.md
CHANGED
|
@@ -11,10 +11,21 @@ For PRs, keep the change narrow and include tests for behavior changes when poss
|
|
|
11
11
|
Before opening a PR, run:
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
|
-
|
|
14
|
+
bun run check # typecheck + oxlint + full test suite
|
|
15
15
|
git diff --check
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
+
Or enable the fast pre-commit hook once per clone — it runs typecheck, oxlint
|
|
19
|
+
and only the test files you touched:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
bun run hooks:install
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Bun is the package manager (`bun install --frozen-lockfile`); the npm CLI is
|
|
26
|
+
reserved for registry operations (`npm pack`/`npm publish`). The test runner
|
|
27
|
+
stays Node's `node:test` — see `docs/testing.md` for why.
|
|
28
|
+
|
|
18
29
|
## Branching
|
|
19
30
|
|
|
20
31
|
Trunk-based, with named prefixes. `main` is always releasable and protected; release tags are cut from it by `scripts/release.mjs`.
|
package/RELEASE.md
CHANGED
|
@@ -12,37 +12,48 @@ verify it.
|
|
|
12
12
|
- **GitHub repo:** `GroepOnline/pi-wishcraft` (forked from `nicobailon/pi-powerline-footer`, independently maintained)
|
|
13
13
|
- **npm package:** `@groeponline/pi-wishcraft` (scoped, `--access public`)
|
|
14
14
|
- **Default branch:** `main`
|
|
15
|
-
- **Local checkout:**
|
|
15
|
+
- **Local checkout:** `/home/joep/Code/GroepOnline/pi-wishcraft/repo`
|
|
16
16
|
- **Pi settings (`~/.pi/agent/settings.json`):** packages entry `npm:@groeponline/pi-wishcraft` and `"powerline": "chef"`. For active development, temporarily swap the entry to the local checkout path so edits are picked up on `/reload` — no reinstall needed; switch back to the npm name for the installed release.
|
|
17
17
|
|
|
18
18
|
## The release flow
|
|
19
19
|
|
|
20
|
-
**Default:** every merge to `main` is a release
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
20
|
+
**Default:** every merge to `main` is a release, but no version is cut on the
|
|
21
|
+
merge commit itself — `main` stays protected and the bump lands through a
|
|
22
|
+
verified candidate.
|
|
23
|
+
|
|
24
|
+
1. `verify` runs the same lanes as a PR (`.github/workflows/test.yml`).
|
|
25
|
+
2. `prepare` confirms verified `main` is still the head of `main` (otherwise a
|
|
26
|
+
newer push owns the next candidate) and cuts an idempotent candidate branch
|
|
27
|
+
`release-candidate/v<next>-<base-sha12>` holding only the
|
|
28
|
+
`chore: release <next>` bump of `package.json` and `CHANGELOG.md`
|
|
29
|
+
(`bun.lock` stores no root version, so it never changes).
|
|
30
|
+
3. `prepare` dispatches Verify on that exact candidate ref.
|
|
31
|
+
4. The final step of that Verify run dispatches
|
|
32
|
+
`.github/workflows/promote-release-candidate.yml`, which re-validates the
|
|
33
|
+
candidate from GitHub metadata only (branch name, parent == `main`, commit
|
|
34
|
+
subject, exactly those two files, both versions, and the
|
|
35
|
+
`## [<version>] - <date>` heading), fast-forwards `main` to the candidate
|
|
36
|
+
SHA, and creates tag `v<version>` via the API.
|
|
37
|
+
5. A tag created with `GITHUB_TOKEN` emits no `push` event, so Promote
|
|
38
|
+
explicitly dispatches `release.yml` on the tag ref. That run's `publish` job
|
|
39
|
+
is what publishes to npm and creates the GitHub Release.
|
|
40
|
+
|
|
41
|
+
`auto` reads commit subjects since the last `v*` tag: `feat:` → minor,
|
|
28
42
|
`feat!:` / `BREAKING CHANGE` → major, otherwise patch.
|
|
29
43
|
|
|
30
|
-
|
|
31
|
-
`
|
|
32
|
-
`
|
|
33
|
-
|
|
34
|
-
Put `[skip release]` in the merge-commit subject to opt out. The follow-up
|
|
35
|
-
`chore: release X.Y.Z` commit does not tag again.
|
|
44
|
+
Put `[skip release]` in the merge-commit subject to opt out; the follow-up
|
|
45
|
+
`chore: release X.Y.Z` commit is skipped the same way. When `CHANGELOG.md` has
|
|
46
|
+
no `## [Unreleased]` notes, `prepare` reports "nothing to release" and cuts no
|
|
47
|
+
candidate.
|
|
36
48
|
|
|
37
49
|
Manual still works when you want an explicit version before pushing tags:
|
|
38
50
|
|
|
39
51
|
```bash
|
|
40
|
-
# 1. Make sure the working tree is clean and
|
|
41
|
-
|
|
42
|
-
npm test # node --experimental-strip-types --test tests/**/*.test.ts
|
|
52
|
+
# 1. Make sure the working tree is clean and the full chain passes (see Verification)
|
|
53
|
+
bun run check # typecheck + oxlint + tests
|
|
43
54
|
|
|
44
55
|
# 2. Release (patch | minor | major | auto | explicit 1.2.3)
|
|
45
|
-
|
|
56
|
+
bun run release minor # = bun scripts/release.mjs minor
|
|
46
57
|
```
|
|
47
58
|
|
|
48
59
|
`scripts/release.mjs` (zero deps, node built-ins only) does:
|
|
@@ -53,8 +64,9 @@ npm run release minor # = node scripts/release.mjs minor
|
|
|
53
64
|
4. `git commit -m "chore: release <version>"`
|
|
54
65
|
5. `git tag -a v<version> -m "Release <version>"`
|
|
55
66
|
|
|
56
|
-
Locally it does **not** push unless you pass `--push` (CI does
|
|
57
|
-
|
|
67
|
+
Locally it does **not** push unless you pass `--push` (CI does not use `--push`
|
|
68
|
+
anymore: `prepare` pushes only the candidate branch). Manual push uses the
|
|
69
|
+
GroepOnline SSH identity (the laptop default SSH key is denied):
|
|
58
70
|
|
|
59
71
|
```bash
|
|
60
72
|
GIT_SSH_COMMAND='ssh -F ~/.ssh/config-groeponline -o IdentityFile=~/.ssh/sheesh' \
|
|
@@ -64,28 +76,107 @@ GIT_SSH_COMMAND='ssh -F ~/.ssh/config-groeponline -o IdentityFile=~/.ssh/sheesh'
|
|
|
64
76
|
- SSH host alias: `github.com-groeponline` (defined in `~/.ssh/config-groeponline` with `IdentityFile ~/.ssh/sheesh`, `IdentitiesOnly yes`).
|
|
65
77
|
- Push actor: `chefadmin-netizen` (GroepOnline SSH). API/`gh` actor: `MisterWanted` (FG) via `chef-gh`.
|
|
66
78
|
|
|
67
|
-
## CI publish
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
79
|
+
## CI publish (tag ref only)
|
|
80
|
+
|
|
81
|
+
`release.yml`'s `publish` job runs only when `github.ref` is a `v*` tag: the
|
|
82
|
+
dispatch Promote fires, or a manual SSH/PAT tag push. It never runs on `main`.
|
|
83
|
+
|
|
84
|
+
1. `actions/checkout` at the tag (`fetch-depth: 0`)
|
|
85
|
+
2. `actions/setup-node` (node 24)
|
|
86
|
+
3. Stamp `PI_WISHCRAFT_BUILD_SHA` and `build-stamp.json` from the tag commit only
|
|
87
|
+
4. `bun scripts/verify-release-tag.mjs --tag <tag>` (tag == `package.json`, on `origin/main`)
|
|
88
|
+
5. `bun install --frozen-lockfile --ignore-scripts` (devDependencies incl. `typescript`)
|
|
89
|
+
6. `sh scripts/npm-publish.sh` (fail-closed `npm view`, idempotent skip, `npm publish --access public`)
|
|
90
|
+
7. `sh scripts/github-release.sh` with `GITHUB_TOKEN` (idempotent: skip if that tag already has a Release; notes from `CHANGELOG.md`)
|
|
91
|
+
8. `npm pack` + `SHA256SUMS`, uploaded as a run artifact and attached to the GitHub Release
|
|
92
|
+
|
|
93
|
+
The lanes that need to prove the tree (typecheck, lint, tests, package contract)
|
|
94
|
+
live in `test.yml` and run in `verify` — the tag commit already passed them as a
|
|
95
|
+
candidate, so `publish` does not repeat them.
|
|
96
|
+
|
|
97
|
+
## npm authentication: the org `NPM_TOKEN`, with trusted publishing prepared
|
|
98
|
+
|
|
99
|
+
`release.yml` publishes with the organisation secret **`NPM_TOKEN`** (a classic
|
|
100
|
+
token for the `chefgroeponline` account). `setup-node` writes it into `~/.npmrc`
|
|
101
|
+
through `NODE_AUTH_TOKEN`, so the value lives only in the org secret store: not
|
|
102
|
+
in the repo, not in a dispatch input, never printed by a run.
|
|
103
|
+
|
|
104
|
+
Trusted publishing (OIDC) is wired but **not active**. The `publish` job still
|
|
105
|
+
grants `id-token: write`, but npm answers the OIDC exchange for
|
|
106
|
+
`@groeponline/pi-wishcraft` with **HTTP 401**, so npmjs.com carries no matching
|
|
107
|
+
GitHub Actions publisher for this repo. With no publisher and no token, npm
|
|
108
|
+
reports `ENEEDAUTH` — that is what kept v1.18.0–v1.22.0 off the registry.
|
|
109
|
+
Measured from this repository on 2026-11-08:
|
|
110
|
+
|
|
111
|
+
| probe | result |
|
|
112
|
+
|---|---|
|
|
113
|
+
| `npm whoami` with `secrets.NPM_TOKEN` | `chefgroeponline` — valid |
|
|
114
|
+
| `ACTIONS_ID_TOKEN_REQUEST_URL` in a job with `id-token: write` | present |
|
|
115
|
+
| `POST /-/npm/v1/oidc/token/exchange/package/@groeponline%2fpi-wishcraft` | **401** |
|
|
116
|
+
|
|
117
|
+
Two consequences: publishes carry no provenance attestation, and the token has
|
|
118
|
+
to be replaced by hand when it expires. Switching to trusted publishing is a
|
|
119
|
+
one-time npmjs.com change, after which the `NODE_AUTH_TOKEN` env comes out of
|
|
120
|
+
the job:
|
|
121
|
+
|
|
122
|
+
1. Sign in as the maintainer of `@groeponline/pi-wishcraft` (`chefgroeponline
|
|
123
|
+
<joep@chefgroep.online>` — `npm view @groeponline/pi-wishcraft maintainers`).
|
|
124
|
+
2. Package → Settings → **Trusted publishing** → add a **GitHub Actions**
|
|
125
|
+
publisher with exactly these values (all fields case-sensitive):
|
|
126
|
+
- Organization or user: `GroepOnline`
|
|
127
|
+
- Repository: `pi-wishcraft`
|
|
128
|
+
- Workflow filename: `release.yml` (with `.yml`)
|
|
129
|
+
- Environment name: empty
|
|
130
|
+
- Allowed actions: **tick `npm publish`** — it is *not* on by default. A
|
|
131
|
+
configuration created after 2026-09-03 starts with staged publishing only,
|
|
132
|
+
so without this tick every `npm publish` is refused. Leave `npm dist-tag`
|
|
133
|
+
off; this repo does not use it.
|
|
134
|
+
3. Verify before trusting it: the exchange line in the table above must answer
|
|
135
|
+
`200`, not `401`. Only then remove `NODE_AUTH_TOKEN` from the job and let OIDC
|
|
136
|
+
publish.
|
|
137
|
+
|
|
138
|
+
Rules that follow from the current (token) configuration:
|
|
139
|
+
|
|
140
|
+
- The credential must stay in the `publish` job of `release.yml`. Moving
|
|
141
|
+
`npm publish` into a reusable workflow (`workflow_call`) would break the OIDC
|
|
142
|
+
match if trusted publishing is switched on later, because npm validates the
|
|
143
|
+
*calling* workflow's filename.
|
|
144
|
+
- Only GitHub-hosted runners can use OIDC; the token path works anywhere.
|
|
145
|
+
- `npm-publish.sh` publishes with the default `latest` dist-tag, and versions
|
|
146
|
+
only ever increase here, so `npm dist-tag` is deliberately never used.
|
|
147
|
+
|
|
148
|
+
### When publish fails: `E404 Not Found - PUT .../@groeponline%2fpi-wishcraft`
|
|
149
|
+
|
|
150
|
+
For a scoped package npm answers a PUT with **404, not 403**, when the caller
|
|
151
|
+
may not publish. That means the credential is missing, revoked, read-only or
|
|
152
|
+
(`EOTP`) 2FA-blocked, or does not own the scope. It is not "package not found",
|
|
153
|
+
and it is not a retryable transient. `ENEEDAUTH` instead means npm had no
|
|
154
|
+
credential at all: `NODE_AUTH_TOKEN` was empty and the OIDC exchange refused.
|
|
155
|
+
|
|
156
|
+
`scripts/npm-publish.sh` prints that diagnosis instead of the raw 404. Recovery:
|
|
157
|
+
|
|
158
|
+
1. Check the credential before the workflow: `npm whoami` against the registry
|
|
159
|
+
with the same token, or rerun the exchange probe from the table above.
|
|
160
|
+
2. Republish the tags that failed, without cutting new versions. A bare
|
|
161
|
+
dispatch on a `v*` tag ref runs verify + publish for that tag:
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
gh workflow run release.yml --ref v1.19.0 # repeats for each failed tag
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Or re-run the failed job directly: `gh run rerun <run-id> --failed`.
|
|
168
|
+
A tag stranded this way keeps its GitHub Release missing until the publish
|
|
169
|
+
run succeeds; `workflow_dispatch` with `inputs.tag` only backfills the
|
|
170
|
+
release assets and never publishes to npm.
|
|
171
|
+
3. A dispatch runs the workflow file **at that tag**, so a tag whose publish job
|
|
172
|
+
is broken by construction cannot be replayed at all: publish the newer tag
|
|
173
|
+
that carries the fix and leave the version hole, since npm versions need not
|
|
174
|
+
be contiguous. v1.20.0 is such a hole — it was tagged by a publish job that
|
|
175
|
+
called `bun` before installing it.
|
|
176
|
+
|
|
177
|
+
The idempotent skip in `npm-publish.sh` makes a retry safe: a version that is
|
|
178
|
+
already on the registry is skipped, so a partially successful run cannot
|
|
179
|
+
double-publish.
|
|
89
180
|
|
|
90
181
|
The package has no build step — `*.ts` files are shipped directly (`pi.extensions:
|
|
91
182
|
["./index.ts"]`), and Pi loads them with TypeScript stripping at runtime.
|
|
@@ -96,8 +187,8 @@ After a release, confirm each layer. Example for `0.15.0`:
|
|
|
96
187
|
|
|
97
188
|
```bash
|
|
98
189
|
# 1. Local: typecheck + tests green BEFORE tagging
|
|
99
|
-
|
|
100
|
-
|
|
190
|
+
bun run typecheck # exit 0
|
|
191
|
+
bun run test # "ℹ pass 170", "ℹ fail 0"
|
|
101
192
|
|
|
102
193
|
# 2. Tag pushed
|
|
103
194
|
git ls-remote --tags origin | grep v0.15.0 # shows v0.15.0 on origin
|
|
@@ -142,13 +233,15 @@ is that same tag.
|
|
|
142
233
|
|
|
143
234
|
## Versioning
|
|
144
235
|
|
|
145
|
-
Feature PRs **do not bump** `package.json`. They
|
|
146
|
-
|
|
236
|
+
Feature PRs **do not bump** `package.json`. They inherit whatever version
|
|
237
|
+
`main` already carries and append under `## [Unreleased]` in `CHANGELOG.md`.
|
|
147
238
|
The bump happens on `main` in the release workflow, not in the feature PR.
|
|
148
239
|
|
|
149
240
|
Never bump in the same PR as the feature work. Never tag from a stacked
|
|
150
|
-
feature branch. `
|
|
151
|
-
`
|
|
241
|
+
feature branch. `v<version>` exists only after Promote tags the verified
|
|
242
|
+
candidate (or a manual `bun run release` on `main`). `main` can be ahead of npm
|
|
243
|
+
when a publish fails — compare `npm view @groeponline/pi-wishcraft version`
|
|
244
|
+
with `bun scripts/release.mjs next auto` before assuming they agree.
|
|
152
245
|
|
|
153
246
|
- **patch** (`0.18.0 → 0.18.1`): bug fixes, doc only; escape hatch if 0.19 slips.
|
|
154
247
|
- **minor** (`0.18.x → 0.19.0`): new segments/presets, features, additive changes.
|
package/bash-mode/completion.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readdirSync } from "node:fs";
|
|
2
2
|
import { spawnSync } from "node:child_process";
|
|
3
3
|
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
|
4
|
-
import type { AutocompleteItem, AutocompleteProvider
|
|
4
|
+
import type { AutocompleteItem, AutocompleteProvider } from "@earendil-works/pi-tui";
|
|
5
5
|
import { matchHistoryEntries, readGlobalShellHistory, readProjectHistory } from "./history.ts";
|
|
6
6
|
import type { ExtendedCompletionItem, GhostSuggestion } from "./types.ts";
|
|
7
7
|
|
|
@@ -365,7 +365,7 @@ function boostValidatedItemsFromGlobalHistory(
|
|
|
365
365
|
}
|
|
366
366
|
|
|
367
367
|
export class BashCompletionEngine {
|
|
368
|
-
async getGhostSuggestion(line: string, cwd: string, shellPath: string,
|
|
368
|
+
async getGhostSuggestion(line: string, cwd: string, shellPath: string, _signal: AbortSignal): Promise<GhostSuggestion | null> {
|
|
369
369
|
const projectHistoryEntries = readProjectHistory(cwd);
|
|
370
370
|
|
|
371
371
|
if (line.trim().length === 0) {
|
package/build-stamp.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"source_sha":"
|
|
1
|
+
{"source_sha":"a12a4900dbbf32823e1c2c6261c850eb21834d38"}
|
package/docs/configuration.md
CHANGED
|
@@ -210,8 +210,11 @@ The indicator is intentionally separate from the structural appearance motion: i
|
|
|
210
210
|
| `pulse` | expanding/contracting center | clear activity at a glance |
|
|
211
211
|
| `bar` | travelling block across a track | high-information dashboards |
|
|
212
212
|
| `ascii` | portable `[=]`, `[-]`, `[.]` frames | SSH, basic terminals, and logs |
|
|
213
|
+
| `lantern` | rising block ramp (▁→█) | the signature look, ambient sessions |
|
|
214
|
+
| `braille` | classic braille spinner | compact terminals and log capture |
|
|
215
|
+
| `motion` | frames from the Motion Catalog (the motion assigned to `thinking`) at that definition's own `generator.intervalMs` | matching the working spinner to the rest of the motion language |
|
|
213
216
|
|
|
214
|
-
Configure it under `wishcraft.workingIndicatorStyle`; supported values are `dots` (default), `pulse`, `bar`, and `
|
|
217
|
+
Configure it under `wishcraft.workingIndicatorStyle`; supported values are `dots` (default), `pulse`, `bar`, `ascii`, `lantern`, `braille`, and `motion`. The list in the settings UI is derived from `workingIndicatorStyles()`, so it cannot drift from what the renderer accepts. Motion accessibility settings still apply: reduced/off motion uses a static frame, functional motion alternates two frames, and ASCII glyph mode forces the ASCII set — `motion` falls back to `lantern` when its motion id is unknown.
|
|
215
218
|
|
|
216
219
|
```json
|
|
217
220
|
{
|
|
@@ -506,6 +509,8 @@ Four questions — language, status preset, motion level, welcome overlay — th
|
|
|
506
509
|
|
|
507
510
|
On the very first launch the welcome panel shows three next steps under **Getting started** instead of a changelog wall. Later launches show the usual changelog delta under **What's new**.
|
|
508
511
|
|
|
512
|
+
The panel is a cockpit, not a splash screen. `Tab`/`Shift+Tab` step between **What's new**, **Recent Crafts** and **Quick Launch**, ↑/↓ move inside the focused section (the selected changelog bullet expands across wrapped lines instead of being truncated), and `Enter` — only after navigation has paused the countdown — primes the prompt for the focused quick-launch row (`# ` for an idea, `! ` for a sticky bash session). `Recent Crafts` is read-only: Pi exposes no session-resume handle. No printable key is ever captured by the overlay, so the first letter of the prompt you start typing still dismisses it. With `wishcraft.welcome.animateLantern` on, the lantern keeps burning on the ambient heartbeat after the boot reveal instead of freezing on its last frame.
|
|
513
|
+
|
|
509
514
|
## Segment options: two accepted shapes
|
|
510
515
|
|
|
511
516
|
Segment options may be written either hand-edited at the top level of `powerline`, or nested under `powerline.segmentOptions` (the shape the settings UI writes):
|
|
@@ -40,7 +40,7 @@ Wishcraft establishes a multi-tiered test strategy that verifies behavior withou
|
|
|
40
40
|
|
|
41
41
|
1. **No Headless TUI Requirement**: Core algorithms must never instantiate `ctx.ui` or direct terminal stdout in tests. Renderers emit plain string buffers or token trees.
|
|
42
42
|
2. **Deterministic Time Control**: The `MotionScheduler` accepts an injected virtual clock or step function for precise, millisecond-accurate timeline assertions.
|
|
43
|
-
3. **Circular Dependency Prevention**: Verified automatically on every build via `
|
|
43
|
+
3. **Circular Dependency Prevention**: Verified automatically on every build via `bun run circular`.
|
|
44
44
|
4. **Strict TypeScript Checking**: `tsc --noEmit` runs under strict mode with zero implicit `any`.
|
|
45
45
|
|
|
46
46
|
---
|
package/docs/testing.md
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# Testing & tooling
|
|
2
|
+
|
|
3
|
+
Bun owns installation and script execution. The test **runner** stays Node's
|
|
4
|
+
`node:test`, and that split is deliberate.
|
|
5
|
+
|
|
6
|
+
## Why Bun installs but Node runs the tests
|
|
7
|
+
|
|
8
|
+
`bun test` runs this suite, but not identically: measured on 1.4.2 with 1037
|
|
9
|
+
tests, it reports **1035 pass / 2 fail** where `node --test` reports 1037/0.
|
|
10
|
+
|
|
11
|
+
| Failing under `bun test` | Cause |
|
|
12
|
+
|---|---|
|
|
13
|
+
| `ports.test.ts` — "serves stale rows past the TTL instead of blanking the number" | Bun's fake-timer semantics differ from `node:test`'s `mock.timers`; the TTL never advances as the test expects. |
|
|
14
|
+
| `ptyshell-managed.test.ts` — "carries cwd between commands via the sentinel" | Bun's PTY/child-process timing diverges; the sentinel is read before the shell writes it. |
|
|
15
|
+
|
|
16
|
+
Both suites assert real behaviour that Node's runner exercises correctly, so
|
|
17
|
+
the choice is "keep the runner that proves the behaviour" rather than
|
|
18
|
+
"rewrite two suites to fit a faster runner". The coverage thresholds
|
|
19
|
+
(`--test-coverage-lines=70 --test-coverage-functions=60`) are `node:test` flags
|
|
20
|
+
as well. Everything else — install, script invocation, lint, typecheck — is Bun.
|
|
21
|
+
|
|
22
|
+
Re-evaluate when Bun's runner gains `node:test` timer parity; the gate is a
|
|
23
|
+
single command change in `package.json`, not a rewrite.
|
|
24
|
+
|
|
25
|
+
## Commands
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
bun install --frozen-lockfile # install exactly what bun.lock pins
|
|
29
|
+
bun run typecheck # tsc --noEmit
|
|
30
|
+
bun run lint # oxlint --deny-warnings
|
|
31
|
+
bun run test # node:test + coverage thresholds
|
|
32
|
+
bun run check # typecheck + lint + test
|
|
33
|
+
bun run circular # madge --circular
|
|
34
|
+
bun run verify:package # package + Pi contract gates
|
|
35
|
+
bun run audit # dependency audit gate
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Pre-commit hook
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
bun run hooks:install # git config core.hooksPath .githooks
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
`.githooks/pre-commit` runs `typecheck`, `lint` and only the test files the
|
|
45
|
+
commit touches. It is fast by design; the full suite is CI's job (or
|
|
46
|
+
`bun run check` before pushing).
|
|
47
|
+
|
|
48
|
+
## Lint
|
|
49
|
+
|
|
50
|
+
`oxlint` (config in `.oxlintrc.json`) is the lint gate: correctness rules are
|
|
51
|
+
errors, and CI runs with `--deny-warnings`, so a warning fails the build the
|
|
52
|
+
same way an error does. `no-control-regex` is off repo-wide because matching
|
|
53
|
+
ANSI SGR sequences is the product, not an accident.
|
|
54
|
+
|
|
55
|
+
## CI shape
|
|
56
|
+
|
|
57
|
+
`.github/workflows/test.yml` runs seven independent lanes in parallel —
|
|
58
|
+
`whitespace`, `typecheck`, `lint`, `test`, `circular`, `package-contract`,
|
|
59
|
+
`audit` — each installing from the frozen lockfile through the
|
|
60
|
+
`.github/actions/bun-setup` composite action. A final `verify` job aggregates
|
|
61
|
+
them and keeps that name as the single required status check; it also carries
|
|
62
|
+
the release-candidate promotion dispatch, so the Verify → Promote chain is
|
|
63
|
+
unchanged.
|
|
64
|
+
|
|
65
|
+
## Dependency audit
|
|
66
|
+
|
|
67
|
+
`scripts/audit-gate.mjs` reads `bun audit --json` plus `bun pm ls --all`.
|
|
68
|
+
Because `bun.lock` carries no `package-lock.json` for npm to read, the npm
|
|
69
|
+
audit schema is gone; the gate keeps the same guarantees in bun's terms:
|
|
70
|
+
|
|
71
|
+
- a new high/critical advisory fails, matched by GHSA id **and** package name;
|
|
72
|
+
- a baselined advisory whose vulnerable range moved fails (the exemption is
|
|
73
|
+
pinned to the range a human reviewed);
|
|
74
|
+
- an installed version the baseline does not list fails, so a second
|
|
75
|
+
vulnerable copy of an accepted package cannot inherit the exemption;
|
|
76
|
+
- a baselined advisory that stops reporting as high/critical fails, so the
|
|
77
|
+
baseline cannot rot into a blanket exemption;
|
|
78
|
+
- an audit that cannot run exits 2 instead of reading as "zero findings".
|
|
79
|
+
|
|
80
|
+
`audit-baseline.json` and `.oxlintrc.json` are excluded from the published
|
|
81
|
+
tarball; `bun.lock` never ships because `files` does not match it.
|