pgserve 1.2.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.genie/brainstorms/pgserve-v2/DESIGN.md +174 -0
- package/.genie/wishes/pgserve-v2/BRIEF-from-genie-pgserve.md +99 -0
- package/.genie/wishes/pgserve-v2/WISH.md +442 -0
- package/.genie/wishes/release-system-genie-pattern/WISH.md +9 -9
- package/.genie/wishes/release-system-genie-pattern/validation.md +43 -10
- package/.github/workflows/ci.yml +8 -4
- package/.github/workflows/version.yml +2 -2
- package/CHANGELOG.md +150 -0
- package/README.md +186 -1
- package/bin/pglite-server.js +253 -1
- package/eslint.config.js +2 -0
- package/package.json +1 -1
- package/src/admin-client.js +171 -0
- package/src/audit.js +168 -0
- package/src/control-db.js +313 -0
- package/src/daemon-control.js +408 -0
- package/src/daemon-shared.js +18 -0
- package/src/daemon-tcp.js +296 -0
- package/src/daemon.js +629 -0
- package/src/fingerprint.js +453 -0
- package/src/gc.js +351 -0
- package/src/index.js +11 -0
- package/src/protocol.js +131 -0
- package/src/router.js +8 -0
- package/src/tenancy.js +75 -0
- package/src/tokens.js +102 -0
- package/tests/audit.test.js +189 -0
- package/tests/control-db.test.js +285 -0
- package/tests/daemon-fingerprint-integration.test.js +109 -0
- package/tests/daemon-pr24-regression.test.js +201 -0
- package/tests/fingerprint.test.js +249 -0
- package/tests/fixtures/240-orphan-seed.sql +30 -0
- package/tests/orphan-cleanup.test.js +390 -0
- package/tests/tcp-listen.test.js +368 -0
- package/tests/tenancy.test.js +403 -0
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# DESIGN — pgserve v2 (consolidated from genie-pgserve agent brain)
|
|
2
|
+
|
|
3
|
+
| Field | Value |
|
|
4
|
+
|-------|-------|
|
|
5
|
+
| **Status** | CRYSTALLIZED |
|
|
6
|
+
| **Origin** | Council v2 deliberation (`conv-bf3e8657`, 2026-04-26) — total convergence in Round 2 |
|
|
7
|
+
| **Source agent** | `genie-pgserve` (`/home/genie/workspace/agents/genie-pgserve`) |
|
|
8
|
+
| **Source docs** | `brain/_decisions/pgserve-roadmap-design.md` + `brain/_decisions/pgserve-roadmap-open-questions-resolved.md` |
|
|
9
|
+
| **Council members** | questioner, architect, simplifier, ergonomist |
|
|
10
|
+
| **Slug** | `pgserve-v2` |
|
|
11
|
+
|
|
12
|
+
## Problem
|
|
13
|
+
|
|
14
|
+
pgserve = "Neon for AI agents" — embedded Postgres-as-a-service for Node.js apps. Tagline: "npx pgserve and it just works, no credentials needed." `postgres/postgres` superuser is intentional product DNA.
|
|
15
|
+
|
|
16
|
+
Production usage growing across 6 Namastex apps (brain, omni, rlmx, genie, hapvida-eugenia, email). Pain points:
|
|
17
|
+
|
|
18
|
+
1. Each app spawns its own pgserve → port conflicts.
|
|
19
|
+
2. 240+ orphaned test DBs accumulated (no ownership, no GC) — caught a 2,130 errors/sec outage on 2026-04-24 (PR #24 fix).
|
|
20
|
+
3. No isolation — any app can see any other app's data (shared superuser by design).
|
|
21
|
+
4. PR #16 attempted schema-per-name + role-per-tenant + deny-by-default — rejected because consumer-owns-naming felt wrong.
|
|
22
|
+
|
|
23
|
+
## Goal
|
|
24
|
+
|
|
25
|
+
Cut pgserve **v2.0.0** — breaking semver bump (deliberately violating the original "we do not break userspace" plan). Replace v1's per-app TCP spawn + shared-superuser-without-isolation with a portless, fingerprinted, kernel-rooted, lifecycle-managed model. Use `automagik-dev/genie` as the canary consumer (dogfood loop) to validate the design empirically before broader migration.
|
|
26
|
+
|
|
27
|
+
The original design (`pgserve-roadmap-design.md`) staged this evolution v1.0 → v2.0 across 5 ABI-compatible releases. Felipe's direction on 2026-04-26 collapsed this into a single v2.0.0 cut, accepting the breakage cost in exchange for shorter cycle time and aligning the breaking semver with the actual breaking change.
|
|
28
|
+
|
|
29
|
+
## Approach
|
|
30
|
+
|
|
31
|
+
### 1. Transport — portless by default
|
|
32
|
+
|
|
33
|
+
- Singleton daemon binds well-known control socket at `$XDG_RUNTIME_DIR/pgserve/control.sock` (fallback `/tmp/pgserve/control.sock` for hosts without XDG_RUNTIME_DIR).
|
|
34
|
+
- Per-pid sockets remain for direct-embed callers (preserve PR #24 invariants — `_stopping` flag, exit-handler reset, router fallback-on-missing-socket).
|
|
35
|
+
- TCP only behind `--listen :PORT` opt-in (k8s pods, remote sync).
|
|
36
|
+
- **Kills port conflicts forever** — no ports to conflict over by default.
|
|
37
|
+
|
|
38
|
+
### 2. Identity — kernel-rooted, package.json-keyed
|
|
39
|
+
|
|
40
|
+
**Tuple:** `(realpath(nearest-ancestor-package.json), name field, uid)` → `sha256(...).slice(0, 12)`.
|
|
41
|
+
|
|
42
|
+
Mechanism:
|
|
43
|
+
1. SO_PEERCRED on Unix socket → unforgeable `(pid, uid, gid)` from kernel.
|
|
44
|
+
2. pgserve walks up `/proc/$pid/cwd` to find nearest `package.json`.
|
|
45
|
+
3. Hash the tuple → 12 hex char fingerprint.
|
|
46
|
+
4. **Fallback** for scripts with no package.json: `(uid, sha256(cwd + cmdline[1]).slice(0, 12))`.
|
|
47
|
+
|
|
48
|
+
Why NOT others considered:
|
|
49
|
+
- ❌ `sha256(/proc/$pid/exe)` — every Node app resolves to `/usr/local/bin/node`, collision.
|
|
50
|
+
- ❌ `cmdline` — mutable (pm2/tsx/nodemon rewrite).
|
|
51
|
+
- ❌ `cwd` alone — different cwd in same project = different DBs (wrong).
|
|
52
|
+
- ✅ `package.json` realpath — stable across npm install, runtime swap (node→bun), git pull, sub-cd.
|
|
53
|
+
|
|
54
|
+
### 3. Tenancy — database-per-fingerprint (NOT schema-per)
|
|
55
|
+
|
|
56
|
+
Schema-per is "isolation theater" under shared superuser — `SET search_path` to anything, fully-qualified SELECTs across schemas, `pg_catalog` enumeration.
|
|
57
|
+
|
|
58
|
+
Database-per wins because:
|
|
59
|
+
- DROP DATABASE atomic → GC trivial (one statement).
|
|
60
|
+
- pg_dump per-app works as-is (backup boundary = isolation boundary).
|
|
61
|
+
- App still sees `postgres://postgres:postgres@.../app-db` with full superuser inside its DB → magic preserved.
|
|
62
|
+
- Cross-DB requires re-auth → proxy routes back → mechanical isolation, not policy.
|
|
63
|
+
|
|
64
|
+
Database name format: `app_<sanitized-name>_<12hex>`.
|
|
65
|
+
|
|
66
|
+
### 4. Lifecycle — 3-layer composition
|
|
67
|
+
|
|
68
|
+
| Layer | Mechanism |
|
|
69
|
+
|-------|-----------|
|
|
70
|
+
| Default | Ephemeral — auto-DROP when liveness signal lost AND TTL elapsed. |
|
|
71
|
+
| Liveness signal | `kill -0 $pid` or `stat /proc/$pid` — owner died starts TTL. |
|
|
72
|
+
| Grace window | TTL 24h since last connection — restart with same fingerprint reclaims its DB. |
|
|
73
|
+
| Override | `package.json: "pgserve": {"persist": true}` — disables both, durable until explicit drop. |
|
|
74
|
+
|
|
75
|
+
Composition: test DBs vanish minutes after exit, agent runs vanish 24h after last activity, production knowledge stores never vanish. Zero cron config-side, 240-orphan disease cures itself.
|
|
76
|
+
|
|
77
|
+
### 5. GC sweep — three composed triggers
|
|
78
|
+
|
|
79
|
+
| Trigger | When |
|
|
80
|
+
|---------|------|
|
|
81
|
+
| Opportunistic | Every new connection acquired through control socket (sample 1/N to avoid herd). |
|
|
82
|
+
| Periodic | Hourly daemon timer. |
|
|
83
|
+
| Boot | Daemon startup (catches orphans accumulated while daemon was down). |
|
|
84
|
+
|
|
85
|
+
All three call one `gcSweep()` function — no cron config, no consumer involvement.
|
|
86
|
+
|
|
87
|
+
### 6. Audit log — tiered
|
|
88
|
+
|
|
89
|
+
| Tier | Destination | Default | Introduced |
|
|
90
|
+
|------|-------------|---------|------------|
|
|
91
|
+
| 1 | `~/.pgserve/audit.log` (JSONL, rotating 50MB × 5) | ON | v2.0 |
|
|
92
|
+
| 2 | Local syslog (`pgserve.audit.target: "syslog"`) | OFF | v2.0 |
|
|
93
|
+
| 3 | HTTP webhook (`pgserve.audit.target: "url"`) | OFF | v2.1 |
|
|
94
|
+
|
|
95
|
+
Schema: `{ts, event, fingerprint, db, peer_uid, peer_pid, package_realpath, ...event_specific}`.
|
|
96
|
+
|
|
97
|
+
Events: `db_created`, `db_reaped_ttl`, `db_reaped_liveness`, `db_persist_honored`, `connection_routed`, `connection_denied_fingerprint_mismatch`, `enforcement_kill_switch_used`.
|
|
98
|
+
|
|
99
|
+
### 7. Enforcement — default-on with kill switch
|
|
100
|
+
|
|
101
|
+
- Default-ON in v2.0.
|
|
102
|
+
- `PGSERVE_DISABLE_FINGERPRINT_ENFORCEMENT=1` environment variable bypasses enforcement (panic kill switch for ops emergencies).
|
|
103
|
+
- Marked deprecated; removal slated for v3.0.
|
|
104
|
+
|
|
105
|
+
### 8. Monorepo behavior
|
|
106
|
+
|
|
107
|
+
Walk up from `/proc/$pid/cwd` to first `package.json` (deepest match wins). Matches Node's `require.resolve` semantics.
|
|
108
|
+
|
|
109
|
+
Edge case: `npm workspaces` runs from repo root → all members share root fingerprint → all share one DB. Documented; if isolation needed, run member directly: `cd packages/foo && bun run start`.
|
|
110
|
+
|
|
111
|
+
Future escape hatch (deferred): `pgserve.fingerprintRoot: "monorepo-root"` in package.json. Build only when demand surfaces.
|
|
112
|
+
|
|
113
|
+
### 9. Control schema — `pgserve_meta`
|
|
114
|
+
|
|
115
|
+
Lives in pgserve's own admin DB (separate from user DBs):
|
|
116
|
+
|
|
117
|
+
```sql
|
|
118
|
+
CREATE TABLE pgserve_meta (
|
|
119
|
+
database_name TEXT PRIMARY KEY,
|
|
120
|
+
fingerprint TEXT NOT NULL, -- 12 hex
|
|
121
|
+
peer_uid INTEGER NOT NULL,
|
|
122
|
+
package_realpath TEXT, -- NULL for script fallback
|
|
123
|
+
created_at TIMESTAMPTZ DEFAULT now(),
|
|
124
|
+
last_connection_at TIMESTAMPTZ DEFAULT now(),
|
|
125
|
+
liveness_pid INTEGER, -- last known owner pid
|
|
126
|
+
persist BOOLEAN DEFAULT false
|
|
127
|
+
);
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Decisions
|
|
131
|
+
|
|
132
|
+
| # | Decision | Rationale |
|
|
133
|
+
|---|----------|-----------|
|
|
134
|
+
| 1 | Single v2.0.0 cut, not staged | Felipe 2026-04-26: bundle the breaking changes under one semver-major. Cycle time over compat. |
|
|
135
|
+
| 2 | Portless default + Unix socket | Eliminates port conflicts (THE #1 embedded-server failure mode) + enables SO_PEERCRED for kernel-rooted identity. |
|
|
136
|
+
| 3 | package.json as identity key | Stable across npm install, runtime swap, git pull. npm already mandates it for unrelated reasons. |
|
|
137
|
+
| 4 | Database-per-fingerprint over schema-per | Real mechanical isolation vs theater under shared superuser; atomic GC; tool compat (pg_dump, drizzle, prisma). |
|
|
138
|
+
| 5 | Fingerprint hash truncated to 12 hex (48-bit) | Birthday-bound at ~16M projects. Postgres ident limit (63) leaves room for `app_<sanitized-name>_<12hex>`. |
|
|
139
|
+
| 6 | GC: opportunistic + hourly + boot, single sweep function | Bounds worst-case orphan lifetime ≤ 1h on idle hosts; immediate on active hosts. |
|
|
140
|
+
| 7 | Enforcement default-ON with `PGSERVE_DISABLE_FINGERPRINT_ENFORCEMENT=1` kill switch | Simplifier wins happy path; architect keeps emergency valve. |
|
|
141
|
+
| 8 | Monorepo: nearest-ancestor package.json wins | Matches Node `require.resolve`; familiar mental model. |
|
|
142
|
+
| 9 | Audit log tiered (file → syslog → webhook) | Zero-config promise honored at tier 1; ops opt into separate sink. |
|
|
143
|
+
| 10 | Dogfood `automagik-dev/genie` consumer in lockstep | Provides empirical safety net for the breaking cut; first canary before brain/omni/rlmx/eugenia/email migrate. |
|
|
144
|
+
| 11 | DELETE PR #16 schema/role machinery | Replaced by database boundary + peer-creds routing — fewer lines AND honest isolation. |
|
|
145
|
+
|
|
146
|
+
## Risks & Assumptions
|
|
147
|
+
|
|
148
|
+
| Risk | Severity | Mitigation |
|
|
149
|
+
|------|----------|------------|
|
|
150
|
+
| 5 other consumer apps (brain, omni, rlmx, hapvida-eugenia, email) break on v2.0 install | High | Pin v1.x in their package.json until per-app migration wishes ship. Document upgrade path in v2.0 release notes. |
|
|
151
|
+
| package.json walk fails on edge cases (worktree without root, monorepos) | Medium | Fallback to script-mode hash; document monorepo behavior; defer escape hatch until demanded. |
|
|
152
|
+
| Production knowledge store loses data on missed `persist: true` flag | High | Errors-that-teach: "Database for `myapp` was reaped — to survive long gaps, set `persist:true`. See pgserve.dev/persist". Pre-flight warning at 90% of TTL. |
|
|
153
|
+
| Daemon mode = single point of failure for whole machine | Low | pgserve daemon supervised (PM2/systemd); restart fast; existing apps already tolerated pgserve restarts (per-app spawn). |
|
|
154
|
+
| Existing 240 orphans contain sensitive data (PII from hapvida-eugenia, etc) | Medium | One-time inventory + classification BEFORE GC sweep on prod hosts. Separate ops task (out of this wish). |
|
|
155
|
+
| Genie consumer migration reveals design flaw mid-build | Medium | Dogfood twin reports daily; if blocking flaw surfaces, pause wish, reconvene council, possibly revert to staged plan. |
|
|
156
|
+
| PR #24's stale-socketDir invariants regress in daemon work | High | Wave 2 group must regression-test the three scenarios from #24 (stop nulls socketDir, double-start no-op, exit-handler resets state). |
|
|
157
|
+
|
|
158
|
+
## What was considered and rejected
|
|
159
|
+
|
|
160
|
+
- Use vanilla Postgres + 50-line script — pgserve IS the answer; vanilla lacks npx-magic embed.
|
|
161
|
+
- Per-app credentials in `.env` — leak via git/Slack/CI logs.
|
|
162
|
+
- Schema-per-fingerprint with search_path — isolation theater under shared superuser.
|
|
163
|
+
- Pure binary_hash fingerprint — Node apps all resolve to `/usr/local/bin/node`.
|
|
164
|
+
- Pure cwd fingerprint — different cwd in same project = different DBs.
|
|
165
|
+
- Consumer-supplied naming (PR #16) — pushes ownership to consumer, recreates naming problem.
|
|
166
|
+
- TTL-only lifecycle (24h universal) — risks "production data vanished after long weekend".
|
|
167
|
+
- ps-aux-only liveness — production knowledge store on host that crashes for 25h would lose data invisibly.
|
|
168
|
+
- ABI-compatible 5-stage rollout (`pgserve-roadmap-design.md` original plan) — superseded by Felipe's 2026-04-26 call to bundle as v2.0.
|
|
169
|
+
|
|
170
|
+
## Open follow-ups (not blockers for this wish)
|
|
171
|
+
|
|
172
|
+
- One-time inventory + classification of existing 240 orphans on prod hosts (separate ops task).
|
|
173
|
+
- Migration wishes for the 5 non-genie consumers (one per app: brain, omni, rlmx, hapvida-eugenia, email).
|
|
174
|
+
- Future: cross-host coordination, encryption-at-rest, TLS, multi-tenant role permissions.
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# Brief from genie-pgserve (parent agent)
|
|
2
|
+
|
|
3
|
+
Read this AFTER WISH.md. Critical operational context that supersedes parts of the wish.
|
|
4
|
+
|
|
5
|
+
## CRITICAL — genie wish-parser regression is active
|
|
6
|
+
|
|
7
|
+
Your CLI version is **4.260426.4**. Both `genie work pgserve-v2` and `genie wish status pgserve-v2` will fail with:
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
❌ Group "N" depends on non-existent group "pgserve-v2#M"
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
**Do not interpret that error as a wish problem.** The wish is well-formed.
|
|
14
|
+
|
|
15
|
+
**Root cause** (already debugged):
|
|
16
|
+
- `automagik-dev/genie` `src/term-commands/dispatch.ts:189-197` — `parseWishGroups` stores group names as bare ids (`"1"`, `"2"`) but does NOT strip the `<slug>#` prefix from depends-on entries.
|
|
17
|
+
- Then `src/lib/wish-state.ts:240` — `validateGroupRefs` does `groupNames.has("pgserve-v2#1")` against `{"0".."8"}` and throws.
|
|
18
|
+
- Affects EVERY wish using the canonical `<slug>#<n>` depends-on form, including the shipped reference `release-system-genie-pattern`.
|
|
19
|
+
|
|
20
|
+
**GH issue filed**: https://github.com/automagik-dev/genie/issues/1406
|
|
21
|
+
**Fix-wish draft**: `/home/genie/workspace/agents/genie-pgserve/brain/_decisions/genie-parser-fix-wish-draft.md`
|
|
22
|
+
|
|
23
|
+
## Workaround you must use
|
|
24
|
+
|
|
25
|
+
Bypass `genie work` orchestration. Dispatch each group manually:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
genie spawn engineer # or reviewer / fix / qa
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Then `genie send` a curated prompt to the spawned engineer with this structure (per the /work skill's Context Curation rules — DO NOT just say "read WISH.md"):
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
Execute Group N of wish "pgserve-v2".
|
|
35
|
+
|
|
36
|
+
Goal: <one sentence from WISH.md>
|
|
37
|
+
|
|
38
|
+
Deliverables:
|
|
39
|
+
1. <copy from WISH.md>
|
|
40
|
+
2. <copy from WISH.md>
|
|
41
|
+
|
|
42
|
+
Acceptance Criteria:
|
|
43
|
+
- [ ] <copy from WISH.md>
|
|
44
|
+
- [ ] <copy from WISH.md>
|
|
45
|
+
|
|
46
|
+
Validation:
|
|
47
|
+
<copy the bash block from WISH.md>
|
|
48
|
+
|
|
49
|
+
Depends-on: <human-resolved — group N from this wish, already complete>
|
|
50
|
+
|
|
51
|
+
Repo: /home/genie/workspace/repos/pgserve
|
|
52
|
+
Branch: pgserve-v2 (from wish/pgserve-v2)
|
|
53
|
+
Worktree: /home/genie/.genie/worktrees/pgserve/pgserve-v2
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Track group state via your own scratchpad in this worktree (e.g. `STATUS.md` next to this brief). Mark groups done as engineers report PASS.
|
|
57
|
+
|
|
58
|
+
## Execution order — start parallel waves NOW
|
|
59
|
+
|
|
60
|
+
| Wave | Groups | When |
|
|
61
|
+
|------|--------|------|
|
|
62
|
+
| **0** | Group 0 (dogfooder twin) | Spawn now, runs continuously |
|
|
63
|
+
| **1** | Group 1 (control DB + audit infra) | Sequential foundation, spawn now in parallel with Group 0 |
|
|
64
|
+
| **2** | Group 2 (daemon) ‖ Group 3 (fingerprint) | After Group 1 ships |
|
|
65
|
+
| **3** | Group 4 (per-fp DB enforcement) | After Wave 2 ships |
|
|
66
|
+
| **4** | Group 5 (lifecycle/GC) ‖ Group 6 (--listen TCP) | After Group 4 ships |
|
|
67
|
+
| **5** | Group 7 (genie consumer migration) | After Wave 4 ships |
|
|
68
|
+
| **6** | Group 8 (release prep, ship 2.0.0) | After Group 7 ships |
|
|
69
|
+
|
|
70
|
+
## Important consumer-repo correction
|
|
71
|
+
|
|
72
|
+
The canary consumer for Group 7 / Group 0 dogfooder is **`automagik-dev/genie`**, cloned at **`/home/genie/workspace/repos/genie`**. NOT `automagik-genie` (no such repo). NOT `namastexlabs/genie`. The wish has been corrected but your initial mental model may have absorbed the earlier wrong name from the genie team-lead system prompt. Verify with `git -C /home/genie/workspace/repos/genie remote -v` before any consumer-side work.
|
|
73
|
+
|
|
74
|
+
## Felipe's standing constraints
|
|
75
|
+
|
|
76
|
+
From `agents/genie-pgserve/AGENTS.md`:
|
|
77
|
+
- **Don't restart the running pgserve daemon** at PID 160588 (orphaned but Felipe is using it for the email brain).
|
|
78
|
+
- **Don't drop any `brain_*` databases** without Felipe's explicit OK.
|
|
79
|
+
- **Don't merge PR #16** in `namastexlabs/pgserve` — it's superseded by this wish (delete schema/role machinery, use database-per-fingerprint).
|
|
80
|
+
- **Don't propose substituting pgserve with vanilla Postgres** — pgserve IS the answer.
|
|
81
|
+
- **Don't spawn pgserve daemons for testing** — use ephemeral test instances per the wish's test fixtures, never daemon mode in tests.
|
|
82
|
+
|
|
83
|
+
## Reporting back
|
|
84
|
+
|
|
85
|
+
Cross-team scope is locked one-way (you → me, not me → you). Report status, blockers, completions to me via:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
genie send '<msg>' --to genie-pgserve --bridge
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
I will relay any course-correction signals from Felipe back to you via this BRIEF file (I'll append a `## Update <timestamp>` section). Re-read this file at the start of each wave.
|
|
92
|
+
|
|
93
|
+
## Felipe's non-negotiable for this wish
|
|
94
|
+
|
|
95
|
+
- 1.2.0 is the last non-breaking version on the v1 line; lock it.
|
|
96
|
+
- 2.0.0 is THIS wish's target — single breaking cut, not staged.
|
|
97
|
+
- The 5 non-genie consumers (brain, omni, rlmx, hapvida-eugenia, email) MUST be advised to pin `pgserve@^1.x` before 2.0.0 ships. Group 8's release prep includes that advisory; do not skip it.
|
|
98
|
+
|
|
99
|
+
Good hunting.
|