# Installer State Machine and Recovery

The unified installer uses a transactional P0–P9 model. It may report success only after P9 reasserts every applicable committed postcondition. Internal phases invoke the CLI by P3's absolute path; shell discovery is checked only at P8.

## Canonical phases

| Phase                           | Responsibility                                                                                                                    | Failure disposition                                                              |
| ------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- |
| P0 Resolve context              | Bind uid/username to the authoritative passwd HOME/shell, state privilege mode, architecture, libc, Node, and npm                 | Fail before mutation                                                             |
| P1 Preflight                    | Validate downstream tool closure (including `git` and `python3`), writable prefix, registry lane, disk/inodes, and exclusive lock | Fail before target mutation                                                      |
| P2 Acquire artifacts            | Resolve exact registry versions and an immutable framework commit; record lane and SHA-256                                        | Discard temporary work                                                           |
| P3 Install CLI                  | Install at the configured absolute prefix; require exact version plus target owner/group and non-writable executable mode         | Restore the prior prefix/npmrc snapshot                                          |
| P4 Install framework + skills   | Sync framework and consume a checkout-free, lane/versioned shipped-skill declaration                                              | Restore prior framework/runtime trees                                            |
| P5 Identity                     | Validate SOUL/USER content and private modes; require private credential storage and target owner/group                           | Restore generated identity/credential binding                                    |
| P6 Runtime linking / activation | Evaluate activation honestly; never treat dead enforcement hooks as active readiness                                              | Restore runtime activation files                                                 |
| P7 Services                     | Provision only requested services/resources after any required P5 credential commits                                              | Stop and restore requested services/resources                                    |
| P8 Shell discoverability        | Require fresh login and non-login shells of the actual target shell to resolve P3's path                                          | Restore shell profiles                                                           |
| P9 Verify + commit              | Re-run P0–P8, commit the manifest, and seal the journal                                                                           | Leave an honestly reported resumable failure or restore the pre-install snapshot |

The phase numbers are a cross-workstream contract and must not be renumbered.

## Side-effect-free check

```bash
bash tools/install.sh --check          # stable/latest lane
bash tools/install.sh --check --next   # prerelease lane
```

`--check`:

- emits exactly one `[P0]` through `[P8]` PASS/FAIL row;
- exits non-zero if any predicate fails;
- does not create the npm prefix, lock, journal, manifest, shell profile, or runtime file;
- uses temporary npm observation storage outside the target HOME and removes it before exit.

P4 currently fails as `NOT-MEASURED / UNDECLARED` until the installer publishes `~/.config/mosaic/.install-shipped-skills.json`. C1 deliberately does not select among the conflicting candidate populations; C5 owns publishing and fulfilling that declaration. Once present, the P4 predicate requires the declaration's lane/version to match the resolved install and every named skill to remain contained under `skills/<name>/SKILL.md` with matching loadable frontmatter.

## Durable journal

Each mutating run creates a private transaction directory:

```text
${XDG_STATE_HOME:-~/.local/state}/mosaic/install/
  active.json
  <UTC-run-id>/
    journal.ndjson
    journal.ndjson.sha256   # committed runs only
    commands.log
    snapshot/
```

Before each mutation scope is touched, `journal.ndjson` records:

- phase and path;
- whether prior state existed and where its snapshot lives;
- the reversal action;
- the captured command-output location and command status.

Journal, action-status, manifest, or command-log write/sync failure is fatal. An unrecorded mutation is not allowed. Command diagnostics are redacted before terminal output or durable logging; credential-shaped environment values, bearer values, auth tokens, and credentialed URLs are never deliberately persisted. Successful P9 runs append a seal event, write the SHA-256 sidecar, and make the journal and sidecar read-only. Required P4/P6 action failures are persisted in the manifest so a later `--check` cannot turn a failed action into a false pass.

Rollback roots must be non-overlapping, non-symlinked, target-user-owned strict descendants of canonical `HOME`; unsafe custom `MOSAIC_HOME`/`MOSAIC_PREFIX` values fail at P0. The same validation runs again immediately before recursive rollback. The OS lock is concurrency authority: if a process dies while `active.json` still says `in-progress`, a retry that acquires the free lock preserves the stale projection as `prior-active.json` and proceeds from the honestly retained partial state.

`active.json` is the current projection:

- `in-progress`: incomplete/open transaction;
- `rolled-back`: a fault restored the snapshot;
- `rollback-failed`: restoration failed or refused a replaced/unsafe target and requires manual recovery;
- `failed-resumable`: named postconditions failed and the recorded partial state remains for remediation;
- `committed`: P9 passed and the journal is sealed.

## Failure recovery

1. Read the named phase and remediation line from installer stderr.
2. Inspect `active.json`, then the referenced `journal.ndjson` and `commands.log`. Command output needed to diagnose a failure is preserved and surfaced; it is not redirected away.
3. For `rolled-back`, verify the target paths match their pre-install state before retrying.
4. For `failed-resumable`, repair the named phase owner requirement, then run `install.sh --check` before retrying the installer.
5. Do not activate the #869 enforcement hooks merely to turn P6 green. A broker-less host with those hooks is a failed P6 state.

## Greenfield CI gate

`.woodpecker/greenfield-install.yml` runs `tools/e2e-install-test.sh` from zero in Debian/glibc as a non-root uid with `env -i`. No host HOME, npm cache, credentials, or bind mount enters the target process. Checkout mode packages the complete current checkout into an archive, pins its SHA-256 through an internal fixture seam, and copies the self-contained fixture into the container; framework-installer changes in the PR are therefore exercised rather than fetched from an older remote branch.

The C1 fixture intentionally returns an attributable RED while C2–C5 remain open. CI itself remains green only when the fixture's final P0–P9 verdicts, required discriminator rows, seeded secret-canary scan, and non-zero exit match the versioned contract in `tools/fixtures/greenfield-expected-red.tsv`. The comparator validates the complete three-case schema before selecting a case: exactly one exit and P0–P9 disposition per case, pinned require/forbid populations, and no duplicate or unknown rows. Any later remediation that changes an observed verdict makes CI red until the owning lane deliberately updates that manifest:

- `git` present: P1 and strict P3 pass; P4/P5/P6/P8 fail for their own reasons; P9 refuses success.
- `git` absent: P1 fails before target mutation and the installer emits no `Done.`.

The fixture is lane-parametric:

```bash
bash tools/e2e-install-test.sh --lane next --git present
bash tools/e2e-install-test.sh --lane main --git present
```

CI exercises both lane parameters as expected-RED structural checks. A separate remote-contract arm fetches the installer at the immutable CI commit, rejects failed or empty HTTP-success bodies, compares it to the reviewed `tools/install.sh.sha256`, and executes that exact fetched artifact. The P2–P8 fault matrix runs the real phase actions (including the P3 npm path, P4 framework path, and wizard path) rather than synthetic representative writes, then compares the complete target tree to its pre-install fingerprint. Delivery targets `main` under the trunk-only merge rule; `next` remains a non-merging integration lane. The linked installer issue stays open after merge and closes only after Jarvis independently validates the greenfield behavior.

## Source trust boundary

Remote installer mode requires a non-empty body and an expected SHA-256 before execution. Remote source-archive mode separately pins the resolved commit, records the archive SHA-256, limits compressed/expanded size and entry count, and rejects traversal, links, devices, and special files before extraction. These controls provide immutable run provenance and archive safety, not an independent signing root. Signed artifact metadata/provenance is explicitly deferred by the canonical greenfield PRD; C1 does not invent a signing system. The checkout and remote CI seams verify reviewed digests before executing their artifacts.
