Skip to content

Add facet update with interactive picker and tagged InstallOperation - #557

Merged
eXamadeus merged 10 commits into
mainfrom
julian/08-31-implement_facet_update_with_interactive_picker_and_tagged_install_operation
Sep 1, 2026
Merged

Add facet update with interactive picker and tagged InstallOperation#557
eXamadeus merged 10 commits into
mainfrom
julian/08-31-implement_facet_update_with_interactive_picker_and_tagged_install_operation

Conversation

@eXamadeus

@eXamadeus eXamadeus commented Sep 1, 2026

Copy link
Copy Markdown
Member

Why

A project could declare facets and reproduce them, but had no way to move them to newer releases. facet install reproduces what the lockfile already records — the opposite operation.

#542 landed the read-only half of this: prepareFacetUpdate, batched registry metadata with the TOO_MANY_SPECIFIERS guard, and response identity checks. This PR adds everything that turns that plan into a shipped command — the application half and the facet update CLI surface, aliased facet upgrade. It supersedes the stack that spanned #543 through #552, which is closed in favor of reviewing this as one unit.

Details

Applying a reviewed plan

Planning runs without the install lock so a user can read an interactive picker or a dry-run preview without blocking the machine. Application then runs under the lock as an ordinary install transaction.

The plan carries the exact FileState of both facets.json and facets.lock from the moment it was built. Before touching anything, application compares those bytes against disk; if the project moved while the plan was being reviewed, the plan is withdrawn (UPDATE_PLAN_STALE) rather than merged into state the user never saw. The comparison is byte-level rather than semantic, matching the write preconditions used later in the same run.

No registry question is asked twice. Discovery resolved the metadata, the user approved that answer, and application installs precisely that release — re-resolving a range at apply time could pick up something published after the user looked.

InstallOperation

The install entry point previously took an optional additions/removals delta plus a separate frozenLockfile boolean. That pair could express states no caller should be able to say: the same facet added and removed, an add and a remove in one run, a frozen run that mutates the locked set, and two spellings of "just reproduce the lockfile". It is now one tagged union — reproduce / add / remove / update — where those combinations are unconstructible, so the FROZEN_WITH_DELTA and DELTA_CONFLICT runtime guards are deleted rather than reimplemented. Interaction capabilities (collision resolution, MCP consent, asset takeover) hang off the arms that can legitimately use them, so frozen reproduction can no longer be handed a resolver it would silently ignore.

Current, Target, Latest

Every checkable facet has three versions, and the plan shows all three — including for facets that are not moving, since that is what answers "why is this one staying put?". Plain facet update takes every Target that advances, staying inside the declared range and never rewriting facets.json. --latest takes every Latest that advances, and because that may cross the declared range, it rewrites the specifier by the smallest edit that admits the new version while preserving how the intent was expressed: a pin stays a pin, a major wildcard stays a major wildcard, an already-floating specifier is left verbatim.

Interactive selection

--interactive opens a three-column picker with arrow-key column navigation and a discovery progress indicator. Every row starts on its latest release with nothing selected, so a single keypress takes the newest version. Selection logic lives in the engine rather than the view, so a dry run and a real application cannot disagree about what a choice commits. Without a usable terminal the command fails before contacting the registry rather than after.

Version components are bounded at 2^53−1 so ordering comparisons stay exact.

Verification

bun check is green: lint, type checks, unit tests, e2e, docs validation, and OpenSpec validation. Coverage includes version ordering, specifier rewriting, discovery classification, picker interaction and column layout, the stale-plan withdrawal path, and the apply lifecycle against a real temporary project tree.

The OpenSpec change add-facet-update-command is archived into the canonical specs here.

@mintlify

mintlify Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
AgentFacets 🟢 Ready View Preview Sep 1, 2026, 12:43 AM

@changeset-bot

changeset-bot Bot commented Sep 1, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 1c0c5e7

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 2 packages
Name Type
agent-facets Minor
@agent-facets/protocol Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@greptile-apps

greptile-apps Bot commented Sep 1, 2026

Copy link
Copy Markdown

Too many files changed for review (117 files, 100 file limit).

Bypass the limit by tagging @greptile-apps to review.

@eXamadeus eXamadeus changed the title Add prepareFacetUpdate, batch metadata limits, and response identity checks Add facet update with interactive picker and tagged InstallOperation Sep 1, 2026
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 1, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-01T00:47:44.591540Z 1c0c5e7 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

Copy link
Copy Markdown
Member Author

This stack of pull requests is managed by Graphite. Learn more about stacking.

@coderabbitai

coderabbitai Bot commented Sep 1, 2026

Copy link
Copy Markdown

Important

Review skipped

Too many files!

This PR contains 117 files, which is 17 over the limit of 100.

To get a review, reduce the PR to 100 files or fewer by splitting it into smaller PRs or changing its base branch.

Upgrade to a paid plan to raise the limit.

This review couldn't start because sufficient usage credits or metered capacity aren't available. Add credits or update usage-based reviews in the billing tab, then retry.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Team

Run ID: 516a0e62-9e3b-42a5-b573-e4d79fa10086

📥 Commits

Reviewing files that changed from the base of the PR and between cf16247 and 1c0c5e7.

📒 Files selected for processing (117)
  • .changeset/small-icons-write.md
  • README.md
  • docs/cli/add.mdx
  • docs/cli/index.mdx
  • docs/cli/list.mdx
  • docs/cli/self-update.mdx
  • docs/cli/update.mdx
  • docs/cli/upgrade.mdx
  • docs/docs.json
  • docs/guides/install-facets.mdx
  • docs/guides/troubleshooting.mdx
  • docs/roadmap/alpha.mdx
  • docs/roadmap/beta.mdx
  • docs/roadmap/stable.mdx
  • docs/specification/commit.mdx
  • docs/specification/materialization.mdx
  • openspec/changes/add-facet-update-command/.openspec.yaml
  • openspec/changes/add-facet-update-command/design.md
  • openspec/changes/add-facet-update-command/proposal.md
  • openspec/changes/add-facet-update-command/specs/cli/spec.md
  • openspec/changes/add-facet-update-command/specs/installation/spec.md
  • openspec/changes/add-facet-update-command/tasks.md
  • openspec/specs/cli/spec.md
  • openspec/specs/installation/spec.md
  • openspec/specs/protocol__version-spec/spec.md
  • packages/cli/src/__tests__/cli.e2e.test.ts
  • packages/cli/src/__tests__/help.test.ts
  • packages/cli/src/__tests__/helpers/capture-log.ts
  • packages/cli/src/__tests__/helpers/capture-std.ts
  • packages/cli/src/__tests__/install-view.test.tsx
  • packages/cli/src/__tests__/instructions.e2e.test.ts
  • packages/cli/src/__tests__/mcp-consent.e2e.test.ts
  • packages/cli/src/__tests__/run-flags.test.ts
  • packages/cli/src/__tests__/update.e2e.test.ts
  • packages/cli/src/commands.ts
  • packages/cli/src/commands/__tests__/self-update.test.ts
  • packages/cli/src/commands/add/index.ts
  • packages/cli/src/commands/install/index.ts
  • packages/cli/src/commands/instructions/__tests__/instructions.test.ts
  • packages/cli/src/commands/remove/index.ts
  • packages/cli/src/commands/shared/__tests__/install-failure.test.ts
  • packages/cli/src/commands/shared/install-failure.ts
  • packages/cli/src/commands/update/__tests__/command.test.ts
  • packages/cli/src/commands/update/__tests__/fixtures.ts
  • packages/cli/src/commands/update/__tests__/picker.test.tsx
  • packages/cli/src/commands/update/__tests__/plan-view.test.tsx
  • packages/cli/src/commands/update/__tests__/registration.test.ts
  • packages/cli/src/commands/update/__tests__/selection.test.ts
  • packages/cli/src/commands/update/errors.ts
  • packages/cli/src/commands/update/index.ts
  • packages/cli/src/commands/update/picker.tsx
  • packages/cli/src/commands/update/preview.ts
  • packages/cli/src/commands/update/run-discovery.ts
  • packages/cli/src/commands/update/run-picker.ts
  • packages/cli/src/commands/update/selection.ts
  • packages/cli/src/help.ts
  • packages/cli/src/prompts/overview.txt
  • packages/cli/src/prompts/usage.txt
  • packages/cli/src/run.ts
  • packages/cli/src/tui/views/install/failure-block.tsx
  • packages/cli/src/tui/views/install/install-view.tsx
  • packages/cli/src/tui/views/update/__tests__/discovery-view.test.tsx
  • packages/cli/src/tui/views/update/__tests__/version-change.test.ts
  • packages/cli/src/tui/views/update/columns.ts
  • packages/cli/src/tui/views/update/discovery-view.tsx
  • packages/cli/src/tui/views/update/plan-view.tsx
  • packages/cli/src/tui/views/update/version-change.ts
  • packages/cli/src/util/__tests__/interactive.test.ts
  • packages/cli/src/util/interactive.ts
  • packages/engine/src/__tests__/manifest-mutations.test.ts
  • packages/engine/src/__tests__/run-install.test.ts
  • packages/engine/src/index.ts
  • packages/engine/src/install/__tests__/apply-ownership.test.ts
  • packages/engine/src/install/__tests__/compose.test.ts
  • packages/engine/src/install/__tests__/install-operation.test.ts
  • packages/engine/src/install/__tests__/manifest-transaction.test.ts
  • packages/engine/src/install/__tests__/mcp-install.test.ts
  • packages/engine/src/install/__tests__/mcp-outcomes.test.ts
  • packages/engine/src/install/__tests__/parse-locked-version.test.ts
  • packages/engine/src/install/__tests__/run-install.chain.test.ts
  • packages/engine/src/install/__tests__/run-install.concurrency.test.ts
  • packages/engine/src/install/__tests__/run-install.receipt.test.ts
  • packages/engine/src/install/__tests__/run-install.test.ts
  • packages/engine/src/install/__tests__/run-update.test.ts
  • packages/engine/src/install/add/index.ts
  • packages/engine/src/install/commit/__tests__/effective-locked.test.ts
  • packages/engine/src/install/commit/delta.ts
  • packages/engine/src/install/commit/effective-locked.ts
  • packages/engine/src/install/commit/resolve-all.ts
  • packages/engine/src/install/commit/resolve-facet.ts
  • packages/engine/src/install/commit/resolve-registry.ts
  • packages/engine/src/install/parse-locked-version.ts
  • packages/engine/src/install/remove/index.ts
  • packages/engine/src/install/run-install.ts
  • packages/engine/src/install/types.ts
  • packages/engine/src/install/update/__tests__/discover.test.ts
  • packages/engine/src/install/update/__tests__/plan-fixtures.ts
  • packages/engine/src/install/update/__tests__/prepare.test.ts
  • packages/engine/src/install/update/__tests__/selection.test.ts
  • packages/engine/src/install/update/__tests__/version-order.test.ts
  • packages/engine/src/install/update/advancing.ts
  • packages/engine/src/install/update/apply.ts
  • packages/engine/src/install/update/discover.ts
  • packages/engine/src/install/update/index.ts
  • packages/engine/src/install/update/selection.ts
  • packages/engine/src/install/update/types.ts
  • packages/engine/src/registry/resolve-metadata.ts
  • packages/engine/src/sources/adapter/__tests__/specifier.test.ts
  • packages/engine/src/sources/facet/__tests__/parse-source.test.ts
  • packages/engine/src/sources/facet/__tests__/parse-version.test.ts
  • packages/engine/src/sources/facet/parse-version.ts
  • packages/engine/src/sources/facet/types.ts
  • packages/protocol/src/__tests__/lockfile.test.ts
  • packages/protocol/src/__tests__/version-spec.test.ts
  • packages/protocol/src/index.ts
  • packages/protocol/src/schemas/lockfile.ts
  • packages/protocol/src/sources/version-spec.ts

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@eXamadeus
eXamadeus merged commit 9a061c4 into main Sep 1, 2026
7 checks passed

Copy link
Copy Markdown
Member Author

Merge activity

@eXamadeus
eXamadeus deleted the julian/08-31-implement_facet_update_with_interactive_picker_and_tagged_install_operation branch September 1, 2026 00:46

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1c0c5e71dd

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

// network error produces.
let settled: ReadonlyArray<RegistryResult<ReadonlyArray<RegistryMetadata>>>
try {
settled = await Promise.all(groups.map((group) => resolve(group)))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Bound discovery concurrency across batches

When a project produces more than 100 metadata specifiers, this Promise.all starts every group concurrently, while resolveRegistryMetadataBatch itself starts up to 100 concurrent fetchOne calls (packages/engine/src/registry/resolve-metadata.ts:89). Consequently a 10,000-facet manifest can still open roughly 20,000 requests at once, defeating the new cap's stated purpose and making large updates likely to hit registry rate limits or local socket limits; process groups sequentially or through a bounded worker pool.

Useful? React with 👍 / 👎.

Comment on lines +195 to +196
const targetAt = specs.push({ name: facet.name, version: facet.authored.spec }) - 1
const latestAt = specs.push({ name: facet.name, version: { kind: 'latest' } }) - 1

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reuse the Latest lookup for floating specifiers

For facets authored as * or latest, these two entries ask the registry the same semantic question in separate requests. If a release is published or different registry replicas answer between those requests, Target and Latest can disagree even though both specifiers mean the newest release, so plain update and --latest can select different versions for an already-floating facet; use the single Latest response for both columns when the authored specifier resolves to latest.

Useful? React with 👍 / 👎.

eXamadeus pushed a commit that referenced this pull request Sep 1, 2026
This PR was auto-generated by the release workflow. When you're ready to do a release, you can merge this and the packages will be published to npm automatically. If you're not ready to do a release yet, that's fine, whenever you add more changesets to main, this PR will be updated.

# Releases
## agent-facets@0.33.0

### Minor Changes

- [#557](#557) [`9a061c4`](9a061c4) Thanks [@eXamadeus](https://github.com/eXamadeus)! - **New command: `facet update` (aliased `facet upgrade`)** — moves the registry-backed facets a project declares to newer releases. It reads `facets.json` and `facets.lock`, asks the registry for each facet's range-respecting target and its latest release, and shows both alongside what is installed, so "why is this one not moving?" is answerable from the plan itself. Plain `facet update` takes every target the declared specifier already permits. `--latest` (`-L`) crosses those specifiers and rewrites them by the smallest edit that admits the new version, preserving how the intent was written: a pin stays a pin, `1.*` becomes `2.*`, `1.2.*` becomes `2.4.*`, and `*` and `latest` are left exactly as authored.
    **Previews and per-facet selection.** `--dry-run` prints the plan and writes nothing — no manifest, no lockfile, no receipt, no assets, no cache, and no adapter installation, which makes it safe on a machine with no adapter connected. `--interactive` (`-i`) opens a picker for choosing which facets move and which version each takes. Every row starts on its **latest** release with nothing selected, so walking the list and pressing `Space` takes the newest version of each facet you pick — `Space` always means "yes, this one", and a facet you never touch is left alone. `◀ ▶` (or `l`) moves a row to its range target first when that is what you want instead. `--latest` is accepted alongside `--interactive` but changes nothing there; it is how the non-interactive run asks for what this screen already offers. The picker requires a real terminal and fails immediately without one, before any registry lookup. Git and local facets are named as unsupported rather than counted as current — reporting them as up to date would claim something nothing verified.
    **Applying an update is an install.** Discovery runs read-only and takes no project lock, so reading a plan never blocks another facet operation. Application re-checks under the lock that the project has not moved since the plan was reviewed, then runs the ordinary install pipeline: the same verification, collision handling, MCP approval, rollback, and atomic manifest/lockfile/receipt write. The version you reviewed is the version installed — a release published in between does not silently change it. Recorded materialization choices survive a version change. There is no `--frozen-lockfile`: reproducing what the lockfile already records is the opposite of what this command does.
    **Breaking: `facet upgrade` is no longer a placeholder.** It previously printed a not-yet-implemented notice and exited `0` without touching a single file. It is now an alias of `facet update` — one command, one help page, one behavior — so the same invocation contacts the registry, may install adapters, takes the project lock, and rewrites `facets.json`, `facets.lock`, the install receipt, and your materialized assets. `update` is the canonical spelling, and `facet upgrade --help` prints `Usage: facet update`. If something in your automation called `facet upgrade` expecting a no-op, drop the call or make it explicit with `facet update --dry-run`.
    Neither name touches the CLI binary. That remains `facet self-update`.
    **Protocol: version components are bounded by exact integer representation.** The published version grammar now rejects a specifier or locked version whose numeric component exceeds `2^53 - 1`, with an error that names the magnitude rather than the form. Above that bound two distinct releases are the same double — `9007199254740992` and `9007199254740993` compare equal — so a comparison that decides which release is newer, or whether a locked version still satisfies a manifest range, could answer for a version that was never published. `facet update` is the first command whose whole job is that comparison, which is why the bound lands now. No real version is anywhere near it.

### Patch Changes

- [#554](#554) [`d703ff1`](d703ff1) Thanks [@eXamadeus](https://github.com/eXamadeus)! - Remove empty directories left behind when an install rolls back.
    Cleanup previously only reclaimed directories the run could prove it had
    created, identified by inode. That test was both too strict and unsound: a
    directory that existed before the run was never a candidate no matter how
    empty the rollback left it, and inode identity proves nothing on Linux, which
    recycles an inode the moment it is freed.
    Rollback now asks the only question that matters — is anything left inside? —
    and hands the answer to `rmdir`, which is non-recursive and so refuses to
    remove a directory holding anything at all, yours or ours, in a single step
    with no check-then-delete window. The walk climbs from each restored path and
    stops at the tool's configuration directory (`.claude`, `.opencode`), which is
    never removed and never climbed past, so cleanup can only reclaim the tree the
    install materialized.

## @agent-facets/protocol@0.31.1

### Patch Changes

- [#557](#557) [`9a061c4`](9a061c4) Thanks [@eXamadeus](https://github.com/eXamadeus)! - **New command: `facet update` (aliased `facet upgrade`)** — moves the registry-backed facets a project declares to newer releases. It reads `facets.json` and `facets.lock`, asks the registry for each facet's range-respecting target and its latest release, and shows both alongside what is installed, so "why is this one not moving?" is answerable from the plan itself. Plain `facet update` takes every target the declared specifier already permits. `--latest` (`-L`) crosses those specifiers and rewrites them by the smallest edit that admits the new version, preserving how the intent was written: a pin stays a pin, `1.*` becomes `2.*`, `1.2.*` becomes `2.4.*`, and `*` and `latest` are left exactly as authored.
    **Previews and per-facet selection.** `--dry-run` prints the plan and writes nothing — no manifest, no lockfile, no receipt, no assets, no cache, and no adapter installation, which makes it safe on a machine with no adapter connected. `--interactive` (`-i`) opens a picker for choosing which facets move and which version each takes. Every row starts on its **latest** release with nothing selected, so walking the list and pressing `Space` takes the newest version of each facet you pick — `Space` always means "yes, this one", and a facet you never touch is left alone. `◀ ▶` (or `l`) moves a row to its range target first when that is what you want instead. `--latest` is accepted alongside `--interactive` but changes nothing there; it is how the non-interactive run asks for what this screen already offers. The picker requires a real terminal and fails immediately without one, before any registry lookup. Git and local facets are named as unsupported rather than counted as current — reporting them as up to date would claim something nothing verified.
    **Applying an update is an install.** Discovery runs read-only and takes no project lock, so reading a plan never blocks another facet operation. Application re-checks under the lock that the project has not moved since the plan was reviewed, then runs the ordinary install pipeline: the same verification, collision handling, MCP approval, rollback, and atomic manifest/lockfile/receipt write. The version you reviewed is the version installed — a release published in between does not silently change it. Recorded materialization choices survive a version change. There is no `--frozen-lockfile`: reproducing what the lockfile already records is the opposite of what this command does.
    **Breaking: `facet upgrade` is no longer a placeholder.** It previously printed a not-yet-implemented notice and exited `0` without touching a single file. It is now an alias of `facet update` — one command, one help page, one behavior — so the same invocation contacts the registry, may install adapters, takes the project lock, and rewrites `facets.json`, `facets.lock`, the install receipt, and your materialized assets. `update` is the canonical spelling, and `facet upgrade --help` prints `Usage: facet update`. If something in your automation called `facet upgrade` expecting a no-op, drop the call or make it explicit with `facet update --dry-run`.
    Neither name touches the CLI binary. That remains `facet self-update`.
    **Protocol: version components are bounded by exact integer representation.** The published version grammar now rejects a specifier or locked version whose numeric component exceeds `2^53 - 1`, with an error that names the magnitude rather than the form. Above that bound two distinct releases are the same double — `9007199254740992` and `9007199254740993` compare equal — so a comparison that decides which release is newer, or whether a locked version still satisfies a manifest range, could answer for a version that was never published. `facet update` is the first command whose whole job is that comparison, which is why the bound lands now. No real version is anywhere near it.
eXamadeus pushed a commit that referenced this pull request Sep 1, 2026
This PR was auto-generated by the release workflow. When you're ready to
do a release, you can merge this and the packages will be published to
npm automatically. If you're not ready to do a release yet, that's fine,
whenever you add more changesets to main, this PR will be updated.

# Releases
## agent-facets@0.33.0

### Minor Changes

- [#557](#557)
[`9a061c4`](9a061c4)
Thanks [@eXamadeus](https://github.com/eXamadeus)! - **New command:
`facet update` (aliased `facet upgrade`)** — moves the registry-backed
facets a project declares to newer releases. It reads `facets.json` and
`facets.lock`, asks the registry for each facet's range-respecting
target and its latest release, and shows both alongside what is
installed, so "why is this one not moving?" is answerable from the plan
itself. Plain `facet update` takes every target the declared specifier
already permits. `--latest` (`-L`) crosses those specifiers and rewrites
them by the smallest edit that admits the new version, preserving how
the intent was written: a pin stays a pin, `1.*` becomes `2.*`, `1.2.*`
becomes `2.4.*`, and `*` and `latest` are left exactly as authored.
**Previews and per-facet selection.** `--dry-run` prints the plan and
writes nothing — no manifest, no lockfile, no receipt, no assets, no
cache, and no adapter installation, which makes it safe on a machine
with no adapter connected. `--interactive` (`-i`) opens a picker for
choosing which facets move and which version each takes. Every row
starts on its **latest** release with nothing selected, so walking the
list and pressing `Space` takes the newest version of each facet you
pick — `Space` always means "yes, this one", and a facet you never touch
is left alone. `◀ ▶` (or `l`) moves a row to its range target first when
that is what you want instead. `--latest` is accepted alongside
`--interactive` but changes nothing there; it is how the non-interactive
run asks for what this screen already offers. The picker requires a real
terminal and fails immediately without one, before any registry lookup.
Git and local facets are named as unsupported rather than counted as
current — reporting them as up to date would claim something nothing
verified.
**Applying an update is an install.** Discovery runs read-only and takes
no project lock, so reading a plan never blocks another facet operation.
Application re-checks under the lock that the project has not moved
since the plan was reviewed, then runs the ordinary install pipeline:
the same verification, collision handling, MCP approval, rollback, and
atomic manifest/lockfile/receipt write. The version you reviewed is the
version installed — a release published in between does not silently
change it. Recorded materialization choices survive a version change.
There is no `--frozen-lockfile`: reproducing what the lockfile already
records is the opposite of what this command does.
**Breaking: `facet upgrade` is no longer a placeholder.** It previously
printed a not-yet-implemented notice and exited `0` without touching a
single file. It is now an alias of `facet update` — one command, one
help page, one behavior — so the same invocation contacts the registry,
may install adapters, takes the project lock, and rewrites
`facets.json`, `facets.lock`, the install receipt, and your materialized
assets. `update` is the canonical spelling, and `facet upgrade --help`
prints `Usage: facet update`. If something in your automation called
`facet upgrade` expecting a no-op, drop the call or make it explicit
with `facet update --dry-run`.
Neither name touches the CLI binary. That remains `facet self-update`.
**Protocol: version components are bounded by exact integer
representation.** The published version grammar now rejects a specifier
or locked version whose numeric component exceeds `2^53 - 1`, with an
error that names the magnitude rather than the form. Above that bound
two distinct releases are the same double — `9007199254740992` and
`9007199254740993` compare equal — so a comparison that decides which
release is newer, or whether a locked version still satisfies a manifest
range, could answer for a version that was never published. `facet
update` is the first command whose whole job is that comparison, which
is why the bound lands now. No real version is anywhere near it.

### Patch Changes

- [#554](#554)
[`d703ff1`](d703ff1)
Thanks [@eXamadeus](https://github.com/eXamadeus)! - Remove empty
directories left behind when an install rolls back.
Cleanup previously only reclaimed directories the run could prove it had
created, identified by inode. That test was both too strict and unsound:
a
directory that existed before the run was never a candidate no matter
how
empty the rollback left it, and inode identity proves nothing on Linux,
which
    recycles an inode the moment it is freed.
Rollback now asks the only question that matters — is anything left
inside? —
and hands the answer to `rmdir`, which is non-recursive and so refuses
to
remove a directory holding anything at all, yours or ours, in a single
step
with no check-then-delete window. The walk climbs from each restored
path and
stops at the tool's configuration directory (`.claude`, `.opencode`),
which is
never removed and never climbed past, so cleanup can only reclaim the
tree the
    install materialized.

## @agent-facets/protocol@0.31.1

### Patch Changes

- [#557](#557)
[`9a061c4`](9a061c4)
Thanks [@eXamadeus](https://github.com/eXamadeus)! - **New command:
`facet update` (aliased `facet upgrade`)** — moves the registry-backed
facets a project declares to newer releases. It reads `facets.json` and
`facets.lock`, asks the registry for each facet's range-respecting
target and its latest release, and shows both alongside what is
installed, so "why is this one not moving?" is answerable from the plan
itself. Plain `facet update` takes every target the declared specifier
already permits. `--latest` (`-L`) crosses those specifiers and rewrites
them by the smallest edit that admits the new version, preserving how
the intent was written: a pin stays a pin, `1.*` becomes `2.*`, `1.2.*`
becomes `2.4.*`, and `*` and `latest` are left exactly as authored.
**Previews and per-facet selection.** `--dry-run` prints the plan and
writes nothing — no manifest, no lockfile, no receipt, no assets, no
cache, and no adapter installation, which makes it safe on a machine
with no adapter connected. `--interactive` (`-i`) opens a picker for
choosing which facets move and which version each takes. Every row
starts on its **latest** release with nothing selected, so walking the
list and pressing `Space` takes the newest version of each facet you
pick — `Space` always means "yes, this one", and a facet you never touch
is left alone. `◀ ▶` (or `l`) moves a row to its range target first when
that is what you want instead. `--latest` is accepted alongside
`--interactive` but changes nothing there; it is how the non-interactive
run asks for what this screen already offers. The picker requires a real
terminal and fails immediately without one, before any registry lookup.
Git and local facets are named as unsupported rather than counted as
current — reporting them as up to date would claim something nothing
verified.
**Applying an update is an install.** Discovery runs read-only and takes
no project lock, so reading a plan never blocks another facet operation.
Application re-checks under the lock that the project has not moved
since the plan was reviewed, then runs the ordinary install pipeline:
the same verification, collision handling, MCP approval, rollback, and
atomic manifest/lockfile/receipt write. The version you reviewed is the
version installed — a release published in between does not silently
change it. Recorded materialization choices survive a version change.
There is no `--frozen-lockfile`: reproducing what the lockfile already
records is the opposite of what this command does.
**Breaking: `facet upgrade` is no longer a placeholder.** It previously
printed a not-yet-implemented notice and exited `0` without touching a
single file. It is now an alias of `facet update` — one command, one
help page, one behavior — so the same invocation contacts the registry,
may install adapters, takes the project lock, and rewrites
`facets.json`, `facets.lock`, the install receipt, and your materialized
assets. `update` is the canonical spelling, and `facet upgrade --help`
prints `Usage: facet update`. If something in your automation called
`facet upgrade` expecting a no-op, drop the call or make it explicit
with `facet update --dry-run`.
Neither name touches the CLI binary. That remains `facet self-update`.
**Protocol: version components are bounded by exact integer
representation.** The published version grammar now rejects a specifier
or locked version whose numeric component exceeds `2^53 - 1`, with an
error that names the magnitude rather than the form. Above that bound
two distinct releases are the same double — `9007199254740992` and
`9007199254740993` compare equal — so a comparison that decides which
release is newer, or whether a locked version still satisfies a manifest
range, could answer for a version that was never published. `facet
update` is the first command whose whole job is that comparison, which
is why the bound lands now. No real version is anywhere near it.

Co-authored-by: the-faceter[bot] <272408671+the-faceter[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant