Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 95 additions & 1 deletion docs/concepts/interaction-pattern-catalog.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ Map P0/P1 catalog rows to canary archetypes before picking commands:
| --- | --- | --- | --- | --- | --- |
| Work Routing | IP-001, IP-002, IP-003, IP-007, IP-008, IP-021, IP-029 | Hot-path route canary; Planning governance canary when cadence or repair is involved | `quota should-run`, `interaction_contract`, `work_lane_contract`, scheduler hint, handoff todo state | one eligible delivery fixture, one blocked/fallback fixture, one quiet or monitor fixture | agent turn routing is unsafe: it may spend, wait, notify, or choose fallback incorrectly |
| Human Decision | IP-004, IP-014, IP-017, IP-027, IP-030, IP-033 | Scoped decision canary; Product/readiness canary when first-screen human copy changes | user todos, decision scope, operator-gate/reward preview, deferred resume candidates | one concrete user ask, one scoped non-blocking gate, one preview-or-append dry run | humans may be asked the wrong question, or an agent may continue without the needed decision |
| State And Boundary | IP-005, IP-006, IP-011, IP-016, IP-019, IP-020, IP-022, IP-023, IP-025, IP-026, IP-028, IP-031, IP-032 | Projection and boundary canary; Hot-path route canary when the projection feeds quota/status | active state, todo metadata, task graph, authority source, claim lease, completed-work archive, connector runtime policy, public/private scan | fixture state plus structured projection check; boundary scan for touched public files | compact state and executable truth diverge, so dashboards and agents may trust stale or unsafe authority |
| State And Boundary | IP-005, IP-006, IP-011, IP-016, IP-019, IP-020, IP-022, IP-023, IP-025, IP-026, IP-028, IP-031, IP-032, IP-035 | Projection and boundary canary; Hot-path route canary when the projection feeds quota/status | active state, todo metadata, task graph, authority source, claim lease, completed-work archive, install ownership, connector runtime policy, public/private scan | fixture state plus structured projection check; boundary scan for touched public files | compact state and executable truth diverge, so dashboards and agents may trust stale or unsafe authority |
| Evidence Lifecycle | IP-012, IP-015 | Evidence lifecycle canary; Product/readiness canary when evidence is rendered | external handle observation, benchmark lifecycle reducer, compact result projection | compact public-safe evidence fixture with raw-material exclusion assertions | progress evidence may be missing, double-counted, or represented with unsafe raw material |
| Planning Governance | IP-010, IP-013, IP-018, IP-024 | Planning governance canary; Hot-path route canary when cadence changes affect execution | stalled run history, autonomous replan obligation, repair delta, cadence hint, plan-to-todo writeback | two-turn stalled fixture plus repair/writeback delta assertion | the agent may keep planning in prose while the machine-visible frontier stays unchanged |

Expand Down Expand Up @@ -343,6 +343,7 @@ Projection, authority, write scope, and lease integrity.
| P1 | IP-028 | Connector Runtime Boundary | Connector/runtime owners | notify only if the required owner decision is missing | enforce runtime allow/deny policy before browser or API connector reads can autoload raw material |
| P1 | IP-031 | Manager Context Is Not Turn Authority | Manager connection owner | no interruption; retention is silent | retain group context only and act only on a provider-native mention, verified reply, or existing typed authority |
| P1 | IP-032 | Completed Work Archive With Durable Decision Retention | Archive selector plus controller | no interruption; preview-then-execute readback | treat archived done work as history, keep durable decisions authoritative, and never move another role's lane |
| P1 | IP-035 | Install Ownership Is Not An Update Permission | Install lifecycle owner plus user | no silent mutation; report the owning installer and its command | classify the install before mutating it; when LoopX does not own it, hand back the owner-owned command instead of switching install channels |

### Evidence Lifecycle

Expand Down Expand Up @@ -2419,6 +2420,99 @@ quietly disagree with what the operator believes happened.
assertion that `--role user` leaves `Agent Todo` byte-identical is proposed
and not yet landed.

#### IP-035 Install Ownership Is Not An Update Permission

**Trigger**

- `loopx update` or `loopx doctor` reports upgrade drift: `requires_upgrade=true`
or an install-freshness status that no longer matches the release manifest;
- the active install is one of three owned kinds, `release_snapshot`,
`python_distribution`, or `live_checkout`, and only some of them are writable
by LoopX itself;
- an agent, automation, or operator script wants the upgrade to actually happen
in this turn rather than be reported.

**Expected behavior**

An update is a write to an installation LoopX does not always own. Three rules
keep "newer version available" from becoming "rewrite whatever is installed".

1. **Classify before you mutate.** `install_lifecycle` names the
`install_kind`, the `owner` (`loopx_release_snapshot`,
`python_package_manager`, or `source_checkout`), `loopx_apply_supported`,
the `execution_driver`, and the `owner_upgrade_command`. No mutating step
runs before that classification exists.
2. **An unowned install fails closed instead of guessing.** When
`loopx_apply_supported=false`, `update apply` returns `ok=false`,
`commands.apply=None`, `changes_applied=false`, and
`next_action.kind=use_installation_owner`. It must not substitute a
different installer, so a `custom-manager` Python environment never gets a
guessed `pip install`; it must not run `git pull` on a `live_checkout`; and
it must not replace a source checkout with an archive snapshot.
3. **Report the owner's command, then stop.** The payload carries
`owner_upgrade_command` (or `plan.install_command`) and
`post_update_validation=loopx doctor`. When no owner command exists, the
correct output is "this install is owned by X" with
`next_action.command=None`, not a silent success and not a fabricated
command.

IP-006 owns the case where a required *write scope* is not projected at all.
IP-035 is the sibling case: the scope is known, but the write target itself has
a different owner. IP-030 owns revision-guarded preview/apply for machine
configuration; the same "exact target plus explicit approval" discipline applies
here, except the approval belongs to the install owner rather than to LoopX.

**Visual Model**

```mermaid
flowchart TD
A["requires_upgrade / freshness drift"] --> B{"install_lifecycle.install_kind"}
B -->|"release_snapshot"| C{"POSIX?"}
C -->|"yes"| D["loopx_apply_supported=true<br/>atomic snapshot replace"]
C -->|"no"| E["owner: install-windows.ps1<br/>apply=None"]
B -->|"python_distribution"| F{"installer is pip or pipx?"}
F -->|"yes"| G["driver python_pip / python_pipx<br/>upgrade the owning environment"]
F -->|"no"| H["owner: package manager<br/>no pip guess, apply=None"]
B -->|"live_checkout"| I["owner: source_checkout<br/>no git pull, no channel switch"]
D --> J["loopx doctor revalidation"]
E --> K["use_installation_owner<br/>ok=false, changes_applied=false"]
H --> K
I --> K
```

**Bad smell**

An agent sees `requires_upgrade=true` and reaches for the installer it knows
best: `pip install --upgrade loopx` inside a checkout install, `pipx upgrade`
against an environment LoopX does not own, or `git pull` on a contributor
workspace. The result is a second LoopX in a different environment while `loopx`
on `PATH` still resolves to the old one, or a source checkout silently converted
into an archive snapshot. The operator experience is "doctor says I am behind"
forever, plus an environment nobody can attribute ownership for.

The mirror-image smell is reporting success without writing:
`unsupported_install_owner`, an `apply=None` command, or a `None`
`next_action.command` is rendered as "update complete", so the same drift is
rediscovered next turn. A third smell is a fixture that only ever exercises the
pip path, so no test can tell an owned install from an unowned one.

**Validation**

- `tests/test_self_update_runtime_activation.py` owns the mutation and negative
cases: `test_live_checkout_apply_never_mutates_git_or_switches_install_channels`,
`test_unknown_package_manager_apply_fails_without_guessing_pip`,
`test_python_distribution_apply_uses_the_owning_interpreter_pip`,
`test_pipx_distribution_apply_preserves_the_pipx_environment`, and
`test_windows_execute_update_fails_closed_without_launching_bash`.
- `tests/test_doctor_install_freshness.py` pins install-kind classification
(`live_checkout`, `python_distribution`) behind the freshness contract.
- `loopx/self_update.py::_install_lifecycle` owns the kind, owner, driver, and
`owner_upgrade_command` predicate; `loopx/doctor.py` supplies the install
snapshot it classifies.
- `examples/loopx-update-smoke.py` and `docs/guides/installing-loopx.md` own the
operator-facing update, activation, and recovery path.
- `examples/interaction-pattern-catalog-smoke.py` protects this entry.

### Evidence Lifecycle

#### IP-012 External Evidence Observation
Expand Down