@groeponline/pi-wishcraft 1.15.0 → 1.17.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 +12 -0
- package/CHANGELOG.md +21 -1
- package/build-stamp.json +1 -1
- package/docs/agents/domain.md +60 -0
- package/docs/agents/issue-tracker.md +88 -0
- package/package.json +3 -1
- package/src/extension/core/peer-guard.ts +222 -0
- package/src/extension/queue/queue-integration.ts +21 -1
- package/src/extension/session/activate.ts +12 -0
- package/src/extension/session/session-lifecycle.ts +2 -0
- package/src/working-vibes/provider.ts +6 -2
package/AGENTS.md
CHANGED
|
@@ -252,6 +252,8 @@ function createRuntimeState(hooks: {
|
|
|
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
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
|
|
|
@@ -320,3 +322,13 @@ scripts/docker-test.sh -n 4 # 4 parallel containers
|
|
|
320
322
|
```
|
|
321
323
|
|
|
322
324
|
Each container runs `typecheck + test + circular` with a shared read-only `node_modules` bind mount.
|
|
325
|
+
|
|
326
|
+
## Agent skills
|
|
327
|
+
|
|
328
|
+
### Issue tracker
|
|
329
|
+
|
|
330
|
+
Dual-tracker: Linear for planning/features, GitHub for bugs/triage. See `docs/agents/issue-tracker.md`.
|
|
331
|
+
|
|
332
|
+
### Domain docs
|
|
333
|
+
|
|
334
|
+
Single-context. Current domain source: AGENTS.md. CONTEXT.md and docs/adr/ to be added via `/domain-modeling` when needed. See `docs/agents/domain.md`.
|
package/CHANGELOG.md
CHANGED
|
@@ -2,11 +2,31 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [1.17.0] - 2026-10-27
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
- Bash paste-while-running (issue #72): a bracketed paste performed while a command runs is stripped of its markers and forwarded to the child stdin — including split delivery across input chunks — instead of silently queuing in the editor buffer behind the command.
|
|
9
|
+
- PTY/transcript memory bounds (from jan `fix/runtime-pty-bounds`): a single unterminated output line is capped at a 64 KiB UTF-8 tail with a one-time notice, the partial-escape tail is bounded, and the active transcript command trims head lines/bytes within limits instead of growing unbounded.
|
|
10
|
+
- Segment-options spread hygiene (CodeFactor #86–89): `normalizeSegmentOptions` assigns fields imperatively instead of spreading conditional empty objects.
|
|
11
|
+
|
|
12
|
+
## [1.16.0] - 2026-10-03
|
|
13
|
+
|
|
14
|
+
### Known limitations
|
|
15
|
+
- **`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).
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
- **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.
|
|
19
|
+
- **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.
|
|
20
|
+
- `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.
|
|
21
|
+
- **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.
|
|
22
|
+
- `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".
|
|
23
|
+
- 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.
|
|
24
|
+
|
|
5
25
|
## [1.15.0] - 2026-10-01
|
|
6
26
|
|
|
7
27
|
### Changed
|
|
8
28
|
- **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.
|
|
9
|
-
- Audit findings drop from 3 (1 moderate, 2 high) to 1
|
|
29
|
+
- 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].)
|
|
10
30
|
|
|
11
31
|
## [1.14.0] - 2026-10-01
|
|
12
32
|
|
package/build-stamp.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"source_sha":"
|
|
1
|
+
{"source_sha":"0c1a77045865dbb0dd6138ac1d0b381c067d0395"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Domain Docs
|
|
2
|
+
|
|
3
|
+
How the engineering skills should consume this repo's domain documentation when exploring the codebase.
|
|
4
|
+
|
|
5
|
+
## Before exploring, read these
|
|
6
|
+
|
|
7
|
+
- **`AGENTS.md`** at the repo root — the current domain documentation source with architecture, data flow, and conventions.
|
|
8
|
+
- **`CONTEXT.md`** at the repo root, if it exists (currently absent).
|
|
9
|
+
- **`CONTEXT-MAP.md`** at the repo root if it exists: it points at one `CONTEXT.md` per context. Read each one relevant to the topic.
|
|
10
|
+
- **`docs/adr/`**: read ADRs that touch the area you're about to work in. In multi-context repos, also check `src/<context>/docs/adr/` for context-scoped decisions.
|
|
11
|
+
|
|
12
|
+
If `CONTEXT.md` or `docs/adr/` don't exist, **proceed silently**. Don't flag their absence; don't suggest creating them upfront. The `/domain-modeling` skill (reached via `/grill-with-docs` and `/improve-codebase-architecture`) creates them lazily when terms or decisions actually get resolved.
|
|
13
|
+
|
|
14
|
+
## File structure
|
|
15
|
+
|
|
16
|
+
Single-context repo (this repo):
|
|
17
|
+
|
|
18
|
+
**Current state:**
|
|
19
|
+
- `AGENTS.md` — Primary domain documentation (architecture, data flow, conventions)
|
|
20
|
+
- `docs/` — Operator guides and design specs (no ADRs currently)
|
|
21
|
+
- `CONTEXT.md` — Not yet created (add via `/domain-modeling` when needed)
|
|
22
|
+
|
|
23
|
+
**Target state (when CONTEXT.md exists):**
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
/
|
|
27
|
+
├── AGENTS.md
|
|
28
|
+
├── CONTEXT.md
|
|
29
|
+
├── docs/adr/
|
|
30
|
+
│ ├── 0001-event-sourced-orders.md
|
|
31
|
+
│ └── 0002-postgres-for-write-model.md
|
|
32
|
+
└── src/
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Multi-context repo (presence of `CONTEXT-MAP.md` at the root):
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
/
|
|
39
|
+
├── CONTEXT-MAP.md
|
|
40
|
+
├── docs/adr/ ← system-wide decisions
|
|
41
|
+
└── src/
|
|
42
|
+
├── ordering/
|
|
43
|
+
│ ├── CONTEXT.md
|
|
44
|
+
│ └── docs/adr/ ← context-specific decisions
|
|
45
|
+
└── billing/
|
|
46
|
+
├── CONTEXT.md
|
|
47
|
+
└── docs/adr/
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Use the glossary's vocabulary
|
|
51
|
+
|
|
52
|
+
When your output names a domain concept (in an issue title, a refactor proposal, a hypothesis, a test name), use the term as defined in `AGENTS.md` (current domain source) or `CONTEXT.md` (if it exists). Don't drift to synonyms the glossary explicitly avoids.
|
|
53
|
+
|
|
54
|
+
If the concept you need isn't in the glossary yet, that's a signal: either you're inventing language the project doesn't use (reconsider) or there's a real gap (note it for `/domain-modeling`).
|
|
55
|
+
|
|
56
|
+
## Flag ADR conflicts
|
|
57
|
+
|
|
58
|
+
If your output contradicts an existing ADR, surface it explicitly rather than silently overriding:
|
|
59
|
+
|
|
60
|
+
> _Contradicts ADR-0007 (event-sourced orders), but worth reopening because…_
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# Issue tracker: Linear + GitHub
|
|
2
|
+
|
|
3
|
+
This repo uses a dual-tracker workflow:
|
|
4
|
+
- **Linear**: Long-term planning, roadmap, feature tracking
|
|
5
|
+
- **GitHub**: Bug reports, PR-linked issues, immediate triage
|
|
6
|
+
|
|
7
|
+
## Conventions
|
|
8
|
+
|
|
9
|
+
### GitHub (primary for bugs and triage)
|
|
10
|
+
- **Create an issue**: `gh issue create --title "..." --body "..."`. Use a heredoc for multi-line bodies.
|
|
11
|
+
- **Read an issue**: `gh issue view <number> --comments`, filtering comments by `jq` and also fetching labels.
|
|
12
|
+
- **List issues**: `gh issue list --state open --json number,title,body,labels,comments --jq '[.[] | {number, title, body, labels: [.labels[].name], comments: [.comments[].body]}]'` with appropriate `--label` and `--state` filters.
|
|
13
|
+
- **Comment on an issue**: `gh issue comment <number> --body "..."`
|
|
14
|
+
- **Apply / remove labels**: `gh issue edit <number> --add-label "..."` / `--remove-label "..."`
|
|
15
|
+
- **Close**: `gh issue close <number> --comment "..."`
|
|
16
|
+
|
|
17
|
+
Infer the repo from `git remote -v`; `gh` does this automatically when run inside a clone.
|
|
18
|
+
|
|
19
|
+
### Linear (for planning and features)
|
|
20
|
+
- **Create a ticket**: `linear issue create --title "..." --description "..." --team <team-id>`. Use `linear teams list` to find team IDs.
|
|
21
|
+
- **Read a ticket**: `linear issue view <ticket-id>` or `linear issue search --query "<title>"`.
|
|
22
|
+
- **List tickets**: `linear issue list --team <team-id> --state <state>` with appropriate filters (e.g., `--state Backlog`, `--state In Progress`).
|
|
23
|
+
- **Comment on a ticket**: `linear comment create <ticket-id> --body "..."`
|
|
24
|
+
- **Change state**: `linear issue update <ticket-id> --state <state>`
|
|
25
|
+
- **Assign**: `linear issue update <ticket-id> --assignee <user-id>`
|
|
26
|
+
- **Link to GitHub**: Add a GitHub issue URL in the Linear ticket description, or use Linear's GitHub integration if configured.
|
|
27
|
+
|
|
28
|
+
Use Linear for feature planning, roadmap items, and long-term tracking. Reference Linear tickets in GitHub issue bodies when applicable. Link GitHub PRs to related Linear tickets for traceability.
|
|
29
|
+
|
|
30
|
+
### Linear conventions
|
|
31
|
+
|
|
32
|
+
**Labels** (create in Linear):
|
|
33
|
+
- `type:feature` — New features or enhancements
|
|
34
|
+
- `type:bug` — Bugs that belong in the roadmap (not urgent)
|
|
35
|
+
- `type:refactor` — Technical debt or architectural work
|
|
36
|
+
- `type:research` — Investigation or exploration tasks
|
|
37
|
+
- `priority:high` / `priority:medium` / `priority:low` — Priority levels
|
|
38
|
+
|
|
39
|
+
**States** (use Linear's default workflow or customize):
|
|
40
|
+
- `Backlog` — Planned but not started
|
|
41
|
+
- `In Progress` — Active work
|
|
42
|
+
- `In Review` — Awaiting review
|
|
43
|
+
- `Done` — Completed
|
|
44
|
+
- `Canceled` — Declined or superseded
|
|
45
|
+
|
|
46
|
+
## Pull requests as a triage surface
|
|
47
|
+
|
|
48
|
+
**PRs as a request surface: no.** _(Set to `yes` if this repo treats external PRs as feature requests; `/triage` reads this flag.)_
|
|
49
|
+
|
|
50
|
+
When set to `yes`, PRs run through the same labels and states as issues, using the `gh pr` equivalents:
|
|
51
|
+
|
|
52
|
+
- **Read a PR**: `gh pr view <number> --comments` and `gh pr diff <number>` for the diff.
|
|
53
|
+
- **List external PRs for triage**: `gh pr list --state open --json number,title,body,labels,author,comments` then fetch `authorAssociation` per PR with `gh api repos/:owner/:repo/pulls/<number> --jq '.author_association'` (the `gh pr list --json` field set does not include `authorAssociation`), keeping only `CONTRIBUTOR`, `FIRST_TIME_CONTRIBUTOR`, or `NONE` (drop `OWNER`/`MEMBER`/`COLLABORATOR`).
|
|
54
|
+
- **Comment / label / close**: `gh pr comment`, `gh pr edit --add-label`/`--remove-label`, `gh pr close`.
|
|
55
|
+
|
|
56
|
+
GitHub shares one number space across issues and PRs, so a bare `#42` may be either: resolve with `gh pr view 42` and fall back to `gh issue view 42`.
|
|
57
|
+
|
|
58
|
+
## When a skill says "publish to the issue tracker"
|
|
59
|
+
|
|
60
|
+
**Use GitHub when:**
|
|
61
|
+
- Bug reports or regressions
|
|
62
|
+
- Urgent issues requiring immediate triage
|
|
63
|
+
- PR-linked issues or feedback from code review
|
|
64
|
+
- Questions or clarifications about implementation
|
|
65
|
+
|
|
66
|
+
**Use Linear when:**
|
|
67
|
+
- Feature proposals or enhancements
|
|
68
|
+
- Roadmap items or long-term planning
|
|
69
|
+
- Research tasks or investigations
|
|
70
|
+
- Technical debt or architectural refactoring
|
|
71
|
+
- Multi-step work that benefits from project tracking
|
|
72
|
+
|
|
73
|
+
If unsure, default to GitHub for bugs/immediate work, Linear for features/planning.
|
|
74
|
+
|
|
75
|
+
## When a skill says "fetch the relevant ticket"
|
|
76
|
+
|
|
77
|
+
Run `gh issue view <number> --comments` for GitHub issues. For Linear tickets, use the Linear CLI or API.
|
|
78
|
+
|
|
79
|
+
## Wayfinding operations
|
|
80
|
+
|
|
81
|
+
Used by `/wayfinder`. The **map** is a single Linear ticket with **child** tickets as tasks.
|
|
82
|
+
|
|
83
|
+
- **Map**: a single Linear ticket labelled `wayfinder:map`, holding the Notes / Decisions-so-far / Fog body. `linear issue create --title "Map: <topic>" --description "..." --team <team-id> --label wayfinder:map`.
|
|
84
|
+
- **Child ticket**: a Linear ticket linked to the map as a parent using Linear's parent-child relationship. Add the child to the map's sub-issues and put `Part of <map-id>` at the top of the child description. Labels: `wayfinder:<type>` (`research`/`prototype`/`grilling`/`task`). Once claimed, the ticket is assigned to the driving dev.
|
|
85
|
+
- **Blocking**: Use Linear's **native issue dependencies**. Add a blocker with `linear issue update <child> --blocking <blocker-id>`. Linear reports blocking status in the UI. A ticket is unblocked when every blocker is resolved.
|
|
86
|
+
- **Frontier query**: list the map's open children (`linear issue list --team <team-id> --parent <map-id> --state Backlog,In Progress`), drop any with an open blocker or an assignee; first in map order wins.
|
|
87
|
+
- **Claim**: `linear issue update <ticket-id> --assignee @me`, the session's first write.
|
|
88
|
+
- **Resolve**: `linear comment create <ticket-id> --body "<answer>"`, then `linear issue update <ticket-id> --state Done`, then append a context pointer (gist + link) to the map's Decisions-so-far.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@groeponline/pi-wishcraft",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.17.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
|
},
|
|
@@ -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
|
+
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
|
+
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
|
-
|
|
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);
|
|
@@ -9,6 +9,7 @@ import type {
|
|
|
9
9
|
ProviderStreamOptions,
|
|
10
10
|
} from "@earendil-works/pi-ai";
|
|
11
11
|
import { normalizeContext } from "@earendil-works/pi-ai";
|
|
12
|
+
import { requirePeerSymbol } from "../extension/core/peer-guard.ts";
|
|
12
13
|
import { VIBE_SYSTEM_PROMPT, vibeState } from "./storage.ts";
|
|
13
14
|
|
|
14
15
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -41,8 +42,11 @@ export async function completeVibe(
|
|
|
41
42
|
// `Provider.stream()` now takes a branded `TranscriptContext`, which only
|
|
42
43
|
// `normalizeContext()` produces. It folds `systemPrompt` and `tools` into a
|
|
43
44
|
// leading system message; the brand exists so a raw `Context` cannot reach
|
|
44
|
-
// provider code unnormalized.
|
|
45
|
-
|
|
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();
|
|
46
50
|
}
|
|
47
51
|
|
|
48
52
|
// ═══════════════════════════════════════════════════════════════════════════
|