@groeponline/pi-wishcraft 1.14.0 → 1.16.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 CHANGED
@@ -251,7 +251,9 @@ function createRuntimeState(hooks: {
251
251
  | **Test runner** | Node built-in `node:test` with `--experimental-strip-types` type stripping |
252
252
  | **Circular import check** | `madge --circular src index.ts bash-mode queue` (via `npm run circular`) |
253
253
  | **Import style** | `.ts` extensions on all imports, `node:`-prefixed builtins |
254
- | **Pi CLI** | Installed at `^0.84.0`; wrapped to run under Node 24 via nvm |
254
+ | **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
+ | **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** | `npm 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. |
255
257
  | **Docker** | `fuse-overlayfs` storage driver for nested VM testing |
256
258
  | **Compound Engineering** | `.compound-engineering/` overlay; portable skills via `~/.agents/skills/ce-*`; native Cursor plugin disabled |
257
259
 
package/CHANGELOG.md CHANGED
@@ -2,6 +2,25 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [1.16.0] - 2026-10-03
6
+
7
+ ### Known limitations
8
+ - **`peerDependencies: "*"` cannot express a supported-version floor.** Pi's own docs require the host-provided packages to be declared with a `"*"` range and `verify-pi-package-contract.mjs` enforces it, so the constraint is upstream and cannot be tightened here. The consequence is measured rather than assumed: probing every SDK release for the 12 runtime symbols this package imports, all of them resolve from 0.86.0 up, and the binding one is `normalizeContext` (`src/working-vibes/provider.ts`), which arrived in `pi-ai` 0.86.0. A consumer on 0.84.3 therefore installs this package without warning or resolution error. The manifest side of this stays open — the floor cannot be expressed there at all — but the silent failure is closed: the extension now checks the symbol set at activation and reports the gap (see Fixed).
9
+
10
+ ### Fixed
11
+ - **A Pi SDK below 0.86.0 broke `/vibe` with a bare `TypeError`.** `peerDependencies` are `"*"` by upstream contract, so an old host installs this package without complaint and the breakage surfaced as "normalizeContext is not a function" at the first `/vibe` call. The extension now checks the runtime symbol set it actually imports from the three Pi peers at activation (`src/extension/core/peer-guard.ts`): every missing symbol is named with the feature that depends on it and the version floor to upgrade to, the first session surfaces the same as a UI notification, and the `/vibe` call site fails with that actionable message instead of a TypeError. The guard checks symbol presence rather than version strings because symbols are what the code depends on — a semver comparison would lie across backports or version-skewed trees. `tests/peer-guard.test.ts` keeps the guarded catalog in exact sync with the runtime imports of every published source file in both directions: a newly used SDK symbol that is not guarded fails the suite, as does a catalog entry nothing imports anymore. Each guard was verified against an injected regression rather than only against the passing state.
12
+ - **The dependency audit could never fail CI, so it caught nothing.** The step ran `npm audit --audit-level=high` behind `continue-on-error: true`, labelled "informational" — and it exits 1 today, because of the one finding below. A step that cannot fail is a step that also cannot fail on anything *new*: the `ws@7.5.10` advisory, or the next one, would have shipped silently. The same shape as the coverage gate that printed thresholds while collecting no coverage. `npm run audit` (`scripts/audit-gate.mjs`) now audits against `audit-baseline.json`: a new high or critical advisory fails, the justified inherited advisories are tolerated, a baselined advisory that stops reporting as high/critical fails too (so it cannot become a permanent blanket exemption), and every accepted entry must carry a written reason naming the mechanism that makes it unreachable, its GHSA id, and the install path(s) it covers — so neither a new advisory nor a second vulnerable copy of the same package inherits the exemption. An `npm audit` that errors out (registry down, npm config policy) fails the gate rather than reading as "zero findings". Verified by injecting `ws@7.5.10` into the real tree — the gate named it as new and blocked while still passing the baselined finding.
13
+ - `audit-baseline.json` was shipping inside the npm tarball: `files` contains a `*.json` glob, so a new root-level JSON file is published by default. Excluded, and `tests/audit-gate.test.ts` now reads the real `npm pack` output rather than trusting the `files` array.
14
+ - **The remaining `npm audit` finding was half ours.** The 1.15.0 note recorded that `brace-expansion` "has no fix available at any installable version" — that was wrong on both counts, and it buried a fixable bug under a false exemption. There are two copies in the tree and they have different causes. The top-level one (pulled in via `madge` → `minimatch`) was sitting on 5.0.9 through a stale lockfile pin and is ours: it is now 5.0.12. The copy nested under `@earendil-works/pi-coding-agent` really is out of reach, but not because no fix exists — `brace-expansion@5.0.12` satisfies the `^5.0.8` range the peer's own `minimatch` declares, so the peer can fix it with a lockfile bump alone, no API change. It is unreachable from here for a different reason: the peer publishes an `npm-shrinkwrap.json`, which is authoritative for consumers, and npm installs that pinned tree verbatim without re-resolving it. Confirmed empirically that `overrides` cannot reach it — both a top-level `"brace-expansion": "^5.0.12"` and a path-scoped `"@earendil-works/pi-coding-agent": { "brace-expansion": "^5.0.12" }` left the nested copy at 5.0.9 — so no override is shipped rather than one that would advertise a guarantee it cannot keep.
15
+ - `tests/dependency-provenance.test.ts` guards the distinction: it fails if a vulnerable copy appears anywhere outside the peer's subtree (that one is ours, and names `npm audit fix`), if an unreachable override is ever added, and if the peer's shrinkwrap moves to a fixed version (the finding is then gone and the CHANGELOG needs updating). Each guard was verified against an injected regression rather than only against the passing state. The same file now also guards the general shape: we ship no shrinkwrap ourselves, an `overrides` entry never targets a package that ships one (such an override is a silent no-op), and every baselined advisory names a mechanism rather than just asserting "inherited".
16
+ - Audited the rest of the tree for the same pattern and found no second exemption: one high finding total, no deprecated packages, all 346 registry signatures and 90 attestations verified. A synthetic consumer install (`pi-wishcraft@1.15.0` alongside the three Pi packages) reports exactly the same single finding, so the published package surfaces nothing extra. Of the 146 versions the peer shrinkwraps, 25 are absent from a linux-x64 tree and all 25 are platform-specific `@esbuild/*` binaries with no advisory surface — noted as a real blind spot on other platforms rather than a clean bill of health.
17
+
18
+ ## [1.15.0] - 2026-10-01
19
+
20
+ ### Changed
21
+ - **Pi SDK upgraded `0.84.3` -> `0.99.2`** (`@earendil-works/pi-coding-agent`, `pi-ai`, `pi-tui` moved together). These must be bumped as a set: the agent depends on matching `pi-ai`/`pi-tui`, so bumping it alone leaves a version-skewed tree — which is exactly what the queued Dependabot PR #118 does (its lockfile keeps top-level `pi-ai` at `0.84.3` under a `0.99.2` agent). On the coordinated bump npm still nests copies, but at identical versions, so nothing is skewed. One real API change: `Provider.stream()` now takes a branded `TranscriptContext` produced only by `normalizeContext()`, which folds `systemPrompt`/`tools` into a leading system message — `src/working-vibes/provider.ts` now calls it, and the working-vibes tests assert the prompt the way the SDK delivers it (as `messages[0]`) rather than through a `context.systemPrompt` field that no longer exists.
22
+ - Audit findings drop from 3 (1 moderate, 2 high) to 1 high; the `undici` advisories are cleared. The remaining `brace-expansion` finding is inherited from the peer package's own dependency tree. (Corrected after release: the claim that it had no fix at any installable version was wrong — see [Unreleased].)
23
+
5
24
  ## [1.14.0] - 2026-10-01
6
25
 
7
26
  ### Added
package/README.md CHANGED
@@ -21,7 +21,9 @@
21
21
  pi install npm:@groeponline/pi-wishcraft
22
22
  ```
23
23
 
24
- Reload Pi, then press `alt+p` to open the Deck. Two flows make Wishcraft click fastest:
24
+ Reload Pi, then press `alt+p` to open the Deck. First run? `/wishcraft setup` asks four questions — language, status preset, motion level, welcome overlay — reviews them, and writes one `settings.json`.
25
+
26
+ Two flows make Wishcraft click fastest:
25
27
 
26
28
  ```text
27
29
  # remember to benchmark the new provider path
@@ -30,6 +32,8 @@ Reload Pi, then press `alt+p` to open the Deck. Two flows make Wishcraft click f
30
32
 
31
33
  The `#` line is captured as an idea instead of being sent as a prompt — your active run keeps going. Then `/skills` opens the picker: search, inspect and insert a skill without leaving the session.
32
34
 
35
+ > **Upgrading?** This project was published under the name `@groeponline/pi-powerline-footer`. That package name is deprecated on npm and no longer receives updates — install `@groeponline/pi-wishcraft` instead. Settings keys are unchanged, so an existing `settings.json` keeps working; drop the old entry from your Pi config when you switch.
36
+
33
37
  ## Why Wishcraft
34
38
 
35
39
  Pi stays the engine. Wishcraft adds the operator layer around it: live status, overlays, idea capture, skill search, sticky Bash, hooks, policy controls and session UX.
@@ -52,6 +56,7 @@ Kongming lanterns started as battlefield signals and later carried wishes. Wishc
52
56
  | Read hints | Appends a one-line continuation hint after a partial `read`, so the model knows the next offset. Opt-out: `wishcraft.readHints: false`. |
53
57
  | Policy | In-process deny/inject rules in global settings. No spawn. Kill-switch: `wishcraft.policyEnabled`. |
54
58
  | Working indicator | Four deterministic styles (`dots`, `pulse`, `bar`, `ascii`) with accessibility-aware static fallbacks. |
59
+ | Interface language | English (default) or Dutch, switchable live via `wishcraft.locale`. English is compiled into every call site, so a missing translation falls back instead of blanking a surface. |
55
60
  | Welcome art | `lantern`, `balloon`, and `normal` opening artwork, each with a different silhouette and narrow-terminal fallback. |
56
61
 
57
62
  Pi owns the footer chrome, feed scrolling, and input. Wishcraft supplies widgets, overlays, and the bash/stash/editor integrations. The bar is not clickable; actions are commands and overlays.
@@ -109,6 +114,14 @@ Keybinds (`powerlineShortcuts`, applied after `/reload`; `null` disables):
109
114
 
110
115
  `chef` is muted colors, slash separators, live TPS in/out, and TCP port count. Built-in presets: `default`, `minimal`, `compact`, `full`, `nerd`, `ascii`, `chef`. For every setting at its default, see [`examples/settings.example.json`](examples/settings.example.json).
111
116
 
117
+ Interface language (`en` | `nl`) applies immediately, no restart:
118
+
119
+ ```json
120
+ { "wishcraft": { "locale": "nl" } }
121
+ ```
122
+
123
+ Set `PI_WISHCRAFT_LOCALE` to pick a language before settings are read.
124
+
112
125
  Daily token budget (never blocks a turn):
113
126
 
114
127
  ```json
@@ -128,8 +141,6 @@ Privacy/network boundary: ideas, settings, usage ledgers, and normal cockpit sta
128
141
  - No mouse on the live footer. Pi core owns that surface.
129
142
  - No second `alt+i` product. Ports stay on `alt+i`; other detail is `→` in the navigator.
130
143
  - Compatibility status keys (`powerline.preset`, `powerline.tps`, `powerline.ports`) are available to peer extensions; normal Wishcraft use does not require them.
131
- - The legacy `@groeponline/pi-powerline-footer` package is deprecated in favor of `@groeponline/pi-wishcraft`.
132
- - Tags are not rewritten. 0.19.x through current stay on the timeline.
133
144
 
134
145
  ## vNext direction
135
146
 
package/build-stamp.json CHANGED
@@ -1 +1 @@
1
- {"source_sha":"a7570d9c73e85d4553e6bfbba32e1c76c7d857b9"}
1
+ {"source_sha":"1957ea6e4334e28e25770cba4114e33c69062632"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groeponline/pi-wishcraft",
3
- "version": "1.14.0",
3
+ "version": "1.16.0",
4
4
  "description": "Pi coding agent extension: operator cockpit with a live powerline status bar, idea queue, skill search, sticky Bash, hooks, policy guardrails, and TUI session UX.",
5
5
  "type": "module",
6
6
  "engines": {
@@ -10,6 +10,7 @@
10
10
  "*.ts",
11
11
  "*.md",
12
12
  "*.json",
13
+ "!audit-baseline.json",
13
14
  "banner.png",
14
15
  "docs/**/*.md",
15
16
  "src/**/*.ts",
@@ -64,6 +65,7 @@
64
65
  "prepublishOnly": "tsc --noEmit && node scripts/verify-package.mjs && npm run verify:pi-package",
65
66
  "verify:package": "node scripts/verify-package.mjs && node scripts/verify-pi-package-contract.mjs",
66
67
  "circular": "madge --circular src index.ts bash-mode queue",
68
+ "audit": "node scripts/audit-gate.mjs",
67
69
  "preview": "node --experimental-strip-types scripts/preview.mjs",
68
70
  "verify:pi-package": "node scripts/verify-pi-package-contract.mjs"
69
71
  },
@@ -73,9 +75,9 @@
73
75
  "@earendil-works/pi-tui": "*"
74
76
  },
75
77
  "devDependencies": {
76
- "@earendil-works/pi-ai": "^0.84.0",
77
- "@earendil-works/pi-coding-agent": "^0.84.3",
78
- "@earendil-works/pi-tui": "^0.84.0",
78
+ "@earendil-works/pi-ai": "^0.99.2",
79
+ "@earendil-works/pi-coding-agent": "^0.99.2",
80
+ "@earendil-works/pi-tui": "^0.99.2",
79
81
  "@types/node": "24.13.3",
80
82
  "madge": "^8.0.0",
81
83
  "typescript": "5.9.3"
@@ -0,0 +1,222 @@
1
+ // peer-guard.ts
2
+ // Activation-time guard for the host-provided Pi SDK peers.
3
+ //
4
+ // `peerDependencies` for the three @earendil-works packages must stay "*" (Pi's
5
+ // package contract requires it and `verify-pi-package-contract.mjs` enforces
6
+ // it), so the manifest cannot express the version floor this extension really
7
+ // has. Every runtime symbol it imports exists from 0.86.0 up, and
8
+ // `normalizeContext` (pi-ai) is the binding one — it arrived last. Without a
9
+ // guard, a host on 0.84.3 loads the extension cleanly and only fails at first
10
+ // use: `/vibe` died on a bare `TypeError: normalizeContext is not a function`
11
+ // deep inside provider code.
12
+ //
13
+ // The guard checks the imported *symbol set* rather than a version string:
14
+ // symbol presence is what the code actually depends on, and it survives
15
+ // backports, republishes, and version-skewed trees where a semver comparison
16
+ // would lie. Warn-only by design — an under-floor host still runs every other
17
+ // surface, so blocking activation would break more than it fixes.
18
+
19
+ import * as piAi from "@earendil-works/pi-ai";
20
+ import * as piCodingAgent from "@earendil-works/pi-coding-agent";
21
+ import * as piTui from "@earendil-works/pi-tui";
22
+
23
+ /** Oldest Pi SDK release where every imported symbol exists (probed per release). */
24
+ export const PEER_FLOOR_VERSION = "0.86.0";
25
+
26
+ export type PeerPackage =
27
+ | "@earendil-works/pi-ai"
28
+ | "@earendil-works/pi-coding-agent"
29
+ | "@earendil-works/pi-tui";
30
+
31
+ export interface PeerSymbolRequirement {
32
+ pkg: PeerPackage;
33
+ symbol: string;
34
+ /** Feature that degrades when the symbol is absent. */
35
+ usedBy: string;
36
+ }
37
+
38
+ // The full runtime (non-type) import surface of this package. Type-only
39
+ // imports cannot fail at runtime and are deliberately absent; the structural
40
+ // test in tests/peer-guard.test.ts keeps this list in exact sync with the
41
+ // imports in every published source file, in both directions.
42
+ export const PEER_SYMBOL_REQUIREMENTS: readonly PeerSymbolRequirement[] = [
43
+ {
44
+ pkg: "@earendil-works/pi-ai",
45
+ symbol: "normalizeContext",
46
+ usedBy: "/vibe working messages",
47
+ },
48
+ {
49
+ pkg: "@earendil-works/pi-ai",
50
+ symbol: "createAssistantMessageEventStream",
51
+ usedBy: "studio advice pane",
52
+ },
53
+ {
54
+ pkg: "@earendil-works/pi-coding-agent",
55
+ symbol: "CustomEditor",
56
+ usedBy: "sticky Bash mode",
57
+ },
58
+ {
59
+ pkg: "@earendil-works/pi-coding-agent",
60
+ symbol: "copyToClipboard",
61
+ usedBy: "copy shortcuts and /vibe test",
62
+ },
63
+ {
64
+ pkg: "@earendil-works/pi-coding-agent",
65
+ symbol: "loadSkills",
66
+ usedBy: "skill registry",
67
+ },
68
+ {
69
+ pkg: "@earendil-works/pi-coding-agent",
70
+ symbol: "SessionManager",
71
+ usedBy: "/cd",
72
+ },
73
+ {
74
+ pkg: "@earendil-works/pi-tui",
75
+ symbol: "matchesKey",
76
+ usedBy: "keyboard shortcuts",
77
+ },
78
+ {
79
+ pkg: "@earendil-works/pi-tui",
80
+ symbol: "isKeyRelease",
81
+ usedBy: "keyboard shortcuts",
82
+ },
83
+ {
84
+ pkg: "@earendil-works/pi-tui",
85
+ symbol: "visibleWidth",
86
+ usedBy: "status line rendering",
87
+ },
88
+ {
89
+ pkg: "@earendil-works/pi-tui",
90
+ symbol: "truncateToWidth",
91
+ usedBy: "status line rendering",
92
+ },
93
+ {
94
+ pkg: "@earendil-works/pi-tui",
95
+ symbol: "SelectList",
96
+ usedBy: "select overlays",
97
+ },
98
+ {
99
+ pkg: "@earendil-works/pi-tui",
100
+ symbol: "TUI_KEYBINDINGS",
101
+ usedBy: "default keybindings",
102
+ },
103
+ ];
104
+
105
+ /**
106
+ * Resolve a symbol from a peer module namespace. Looks on the namespace itself
107
+ * and, for interop shapes where named exports land under `default` (CJS-style
108
+ * wrapping during extension load), there too.
109
+ */
110
+ function lookupExport(moduleNs: unknown, symbol: string): unknown {
111
+ if (moduleNs === null || typeof moduleNs !== "object") return undefined;
112
+ const ns = moduleNs as Record<string, unknown>;
113
+ const direct = ns[symbol];
114
+ if (direct !== undefined) return direct;
115
+ const fallback = ns.default;
116
+ if (fallback !== null && typeof fallback === "object") {
117
+ return (fallback as Record<string, unknown>)[symbol];
118
+ }
119
+ return undefined;
120
+ }
121
+
122
+ /** Which required symbols are absent from the given peer namespaces? */
123
+ export function findMissingPeerSymbols(
124
+ exportsByPkg: Readonly<Partial<Record<PeerPackage, unknown>>>,
125
+ requirements: readonly PeerSymbolRequirement[] = PEER_SYMBOL_REQUIREMENTS,
126
+ ): PeerSymbolRequirement[] {
127
+ return requirements.filter(
128
+ ({ pkg, symbol }) => lookupExport(exportsByPkg[pkg], symbol) === undefined,
129
+ );
130
+ }
131
+
132
+ /** The live peer namespaces, collected for the runtime check. */
133
+ export function collectPeerExports(): Record<PeerPackage, unknown> {
134
+ return {
135
+ "@earendil-works/pi-ai": piAi,
136
+ "@earendil-works/pi-coding-agent": piCodingAgent,
137
+ "@earendil-works/pi-tui": piTui,
138
+ };
139
+ }
140
+
141
+ /** The runtime check against the peers this process actually loaded. */
142
+ export function checkPeerRuntime(): PeerSymbolRequirement[] {
143
+ return findMissingPeerSymbols(collectPeerExports());
144
+ }
145
+
146
+ /** Multi-line console warning naming every missing symbol and its blast radius. */
147
+ export function formatPeerGuardWarning(
148
+ missing: readonly PeerSymbolRequirement[],
149
+ ): string {
150
+ const lines = missing.map(
151
+ (m) => ` - ${m.symbol} (${m.pkg}) — needed by ${m.usedBy}`,
152
+ );
153
+ return [
154
+ `Pi SDK below the supported floor: ${missing.length} imported symbol${missing.length === 1 ? " is" : "s are"} missing (all of them exist from ${PEER_FLOOR_VERSION} up).`,
155
+ ...lines,
156
+ `The extension stays loaded and affected features fall back; upgrade @earendil-works/pi-coding-agent with its matching pi-ai/pi-tui to ${PEER_FLOOR_VERSION} or newer.`,
157
+ ].join("\n");
158
+ }
159
+
160
+ /** One-line variant for `ctx.ui.notify`. */
161
+ export function formatPeerGuardNotice(
162
+ missing: readonly PeerSymbolRequirement[],
163
+ ): string {
164
+ const symbols = missing.map((m) => m.symbol).join(", ");
165
+ return `Pi SDK < ${PEER_FLOOR_VERSION}: missing ${symbols} — upgrade the Pi SDK to restore full functionality`;
166
+ }
167
+
168
+ /** Actionable error for a call site that cannot degrade gracefully. */
169
+ export function missingSymbolError(symbol: string): Error {
170
+ const requirement = PEER_SYMBOL_REQUIREMENTS.find(
171
+ (r) => r.symbol === symbol,
172
+ );
173
+ const pkg = requirement?.pkg ?? "@earendil-works/pi-*";
174
+ const feature = requirement?.usedBy ?? symbol;
175
+ return new Error(
176
+ `${feature} needs ${symbol} from ${pkg}, which the installed Pi SDK does not provide — upgrade @earendil-works/pi-* to ${PEER_FLOOR_VERSION} or newer`,
177
+ );
178
+ }
179
+
180
+ /**
181
+ * Call-site guard: fail with the actionable message instead of letting a
182
+ * missing export surface later as "x is not a function".
183
+ */
184
+ export function requirePeerSymbol<T>(
185
+ value: T | undefined | null,
186
+ symbol: string,
187
+ ): T {
188
+ if (value === undefined || value === null) throw missingSymbolError(symbol);
189
+ return value;
190
+ }
191
+
192
+ let noticeDelivered = false;
193
+
194
+ /** Minimal slice of Pi's ExtensionContext the warning needs. */
195
+ export interface PeerGuardContext {
196
+ hasUI?: boolean;
197
+ ui?: {
198
+ notify(message: string, level?: "warning" | "error" | "info"): void;
199
+ };
200
+ }
201
+
202
+ /**
203
+ * One-shot UI warning at session start. `check` is injectable so tests can
204
+ * drive the missing-symbol path without an under-floor SDK.
205
+ */
206
+ export function maybeNotifyPeerGuard(
207
+ ctx: PeerGuardContext | undefined,
208
+ check: () => PeerSymbolRequirement[] = checkPeerRuntime,
209
+ ): void {
210
+ if (noticeDelivered || !ctx?.hasUI || typeof ctx.ui?.notify !== "function") {
211
+ return;
212
+ }
213
+ const missing = check();
214
+ if (missing.length === 0) return;
215
+ noticeDelivered = true;
216
+ ctx.ui.notify(formatPeerGuardNotice(missing), "warning");
217
+ }
218
+
219
+ /** Reset the one-shot latch (tests only). */
220
+ export function resetPeerGuardNotice(): void {
221
+ noticeDelivered = false;
222
+ }
@@ -36,13 +36,33 @@ export function queueItemDescription(item: PowerlineQueueItem): string {
36
36
  return item.target.alias ? `@${item.target.alias}` : item.target.cwd;
37
37
  }
38
38
 
39
+ /**
40
+ * Pi's working indicator (spinner frame + "Working...") can end up at the start
41
+ * of captured text when the editor buffer is captured while the agent runs.
42
+ * Strip that prefix so ideas never render as a permanently "working" card.
43
+ */
44
+ const CAPTURE_NOISE_PREFIX =
45
+ /^[\u2800-\u28FF][\u2800-\u28FF ]*\s*Working[.…]+\s*/;
46
+
47
+ export function stripCaptureNoisePrefix(text: string): string {
48
+ let out = text;
49
+ while (CAPTURE_NOISE_PREFIX.test(out)) {
50
+ out = out.replace(CAPTURE_NOISE_PREFIX, "");
51
+ }
52
+ return out.trimStart();
53
+ }
54
+
39
55
  export function captureQueueItem(
40
56
  rt: RuntimeState,
41
57
  ctx: any,
42
- text: string,
58
+ rawText: string,
43
59
  intent: QueueIntent,
44
60
  target: QueueTarget,
45
61
  ): PowerlineQueueItem {
62
+ const text = stripCaptureNoisePrefix(rawText);
63
+ if (!text.trim()) {
64
+ throw new Error("Nothing to capture");
65
+ }
46
66
  const item = rt.queueStore.add({
47
67
  text,
48
68
  intent,
@@ -15,8 +15,20 @@ import {
15
15
  PRESET_NAMES,
16
16
  setConfig,
17
17
  } from "../core/state.ts";
18
+ import {
19
+ checkPeerRuntime,
20
+ formatPeerGuardWarning,
21
+ } from "../core/peer-guard.ts";
18
22
 
19
23
  export default function powerlineFooter(pi: ExtensionAPI) {
24
+ // peerDependencies are "*" by contract, so the manifest cannot express the
25
+ // real SDK floor. Check the imported symbol set up front and say what is
26
+ // missing, instead of letting an under-floor host fail at first use.
27
+ const missingPeers = checkPeerRuntime();
28
+ if (missingPeers.length > 0) {
29
+ console.warn(`[wishcraft] ${formatPeerGuardWarning(missingPeers)}`);
30
+ }
31
+
20
32
  const startupSettings = readSettings();
21
33
  // Ambient UI language: resolved before anything renders, so the very first
22
34
  // frame of the status line and welcome overlay is already in the right
@@ -53,6 +53,7 @@ import {
53
53
  setCustomCompactionEnabled,
54
54
  } from "../core/state.ts";
55
55
  import { CONTEXT_STATUS_RENDER_MS } from "../core/constants.ts";
56
+ import { maybeNotifyPeerGuard } from "../core/peer-guard.ts";
56
57
  import type { RuntimeState } from "../core/types.ts";
57
58
  import { isStaleExtensionContextError } from "./stale-context.ts";
58
59
  import { dismissWelcome } from "../welcome/welcome-control.ts";
@@ -139,6 +140,7 @@ export function registerSessionLifecycle(
139
140
  registerCustomSegments(config.segments);
140
141
  registerCustomPresets(config.presets);
141
142
  warnInvalidSegmentSettings(ctx);
143
+ maybeNotifyPeerGuard(ctx);
142
144
  rt.stashedPromptHistory = readPersistedStashHistory();
143
145
  rt.bashModeActive = false;
144
146
  rt.bashTranscript = new BashTranscriptStore(rt.bashModeSettings);
@@ -8,6 +8,8 @@ import type {
8
8
  Model,
9
9
  ProviderStreamOptions,
10
10
  } from "@earendil-works/pi-ai";
11
+ import { normalizeContext } from "@earendil-works/pi-ai";
12
+ import { requirePeerSymbol } from "../extension/core/peer-guard.ts";
11
13
  import { VIBE_SYSTEM_PROMPT, vibeState } from "./storage.ts";
12
14
 
13
15
  // ═══════════════════════════════════════════════════════════════════════════
@@ -37,7 +39,14 @@ export async function completeVibe(
37
39
 
38
40
  const baseUrl = (await registry.getProviderAuth(providerId))?.auth.baseUrl;
39
41
  const requestModel = baseUrl ? { ...model, baseUrl } : model;
40
- return provider.stream(requestModel, context, options).result();
42
+ // `Provider.stream()` now takes a branded `TranscriptContext`, which only
43
+ // `normalizeContext()` produces. It folds `systemPrompt` and `tools` into a
44
+ // leading system message; the brand exists so a raw `Context` cannot reach
45
+ // provider code unnormalized. `requirePeerSymbol` turns a missing export on
46
+ // an under-floor Pi SDK into an actionable error instead of
47
+ // "normalizeContext is not a function".
48
+ const normalize = requirePeerSymbol(normalizeContext, "normalizeContext");
49
+ return provider.stream(requestModel, normalize(context), options).result();
41
50
  }
42
51
 
43
52
  // ═══════════════════════════════════════════════════════════════════════════