Skip to content

docs(installer): the installer is invisible; entries own defaults, recovery and cleanup; one release authority - #18

Merged
xxchan merged 4 commits into
mainfrom
docs/installer-voice
Sep 29, 2026
Merged

xxchan merged 4 commits into
mainfrom
docs/installer-voice

Conversation

@xxchan

@xxchan xxchan commented Sep 29, 2026 •

Copy link
Copy Markdown
Member

Why

Ferry's installer is built on this contract. It printed this to its owner, whose ~/.local/bin/ferry was a hand-written launcher script:

Finding ferry-installer for darwin-arm64 on main...
Downloading ferry-installer (7ac50823-f020-47bd-9aca-632d3ac83b50)...
Not done: bin-not-standalone: /Users/…/.local/bin/ferry is a launcher script; ferry-installer never replaces it (remove it, or set FERRY_BIN_DIR). Nothing changed.

Each piece followed the contract's letter. The spirit it missed is the one agreed in earlier threads (2026-09-16/17 in #proj-raft-computer, 2026-09-21 in #all-staff-plus):

Users only touch entries: install.sh / install.ps1, the product's commands, the web upgrade. The installer is an internal implementation detail: never visible, never something a person is told to run. What can be recovered or cleaned up automatically is done by the entry or the installer itself. Cleanup failures are retried without reinstalling or restarting, and never become the user's job. Recovery uses the receipts, with no second cleanup-state system. manifest.json is historical debt: Hands holds the complete release listing.

What changes

docs/installer.html (and one sentence of index.html):

  • Voice (new, first rules):
    • People use entries; the installer is never named, and no line asks a person to run it.
    • Everything a person reads is about the product.
    • Every non-success outcome carries a reason in the user's words and a stable code, kept apart. A line never contains a code.
    • An obstacle gets one runnable next step, and that step is always an entry.
  • Entries (new):
    • An entry turns the person's request into a request with the consents it implies. The install script consents to replacing an occupant of the product's path.
    • An entry finishes what it can:
      • settle unfinished work first;
      • on unresolved, recover and try once more;
      • a product recovers its own interrupted remote upgrade on its next start;
      • a request K never started is recorded as failed before any change.
    • Cleanup is the installer's: retried silently, derived from receipts and K's state.
  • Worlds. A fresh install can find the product's path occupied: a launcher script, a link, a program that isn't the product.
    • It is almost always an earlier way of running the product, so the install script replaces it: moved aside under a name the line gives, never deleted, and the line says how to put it back.
    • Without that consent, the request is held. No question is asked.
    • The five worlds and the entries figure are unchanged: occupied is an obstacle within fresh. A usable binary of the product is still adopted.
  • One release authority. The <version>/manifest.json byte store (RELEASE_BASE, INSTALLER_RELEASE_BASE) is gone from the contract. The authority's answer names the release and the size and sha256 of every file. The crate's StaticManifestSource stays for examples and tests. Retiring it from the integration guide's sample is a follow-up.
  • Presence, --yes, the outcome table ("Fresh, path occupied"; "Recovery unresolved" now says to run the install command again) and the closing paragraph follow.

python3 scripts/check-docs.py passes.

The Ferry side of the same fix: ferry-installer 0.1.1 (a botiverse/ferry PR, linked once opened).

🤖 Generated with Claude Code

…pies its path

A product installer built on this contract printed, on a machine whose
~/.local/bin/app was a hand-written launcher script:

  Downloading app-installer (7ac50823-f020-47bd-9aca-632d3ac83b50)...
  Not done: bin-not-standalone: ~/.local/bin/app is a launcher script;
  app-installer never replaces it (remove it, or set APP_INSTALL_DIR).

Every part of that followed the contract's letter and missed its intent.
The contract said the line is in the user's words, but it did not say
that the installer itself is not something the person knows about, that a
reason for people and a code for software are two different things, or
what to do when the product's path holds something that is neither this
product nor another manager's install.

- Voice (new, first of the rules): everything a person reads is about the
  product; the installer's name, K's terms, ids, unchosen channels and
  codes are not. Outcomes carry a reason in the user's words and a stable
  code, kept apart in the receipt. An obstacle gets one runnable next
  step, and a decision that belongs to the person is asked (attended) or
  named as a command (unattended), never handed back as a chore.
- Worlds: a fresh install can find the product's path occupied. It is the
  person's: attended, ask to replace it (default no), move it aside under
  a name the line gives, say how to put it back; unattended, hold unless
  the request carries --yes. It is never deleted. The five worlds and the
  entries figure are unchanged: occupied is an obstacle within fresh.
- --yes, Presence, the outcome table and the closing paragraph follow.

scripts/check-docs.py passes.
…rity

Folds in how the owner framed this across earlier threads (2026-09-16,
09-17, 09-21):

- Users only touch entries: the install script, the product's commands,
  the product's remote upgrade. The installer is an implementation
  detail; no line names it or asks a person to run it (Voice).
- New "Entries" rules. An entry turns what the person asked for into a
  request with the consents that implies; the install script consents
  to replacing an occupant of the product's path, which is almost
  always an earlier way of running the product, so there is no
  question. An entry finishes what it can: settle first, recover and
  retry once on unresolved, a product recovers its own interrupted
  remote upgrade on its next start, and a request K never started is
  recorded as failed before any change. Cleanup is the installer's,
  retried silently, derived from receipts and K's state, never a
  reason to reinstall, restart or ask.
- Worlds, Presence, --yes and the outcome table follow. Recovery
  unresolved now says to run the install command again, not "repair".
- One release authority. The byte store with <version>/manifest.json
  (RELEASE_BASE, INSTALLER_RELEASE_BASE) is gone from the contract: the
  authority's answer names the release and the size and sha256 of every
  file it serves, and nothing else describes a release. The crate's
  StaticManifestSource stays for examples and tests (index.html).

scripts/check-docs.py passes.
@xxchan xxchan changed the title docs(installer): speak as the product; ask before replacing what occupies its path docs(installer): the installer is invisible; entries own defaults, recovery and cleanup; one release authority Sep 29, 2026
xxchan and others added 2 commits September 29, 2026 22:37
…staller and K

The last two commits added Voice, Entries, the occupied path and one
release authority in place, so the page said several things twice,
kept traces of the old model, and was ordered by history. This pass
orders it for a first-time reader and says each rule once.

- "The model" now leads: entries (the only things people use), the
  request, the installer (an implementation detail of the entries), K,
  and the release authority. The reads, the grid and the interface
  follow, then the rules in the same order: Voice, Entries, Request,
  Presence and consent, Lock and settle, Worlds, The product's answer,
  Verify, Repair, Report, Cleanup. Then the outcome table.
- Consent lives in one section. It was spread over Presence, Entries,
  Repair, --yes, the commands table and the outcome table, and still
  said "running the installer is the consent", "the repair consent
  rule" and "invocation consent".
- Cleanup is its own section. It was in Entries and again in Repair,
  in Request (housekeeping on replay, receipt retention) and in the
  closing paragraph.
- Entries keeps "finish what you can": retry once on unresolved,
  recover a remote upgrade on the next start, nothing else retried on
  its own (moved from Report).
- The closing paragraph is folded into Voice. Restatements of the reads
  section are gone from the rules; a dead lock owner moved to Lock and
  settle; "no separate manifest to agree with" is gone (nothing else
  describes a release says it).
- Outcome table: the presence columns hold only what presence changes.
  The occupied row's held case sat under "Unattended adds", a leftover
  of the --yes model; it is now a Held reason. "Another installer
  running" is "another install or upgrade in progress".

Changed requirements:
- Another manager's installation: the line names the manager and where
  it is, and no longer says how to remove it. A removal instruction is
  a chore, and the next step a person gets is always an entry.
- The next step is given when there is something to do, as the table
  already said; Voice had it unconditional.
- <PRODUCT>_INSTALLER_VERSION is marked as a setting for the product's
  developers, since people never see the installer.

Outside the contract:
- figures/entries-grid.html: "never retries on its own" became false
  when entries started retrying once on unresolved; it now reads
  "never retries a failed target". "seed from the running app" was
  false since adoption works running or stopped; it reads "seed its
  existing bytes".
- guide.html: exit 3 was "never the user's"; the contract gives the
  person a line for it and keeps exit 3 until a repair succeeds.
- prior-art.html: the verify rules taken from Raft Computer no longer
  include two sources that must agree.

scripts/check-docs.py passes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… update

The editorial pass dropped the removal hint, rightly (a chore, not an
entry), but left the person with nothing to do. The manager that owns the
installation is its entry, so its update is the next step.
@xxchan
xxchan merged commit 5aa04c6 into main Sep 29, 2026
10 checks passed
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