Package lifecycle: unified three-phase development process
Status: Design — not yet implemented
Problem
The package system has been migrating from a legacy IPeersPackage export model to a definePackage() + contracts model. The migration is incomplete, creating several problems:
- Broken app discovery.
appNavsdefined indefinePackage()are never propagated to theIPackagedatabase record the UI reads, so apps like Tasks don't appear in the launcher. - Competing tag conventions.
versionTagindefinePackage(),devTagon contracts,followVersionTagson packages, and a hardcoded"beta"in the installer all disagree about what tag a version should have. - No isolation for dev versions. Local disk changes immediately propagate to other devices in the group.
- Admin-only package creation. Non-admins cannot create even local, device-only dev versions.
Design
Three phases
dev ──▶ beta ──▶ stable
| Phase | Created by | Syncs to group? | Auto-activates on other devices? |
|---|---|---|---|
| dev | Disk update (automatic) | Yes (records sync) | Never — excluded from all follow policies unless a device explicitly opts in |
| beta | Promote via UI or tool | Yes | Only on devices following stable+beta or * |
| stable | Promote via UI or tool | Yes | On all devices (default follow policy) |
Rules
- Disk updates always create dev versions.
updatePackageBundle()assignsversionTag: "dev". The tag is never set in code. - Dev versions never auto-activate on other devices.
doesTagMatch()excludes"dev"from every follow policy except an explicitdeviceVersionTag: "dev"override. - Promotion is a platform operation, not a code operation.
versionTagand contractdevTagare removed from thedefinePackage()API. The platform determines the tag based on the promotion state. - Contract devTag is coupled to package promotion. When a package version is promoted to stable, all its contracts are finalized (devTag removed, shape frozen). Previously-frozen contracts remain frozen in dev builds.
- Non-admins (Writers) can create dev versions. Only promoting to beta or stable requires Admin role.
Contract evolution via alsoImplements
Strict immutability is maintained on frozen contracts. When a developer needs to extend a stable contract shape, they:
- Increment the contract version (e.g., v1 → v2).
- Add the new fields/tools/observables (new fields must be optional for backward compat).
- Declare
alsoImplements(contractId, previousVersion). - The system validates that v2 is a structural superset of v1 (
validateProviderSatisfiesContract). - Consumers of v1 continue to work with any provider of v2, because v2 satisfies v1's shape.
This keeps contracts frozen once released while making backward-compatible evolution trivial. The alsoImplements declaration can target a single version or an inclusive range ({ from: 1, to: 3 }).
Re-registration of frozen contracts: When a dev build includes a contract version that was previously frozen (promoted to stable in an earlier release), the installer re-registers it as stable (no devTag). The validateImmutability check correctly rejects attempts to register a dev version of a stable contract, so the installer must preserve the frozen state rather than trying to mark it dev.
Content hash vs promotion signature
computePackageVersionHash is computed from code content only (bundle file hashes). The same code promoted from dev to beta to stable has the same content hash throughout. Promoting a version is a metadata change, not a code change.
The signature field on the PackageVersions record signs contentHash + versionTag + identity fields, so admins attest to "this exact code at this exact promotion level." Anyone can verify the signature to confirm an admin authorized the promotion.
Audit trail
PackageVersions records carry a history array. Each significant action appends an entry:
{
"action": "created | promoted:beta | promoted:stable | activated",
"by": "<peerId>",
"at": "<ISO 8601>",
"signature": "<actor's signature>"
}
The signature in each entry is signed by the actor (Writer for dev creation, Admin for promotions). This provides a verifiable audit trail without creating a new table.
Promotion and activation tools
Promotion and activation are first-class tools, not just UI buttons:
| Tool | Purpose | Required role |
|---|---|---|
promote-package-version | Promote dev→beta or beta→stable | Admin |
set-active-package-version | Set the active version for a package | Admin |
Both tools are callable by AI assistants and CI pipelines. The UI promote/activate buttons call these same tools, ensuring a single code path.
Permission model
| Action | Required role |
|---|---|
| Create a dev version (disk update) | Writer |
| Create/update dev package version records | Writer (signed) |
| Promote to beta | Admin (signed) |
| Promote to stable | Admin (signed) |
| Activate a version | Admin (signed) |
| Pin to a version | Admin (signed) |
Personal space (no group context) bypasses role checks entirely — users can do whatever they want with their own packages.
doesTagMatch behavior
| Follow policy | Matches dev? | Matches beta? | Matches stable? |
|---|---|---|---|
Default (no followVersionTags) | No | Only if active is beta | Only if active is stable |
"stable" | No | No | Yes |
"stable,beta" | No | Yes | Yes |
"*" | Yes | Yes | Yes |
deviceVersionTag: "dev" override | Yes (only dev) | No | No |
Special case: peers-core
peers-core ships bundled with the Electron app and PWA. The syncPeersCoreBundle() startup routine creates versions from the bundled files:
- Development (unpackaged): Creates
"dev"versions, matching the general rule. - Production (packaged app): The bundled peers-core is tagged at build/release time —
"stable"for GA releases,"beta"for beta releases. This is the one case where the tag is determined at build time rather than by developer action or UI promotion.
Implementation phases
Phase 1: Fix the immediate bug (Tasks not showing)
- In
PackageLoader._evaluateBundle(), when the contract path runs, construct a properIPeersPackagereturn value that includesappNavsfrom thepackageDefinition. awaittheinstallContractPackage()call (currently missingawaiton an async function).- Fix
syncPeersCoreBundleappNavs refresh to read frompackageDefinitionwhen present. - Investigate the broken
peers-pwa/src/peers-init.tsimport ofinstallPeersCoreFromBundles.
Phase 2: Enforce dev tag on disk updates
- Change
updatePackageBundle()to useversionTag: "dev". - Update
doesTagMatch()to exclude"dev"unless explicitly opted in. - Handle the peers-core special case in
syncPeersCoreBundle().
Phase 3: Remove versionTag and devTag from definePackage API
- Remove
versionTagsetter fromPackageBuilder. - Remove
versionTagfromIPackageDefinitionResult. - Remove
devTagparameter fromPackageBuilder.contract(). - Remove
correctPackageVersionfromPackageLoader. - Update all packages (peers-core, groceries, timers, frames, voice-hub) to remove
pkg.versionTagand contractdevTagarguments.
Phase 4: Version hash and signature changes
- Remove
versionTagfromcomputePackageVersionHash(). - Update signature computation to cover contentHash + versionTag + identity fields.
- Add
historyarray field toPackageVersionsschema. - Each creation/promotion appends a signed history entry.
Phase 5: Contract devTag tied to promotion
- On promotion to stable, iterate contracts and re-register without
devTag. - On re-registration of an already-frozen contract, preserve stable status.
- Persist finalized contracts in the
Contractstable.
Phase 6: Promotion and activation tools
- Create
promote-package-versiontool. - Create
set-active-package-versiontool. - Wire UI promote/activate buttons to call these tools.
Phase 7: Permission model for dev versions
- Allow
GroupMemberRole.Writerfor dev version creation/updates (signatures required, lower role threshold). - Keep Admin requirement for beta/stable promotion.
Phase 8: UI improvements
- Update promote dropdown for dev → beta → stable path.
- Add "dev" badge style.
- Add "Follow Channel" UI to package settings.
Files impacted
| File | Change |
|---|---|
peers-sdk/src/contracts/builder.ts | Remove versionTag setter, remove devTag param from contract() |
peers-sdk/src/contracts/types.ts | Remove versionTag from IPackageDefinitionResult |
peers-sdk/src/package-loader/contract-package-loader.ts | Handle appNavs propagation |
peers-sdk/src/package-loader/package-loader.ts | Fix IPeersPackage construction, await contract install |
peers-sdk/src/data/package-permissions.ts | Allow Writer role for dev versions |
peers-sdk/src/data/package-version-permissions.ts | Allow Writer role for dev versions |
peers-sdk/src/data/package-versions.ts | Remove versionTag from hash, update doesTagMatch, add history field |
peers-electron/src/server/package-installer.ts | Dev tag for disk updates, contract finalization on promote |
peers-core/src/package.ts | Remove versionTag and devTag |
official-packages/*/src/package.ts | Remove versionTag and devTag |
peers-core/src/tools/ | New promote-package-version and set-active-package-version tools |
peers-ui/src/screens/packages/package-versions.tsx | Promotion UI, dev badge, contract finalization |
Related
- Package contracts — versioned interfaces,
definePackage, validation, registry - Getting started — package system overview
- Tools — tool authoring,
schemaToFields, tool schemas in contracts