Skip to main content

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:

  1. Broken app discovery. appNavs defined in definePackage() are never propagated to the IPackage database record the UI reads, so apps like Tasks don't appear in the launcher.
  2. Competing tag conventions. versionTag in definePackage(), devTag on contracts, followVersionTags on packages, and a hardcoded "beta" in the installer all disagree about what tag a version should have.
  3. No isolation for dev versions. Local disk changes immediately propagate to other devices in the group.
  4. Admin-only package creation. Non-admins cannot create even local, device-only dev versions.

Design

Three phases

dev ──▶ beta ──▶ stable
PhaseCreated bySyncs to group?Auto-activates on other devices?
devDisk update (automatic)Yes (records sync)Never — excluded from all follow policies unless a device explicitly opts in
betaPromote via UI or toolYesOnly on devices following stable+beta or *
stablePromote via UI or toolYesOn all devices (default follow policy)

Rules

  1. Disk updates always create dev versions. updatePackageBundle() assigns versionTag: "dev". The tag is never set in code.
  2. Dev versions never auto-activate on other devices. doesTagMatch() excludes "dev" from every follow policy except an explicit deviceVersionTag: "dev" override.
  3. Promotion is a platform operation, not a code operation. versionTag and contract devTag are removed from the definePackage() API. The platform determines the tag based on the promotion state.
  4. 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.
  5. 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:

  1. Increment the contract version (e.g., v1 → v2).
  2. Add the new fields/tools/observables (new fields must be optional for backward compat).
  3. Declare alsoImplements(contractId, previousVersion).
  4. The system validates that v2 is a structural superset of v1 (validateProviderSatisfiesContract).
  5. 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:

ToolPurposeRequired role
promote-package-versionPromote dev→beta or beta→stableAdmin
set-active-package-versionSet the active version for a packageAdmin

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

ActionRequired role
Create a dev version (disk update)Writer
Create/update dev package version recordsWriter (signed)
Promote to betaAdmin (signed)
Promote to stableAdmin (signed)
Activate a versionAdmin (signed)
Pin to a versionAdmin (signed)

Personal space (no group context) bypasses role checks entirely — users can do whatever they want with their own packages.

doesTagMatch behavior

Follow policyMatches dev?Matches beta?Matches stable?
Default (no followVersionTags)NoOnly if active is betaOnly if active is stable
"stable"NoNoYes
"stable,beta"NoYesYes
"*"YesYesYes
deviceVersionTag: "dev" overrideYes (only dev)NoNo

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 proper IPeersPackage return value that includes appNavs from the packageDefinition.
  • await the installContractPackage() call (currently missing await on an async function).
  • Fix syncPeersCoreBundle appNavs refresh to read from packageDefinition when present.
  • Investigate the broken peers-pwa/src/peers-init.ts import of installPeersCoreFromBundles.

Phase 2: Enforce dev tag on disk updates

  • Change updatePackageBundle() to use versionTag: "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 versionTag setter from PackageBuilder.
  • Remove versionTag from IPackageDefinitionResult.
  • Remove devTag parameter from PackageBuilder.contract().
  • Remove correctPackageVersion from PackageLoader.
  • Update all packages (peers-core, groceries, timers, frames, voice-hub) to remove pkg.versionTag and contract devTag arguments.

Phase 4: Version hash and signature changes

  • Remove versionTag from computePackageVersionHash().
  • Update signature computation to cover contentHash + versionTag + identity fields.
  • Add history array field to PackageVersions schema.
  • 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 Contracts table.

Phase 6: Promotion and activation tools

  • Create promote-package-version tool.
  • Create set-active-package-version tool.
  • Wire UI promote/activate buttons to call these tools.

Phase 7: Permission model for dev versions

  • Allow GroupMemberRole.Writer for 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

FileChange
peers-sdk/src/contracts/builder.tsRemove versionTag setter, remove devTag param from contract()
peers-sdk/src/contracts/types.tsRemove versionTag from IPackageDefinitionResult
peers-sdk/src/package-loader/contract-package-loader.tsHandle appNavs propagation
peers-sdk/src/package-loader/package-loader.tsFix IPeersPackage construction, await contract install
peers-sdk/src/data/package-permissions.tsAllow Writer role for dev versions
peers-sdk/src/data/package-version-permissions.tsAllow Writer role for dev versions
peers-sdk/src/data/package-versions.tsRemove versionTag from hash, update doesTagMatch, add history field
peers-electron/src/server/package-installer.tsDev tag for disk updates, contract finalization on promote
peers-core/src/package.tsRemove versionTag and devTag
official-packages/*/src/package.tsRemove versionTag and devTag
peers-core/src/tools/New promote-package-version and set-active-package-version tools
peers-ui/src/screens/packages/package-versions.tsxPromotion UI, dev badge, contract finalization
  • Package contracts — versioned interfaces, definePackage, validation, registry
  • Getting started — package system overview
  • Tools — tool authoring, schemaToFields, tool schemas in contracts