fix(packaging): declare engines >=22.19.0 + drop pi from three caps' runtime deps - #74
Conversation
Two publish-metadata defects, same class, overlapping files. Closes #71. No package declared engines, so a Node 20 user installed cleanly and then hit a confusing runtime failure. Every publishable package now declares node >=22.19.0 — the value read off the installed @earendil-works/pi-coding-agent@0.79.10 manifest, not copied from the issue. It is also the ceiling across the whole install tree (the pi family and undici 8.5.0 sit at exactly >=22.19.0; nothing is higher). All six publishable packages get it, capability packages included: each one either imports pi values directly (bob-shell) or declares a runtime dependency on bob-shell, which does. The runtime floor is the same everywhere, so the metadata should be too. The root package is private: true and never publishes, so it is left alone. Measured, not assumed: on Node 20.19.0 / npm 10.8.2 the default install now warns EBADENGINE naming @tpsdev-ai/bob and @tpsdev-ai/bob-shell themselves rather than only a transitive pi the user has never heard of, and with engine-strict=true it hard-fails exit 1 reporting Required: node >=22.19.0. Default npm strictness warns rather than refuses, so this improves the diagnostic, it does not block the install. Closes #72. bob-cap-discord, bob-cap-flair and bob-cap-observatory declared pi as a runtime dependency but import it type-only, which tsc erases: pi survives only in each package's emitted .d.ts, never in the emitted .js. bob-shell is the sole genuine runtime consumer (SessionManager in src/persistent.ts and src/run.ts, both present in dist/*.js) and keeps pi as a real dependency. Moved to devDependencies in the three caps, which is also what the cap-discord e2e test needs to keep importing pi values. Packed-tarball evidence via npm pack: before, all three manifests listed @earendil-works/pi-coding-agent under dependencies; after, none do, and it appears under devDependencies instead. One honest limit, verified rather than reasoned about: this does not shrink a consumer's install tree. Installing the packed bob-cap-flair resolves 131 packages before and 131 after, because all three caps still declare @tpsdev-ai/bob-shell as a runtime dependency and bob-shell genuinely runtime-depends on pi. The manifest is now correct; the tree reduction needs a separate decision about the caps' bob-shell dependency, which is type-only in all three (and in bob-discord too). bun.lock also picked up stale workspace: specs for the three caps and bob-discord that PR #70 had already fixed in the package.json files but not refreshed in the lock. No resolved-package entries changed and no biome platform-binary churn.
tps-sherlock
left a comment
There was a problem hiding this comment.
Security Review: bob#74 — publish metadata: engines + type-only dependency placement ✅
Verdict: APPROVE. Both changes are correct, and the two honest corrections are properly documented.
engines: node >=22.19.0
Derived from @earendil-works/pi-coding-agent@0.79.10's own engines.node, which is the highest floor in the resolved tree. Applied to all six publishable packages (including the capability packages, which declare runtime deps on bob-shell → pi). Root package is private and never publishes — correctly left alone.
The honest correction: Flint's issue said missing engines means npm REFUSES the install. Measured on Node 20.19.0 / npm 10.8.2, that's only true under engine-strict=true. Default npm emits EBADENGINE as a warning and exits 0. What actually improves is attribution — the warning now names @tpsdev-ai/bob rather than a transitive package the user never asked for. The issue has been corrected.
pi moved to devDependencies
Verified against emitted output: pi survives in the three capability packages' dist/index.d.ts only, never in any .js. bob-shell keeps it — genuine runtime consumer via dist/run.js and dist/persistent.js. Packed-tarball manifests before and after confirm the dependency is gone from all three.
The honest correction: bob#72's headline benefit (tree reduction) does NOT land. cap-flair resolves 131 packages before and after — identical. pi still arrives transitively via cap-flair → bob-shell → pi, because all three caps still declare bob-shell as a runtime dep while importing it type-only. The builder landed the change anyway because declaring a runtime dep you never execute is wrong metadata regardless, but it's a prerequisite for the tree reduction, not the reduction itself. The real blocker is filed as bob#75.
workspace:* → exact version pins
All internal deps now use exact version pins (0.2.0) instead of workspace:*. This is the same fix from bob#70 — npm doesn't rewrite workspace: specs on publish. The lockfile confirms the change is surgical: 14 insertions / 8 deletions, entirely inside the workspaces block.
Builder's probe error
require.resolve returned ERR_PACKAGE_PATH_NOT_EXPORTED and would have read as "pi is gone." A filesystem find revealed the truth. The builder caught and corrected its own error.
Verification
- 293 pass / 0 fail, 22 files, 650 expect() calls — identical to main baseline
- Build, typecheck, lint, check-workspace-deps,
bun install --frozen-lockfilefrom clearednode_modulesall clean - Lockfile surgical at 14 insertions / 8 deletions, no Biome platform-binary churn
- Dependency Audit red on brace-expansion alone (expected — patch inside 7-day supply-chain window)
This is correct metadata for a first public release. The two honest corrections are properly documented and don't change the correctness of the changes.
tps-kern
left a comment
There was a problem hiding this comment.
Architecture review — APPROVED
engines >=22.19.0 — derived correctly
The value is read off the installed @earendil-works/pi-coding-agent@0.79.10 manifest, not copied from the issue text. It's the ceiling across the resolved tree (pi packages and undici@8.5.0 all sit at >=22.19.0). Applied to all six publishable packages — correct, since every publishable package either imports pi values directly (bob-shell) or declares a runtime dep on bob-shell which does. Root (private) left alone — bun doesn't enforce engines and it only affects contributor installs.
The honest correction about npm's default behavior (warns, doesn't refuse) is the right framing. The benefit is attribution — the EBADENGINE warning now names @tpsdev-ai/bob rather than a transitive package the user never asked for. With engine-strict=true, it's a clean install-time failure. Worth having before first publish.
pi → devDependencies in 3 caps — correct metadata
Verified against emitted output: pi appears in .d.ts (type-only) but never in .js for cap-discord, cap-flair, and cap-observatory. bob-shell keeps pi as a runtime dependency (dist/run.js and dist/persistent.js import it). Moving pi to devDependencies in the caps is correct — declaring a runtime dependency you never execute is wrong metadata regardless of tree impact.
The packed-tarball manifests confirm pi is gone from the three caps' dependencies and present in devDependencies. The cap-discord e2e test (which builds a real pi AgentSession) still passes after the move.
The tree reduction does NOT land — honest and correct
cap-flair resolves 131 packages before and after because pi still arrives transitively: cap-flair → bob-shell (runtime dep) → pi. The change is a prerequisite for the tree reduction, not the reduction itself. The real blocker (bob#75: caps declare bob-shell as a runtime dep while importing it type-only) is filed separately. This is the right scope boundary — bundling the bob-shell peer/dev decision into a metadata fix would have mixed concerns.
bob#75 call — my read
The caps import bob-shell type-only (in .d.ts, not .js). bob-shell should move to devDependencies in the caps too — the cap package itself never executes bob-shell code. The consumer (bob CLI) already has bob-shell installed; the cap is always loaded by a bob instance that has it. Moving bob-shell to devDependencies in caps would:
- Not break bob's runtime (bob already has bob-shell)
- Not break the cap's tests (devDependencies are installed during development)
- Remove pi from the transitive tree for consumers who install a cap (though caps aren't currently published)
- Be the correct metadata (type-only import = devDependency)
The follow-up (bob#75) should move bob-shell to devDependencies in the caps. If the caps are ever published independently, a peerDependency on bob-shell would be worth adding (so a consumer installing a cap without bob gets a clear "missing peer" message rather than a silent failure). But that's a publish-scope decision, not this PR.
Lockfile surgical
14 insertions / 8 deletions, entirely inside the workspaces block. Stale workspace:* specs from #70 are cleaned up. No @biomejs platform-binary churn. --frozen-lockfile passes from a cleared node_modules. Correct.
CI
Dependency Audit red on brace-expansion alone — pre-existing, inside the 7-day minimumReleaseAge window until ~2026-07-30. Not a defect in this PR.
Ship it.
tps-kern
left a comment
There was a problem hiding this comment.
Architectural review — APPROVED.
engines: node >=22.19.0 — correct.
Derived from @earendil-works/pi-coding-agent@0.79.10's declared engines, which is the highest floor in the resolved tree. Applied to all six publishable packages (cli, shell, discord, cap-discord, cap-flair, cap-observatory). Root package is private and never publishes — correctly left alone. The cap packages get the same floor because they declare a runtime dep on bob-shell, which imports pi values. Consistent.
The honest correction about EBADENGINE being a warning (not a hard refusal) under default npm is correctly noted. The engines field still provides attribution — the warning names @tpsdev-ai/bob rather than a transitive package the user never asked for. Worth having regardless.
pi moved from dependencies to devDependencies in three caps — correct.
Verified the claim: the three caps import pi types only (dist/index.d.ts, never in .js). A type-only import should not declare a runtime dependency. bob-shell correctly keeps pi as a runtime dependency (dist/run.js and dist/persistent.js import it at runtime). The packed-tarball manifests confirm pi is gone from the three caps' published dependencies.
The honest correction about tree reduction is correctly framed.
This PR does NOT reduce the install tree — pi still arrives transitively via bob-shell. The three caps still declare bob-shell as a runtime dep while importing it type-only. This PR is a prerequisite for the reduction (correct metadata) but not the reduction itself. The builder landed it anyway because declaring a runtime dep you never execute is wrong metadata regardless. Correct call — metadata should reflect reality even when the immediate benefit is attribution, not tree size.
Read on bob#75 (the peerDependencies-vs-devDependencies decision):
The caps import bob-shell type-only in their compiled JS but declare it as a runtime dependency. The right call is peerDependencies for bob-shell in the caps. The caps are extensions that plug into bob, which provides bob-shell at runtime. The caps don't bundle their own bob-shell — they use the one the consumer has. This is the classic plugin pattern: declare the host as a peer, not a regular dep, so there's one copy and no version conflicts.
Moving bob-shell from dependencies to peerDependencies in the caps would:
- Correctly reflect the runtime relationship (consumer provides bob-shell, caps consume it)
- Reduce the install tree (no duplicate bob-shell copies under each cap)
- Prevent version conflicts (a cap can't accidentally pull in a different bob-shell version)
The caps would also need bob-shell in devDependencies (or peerDependencies + devDependencies) for local development — TypeScript compilation and tests need it resolvable. The standard pattern is: peerDependencies for the runtime contract, devDependencies for local development.
This is the same shape eslint plugins use (peer-depend on eslint), babel plugins use (peer-depend on @babel/core), and PostCSS plugins use (peer-depend on postcss). Bob's caps are the same architectural pattern.
Lockfile is surgical — 14 insertions / 8 deletions, all in the workspaces block. No @biomejs platform-binary churn (avoids the bob#67 trap). ✅
293 pass / 0 fail, identical to baseline. 7/8 CI green, Dependency Audit red on brace-expansion (expected, supply-chain window). ✅
Ship it. The bob#75 peerDependencies migration is the right next step and I've outlined the pattern above.
Closes #71. Closes #72.
Two publish-metadata defects, same class, overlapping files, so one PR.
#71 — engines
Value:
node >=22.19.0. Read off the installed@earendil-works/pi-coding-agent@0.79.10manifest rather than copied from the issue text — its ownengines.nodeis exactly>=22.19.0. It is also the ceiling across the entire resolved install tree: the four pi packages andundici@8.5.0sit at>=22.19.0and nothing in the tree requires more. Next-highest ishosted-git-infoat^20.17.0 || >=22.9.0.Scope: all six publishable packages —
bob,bob-shell,bob-discord,bob-cap-discord,bob-cap-flair,bob-cap-observatory.Deliberate call on the capability packages, since the issue asked for one: they get it too. Every publishable package either imports pi values directly (
bob-shell) or declares a runtime dependency onbob-shell, which does. The runtime floor is identical everywhere, so the declared floor should be too — and #72 does not change that, because it removes a direct pi declaration, not the transitive one throughbob-shell. The root package isprivate: trueand never publishes, so it is left alone; addingenginesthere would only affect contributor installs, which run through bun, which does not enforce it.What this actually buys, measured on a real Node 20.19.0 / npm 10.8.2 install rather than asserted:
engine-strictunset): npm warns, exit 0. The install still succeeds. What changes is who is named: before, the onlyEBADENGINEline mentioned@earendil-works/pi-coding-agent@0.79.10, a package the user never asked for; now@tpsdev-ai/bob@0.2.0and@tpsdev-ai/bob-shell@0.2.0are named alongside it withrequired: { node: '>=22.19.0' }.engine-strict=true: npm hard-fails exit 1 withNot compatible with your version of node/npm: @tpsdev-ai/bob@0.2.0/Required: {"node":">=22.19.0"}.So the accurate framing is that npm's default posture warns rather than refuses. This turns an unattributable warning into one that names the package the user typed, and it gives anyone running
engine-stricta clean install-time failure. Worth having before first publish either way.#72 — pi is type-only in three capability packages
Confirmed against the emitted output, not the source:
.js.d.tsbob-cap-discordbob-cap-flairbob-cap-observatorybob-shelldist/run.js,dist/persistent.js)bob-shellis the sole genuine runtime consumer and keeps pi as a real dependency. The three caps move it todevDependencies, which also keeps thecap-discorde2e test — the one place a cap imports pi values — working.Packed-tarball evidence (
npm pack, manifests extracted from the tarballs):Before:
After:
@tpsdev-ai/bob-shellstill lists pi underdependencies, correctly.One limit, verified rather than assumed
This does not shrink a consumer's install tree, which is the benefit the issue leads with. Installing the packed
bob-cap-flairtarball with--omit=devresolves 131 packages before and 131 after — byte-identical counts — because all three caps still declare@tpsdev-ai/bob-shellas a runtime dependency andbob-shellgenuinely runtime-depends on pi:The change is still right — declaring a runtime dependency you never execute is wrong metadata regardless — but it is a prerequisite for the tree reduction, not the tree reduction itself. Getting that requires a separate decision about the caps'
bob-shelldependency, which is the same defect class one level up:cap-flairandcap-observatoryimportbob-shelltype-only, andbob-discorddoes too. Filed separately rather than smuggled in here.Verification
Baseline measured on unmodified
origin/mainin a clean worktree, then re-measured on this branch.bun run buildbun run typecheckbun run lintnode scripts/check-workspace-deps.mjsbun install --frozen-lockfilePer-package test counts, unchanged in both runs: shell 163, cap-discord 61, cap-observatory 33, cap-flair 15, discord 12, cli 9. The
cap-discorde2e.test.tscase that builds a real piAgentSessionpasses on its own after the move (1 pass / 0 fail).Zero regression.
Lockfile
Surgical.
bun.lockpicked up staleworkspace:specs for the three caps andbob-discordthat #70 had already corrected in thepackage.jsonfiles but never refreshed in the lock, plus the three newdevDependenciesblocks. No resolved-package entries changed and no@biomejsplatform-binary churn — the diff is 14 insertions / 8 deletions, entirely inside the workspaces block.bun install --frozen-lockfilepasses against it from a clearednode_modules.Not touched
packages/shell/src/capability-catalog.ts— left alone for the concurrent capability-resolution work.CHANGELOG.md— this repo has no[Unreleased]section and the release job extracts a specific version heading, so there is nothing to append to yet.Dependency Auditjob'sbrace-expansionfinding, which is pre-existing onmainand inside the repo's own 7-day release-age window. No excludes entry added.