Files
orca/notes/skill-freshness-design.md
Brennan Benson 68fca0b076 Add safe skill freshness detection and update rail (#8637)
* Add safe skill freshness detection

* Accept observed copy-mode rail outcomes

* chore(skills): regenerate snapshot artifacts for the merged guide content

The rebase onto main picked up the reviewed guide fixes (#8624), so the
current manifest hashes and a new appended snapshot generation must
match those bytes; the registry keeps all prior snapshots so existing
installs classify as outdated rather than unrecognized.

* fix(skills): canonicalize snapshot file order and guard released history

Historical snapshots kept git ls-tree byte-order while the working-tree
walk and runtime observation use the sorted depth-first order, so any
future multi-file skill would misclassify older installs as unrecognized
and churn spurious registry revisions; all producers now share one
canonical order (no digest changes for today's single-file packages).
Also rejects executable files from shipped skills (Windows observation
cannot see execute bits, which would misclassify pristine Windows
installs) and adds an explicit append-only invariant for released
snapshots so a generation-logic change cannot rewrite them silently.

* fix(skills): throttle focus rescans and correct self-blocked placement copy

Every window focus re-read and re-hashed all installed packages, and the
nudge and panel each forced their own trailing rescan for one event; a
15s cooldown plus a shared invalidation latch keep one bounded scan per
event while install-change events stay immediate. Bundle artifacts are
now loaded once per run instead of re-parsed on every scan. A read-only
or otherwise unsupported outdated placement now explains that it blocks
itself instead of blaming a phantom sibling placement; the supported
topology set moved to shared so eligibility and copy cannot drift.

* feat(skills): move freshness surfacing to a lingering toast and update modal

The Skills page has been unreachable since its toolbox menu entry was
removed (#4535), so surfacing freshness there buried the feature behind
its own nudge. The nudge now lingers until acted on (ignoring it records
nothing; only the explicit close persists dismissal keys) and opens an
update modal hosting the pre-filled editable terminal, an honest
current/blocked summary, and the per-placement rows in a collapsed
Details section. A compact 'Check for skill updates' row in CLI settings
is the manual re-entry point. Skills page restored to main; design-doc
surfacing section records the venue decision.

* fix(skills): mount update dialog inside the link-routing provider and fold freshness into the setup rails

The dialog hosts a live terminal pane that requires the link-routing
preference context; mounted outside the provider it crashed the renderer
the moment an eligible update existed (caught by live QA — unit tests
mock the terminal). It now mounts inside the provider behind its own
recoverable boundary.

The separate 'Check for skill updates' settings button is gone: the
setup rails' own pill now carries freshness (Update available / Up to
date, falling back to Installed for blocked or unrecognized copies and
for non-local runtimes the local-only scan cannot vouch for), and
Re-check refreshes both installation detection and the freshness
inventory. Wired for the CLI, Orchestration, Computer Use, and
Per-Workspace Environments rails.

* fix(skills): use the sleek scrollbar style in the update dialog

* chore(skills): regenerate manifest for merged main (v1.4.142-rc.1)

Main advanced to 1.4.142-rc.1 with a v1.4.141 release, so the embedded
appVersion and release mapping were stale on the PR's merged tree. Only
appVersion and the new release entry change; no snapshot digests move
(released history preserved).

* fix(skills): bound and batch freshness work

* fix(skills): harden freshness integrity checks

* fix(skills): accept observed copy topology outcomes

* chore(skills): regenerate manifest for current main

* fix(skills): preserve update terminal lifecycle

* chore(skills): regenerate manifest for current main

* fix(skills): fail closed on stale freshness scans

* chore(skills): regenerate manifest for current main

* fix(skills): preserve freshness safety under focus churn

* feat(skills): group the update modal by skill with plain-language status

The Update skills modal now lists only skills that will update or that can't
(with why), grouped by skill with their install locations nested underneath —
no more one row per placement.

- Statuses collapse to "Update available" / "Can't update" at the skill level.
- A location's problem is a chip (Duplicate, Unrecognized, Inaccessible, Read
  only, In a repo, External/Broken link, Plugin cache) with a hover tooltip
  that explains what it means for the user and what to do.
- Up-to-date, unrecognized-only, and unreadable-only skills are hidden; a
  current/unrecognized/etc. location only appears when it explains a shown skill.
- Copy is de-jargoned (drops "copy"/"placement"/"snapshot"/"official copy") and
  names the mechanism as the npx skills update command, not "Orca's update".
- Rename the section to "Update details"; drop the unreachable newer-known state.

Renderer-only: derivation is a pure module (groupSkillFreshness) with unit tests;
no IPC or main-process change. Locales updated for all five languages.

* chore(skills): regenerate manifest for current main (v1.4.143-rc.0)

* feat(skills): don't let a duplicate block the update; clearer skipped copy

- Eligibility: a clean standalone duplicate no longer poisons the whole
  name — the canonical copy still updates and the duplicate is flagged;
  a duplicate-only skill stays unoffered.
- Update modal: "Can't update" -> "Skipped" with a reason-specific
  sentence (edited/read-only/in-a-repo/plugin/link); chips describe only
  the location state; footer "Check now" -> "Re-check".
- Settings sidebar nav pills go amber "Update available" when a skill is
  updatable, matching the setup cards.
- Localized new strings across en/es/ja/ko/zh.

* chore(skills): regenerate manifest for merged main (v1.4.144-rc.1)
2026-07-16 14:47:28 -07:00

263 lines
16 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Skill Freshness: Thin Stubs + Read-Only Detection
Status: adopted direction 2026-07-13. Phase 1 (guide sources + binary-served CLI) is
implemented, pending release; detection and stub migration are not implemented. This is the
authoritative plan. It supersedes the write phases of `skill-auto-update-design.md` (Phases
2–4: background updates, WSL, SSH) and the migration section of
`skill-guide-indirection-design.md`. The detection and content-identity research in those
notes still applies and is referenced below.
## Problem (unchanged)
Orca ships agent skills that teach coding agents to drive the `orca` CLI. Users install them
with `npx skills add stablyai/orca --global`. Installed copies are frozen files; the Orca
binary keeps moving. A stale skill tells an agent to use commands that are wrong or unsafe
for the binary it is driving.
## Decision
Three moves, replacing the in-app write machinery entirely:
1. **Structural fix — content lives in the binary.** All version-sensitive skill content is
served by the CLI (`orca skills get <topic>`), compiled at build time from authoritative
`skill-guides/` sources. Generated `skills/<name>/SKILL.md` files are the installable
discovery surface and become permanent thin stubs. Staleness becomes impossible for the
content that matters, rather than mitigated.
2. **Residual freshness — read-only detection, ecosystem-rail updates.** Orca detects
outdated official copies (content-addressed, LF-normalized identity) and pre-fills a
targeted `npx skills update <names...> --global` command in a terminal. The user reviews
and runs the skills CLI's own update command; Orca never submits it automatically and
never writes a byte into a skill directory.
3. **No persistent ownership or update state.** No ownership ledger, no adoption consent,
no background writer, no transactional publish/rollback stack, no settings toggle. The
one content migration (fat skill → stub) rides the same user-invoked npx rail. A small
dismissed-nudge set remains in app state; it grants no write authority.
### Why the write machinery was dropped (decision record)
- **Maintenance-per-use.** The transactional mutation stack (staging, rollback, crash
recovery, orphan sweeping, ownership ledger, installer attribution) is ~3.2K production
lines before its tests. It is the most correctness-critical code in the tree, built to
write into user-owned directories forever. Under the stub model it would run meaningfully
once. Every module is permanent Windows/WSL/SSH edge-case surface for a one-shot job.
- **Trust posture.** Ecosystem discourse (2026) consistently favors pinning and reviewable,
user-invoked updates over silent writes into `$HOME`; Orca has first-hand precedent of
user backlash from writing into user-owned config directories. Read-only detection has no
trust cost at all.
- **The rail already exists.** `npx skills update <names...> --global` is the ecosystem's
documented remedy, and once `skills/` contains stubs it delivers the migration without an
Orca-owned writer. The rail is treated as an external dependency with a tested contract,
not assumed trustworthy from its lock file alone.
- The full write implementation exists, reviewed and green, on branch
`brennanb2025/skill-auto-update-research` (PR #8496, closed as superseded). If in-app
writes are ever genuinely needed, start from that branch, not from scratch.
## Design
### A. Binary-served guides
```sh
orca skills list # one line per topic: name + when to use
orca skills get <topic> # full version-matched guide, markdown to stdout
orca skills get <topic> --full # include bundled reference docs, if any
```
- Topics are the skill names (orca-cli, orchestration, computer-use, …).
- Full version-sensitive content lives in `skill-guides/<topic>.md`. A generator embeds it
in a concrete CLI module and emits the installable `skills/<name>/SKILL.md` projection.
During the pre-stub release this projection may remain fat; after migration it is a stub.
- The generated-output-current check uses the existing generated-artifact-gate pattern used
by the manifest verifier. It asserts that generated guide data and installable projections
match their sources and that every stub topic resolves against the compiled guide table.
No network or runtime filesystem lookup is required. Unknown topic → nonzero exit + topic
list.
- Authoritative guides, generated projections, and embedded TypeScript are pinned to LF, and
the generator normalizes input before embedding it. Detection still normalizes text identity
because already-installed Windows copies may retain the historical CRLF shape.
- Topic names are append-only and aliased forever: a stub installed in 2026 must still
resolve in 2028. Renames add an alias, never remove one.
- Verb is `skills get` (not `guide`) to match the convention agents are already taught by
other tools in the wild.
### B. Stub format and command resolution
One stub per skill — frontmatter descriptions are the agent-routing layer and stay
per-skill registry entries.
```markdown
---
name: orca-cli
description: <unchanged per-skill trigger copy — the discovery surface>
allowed-tools: <the supported Orca CLI command names>
---
# Orca CLI
This file is a discovery stub, not the usage guide. The full, version-matched reference
lives in the `orca` binary itself.
Before using Orca commands, resolve the Orca CLI for this session and load the guide once:
<resolved-orca-cli> skills get orca-cli
Don't guess subcommands or flags from memory or from cached copies of this skill — they
change between Orca releases; the command above always matches the installed binary that
will handle subsequent Orca commands.
```
Rules:
- The permanent body says when to engage Orca, how to resolve its CLI, and where to fetch
the version-matched guide. It does not carry the changing command reference.
- A stub must never blindly invoke bare `orca` outside an Orca-managed terminal on Linux;
that name commonly resolves to the GNOME Orca screen reader. The contract must cover
packaged `orca`, Linux/WSL `orca-ide`, SSH relay `orca`, and development `orca-dev`, and
`allowed-tools` must cover every command the resolution contract can select.
- **Linux command decision (2026-07-13):** do not install a uniform global bare `orca` alias;
it would shadow or risk launching the GNOME Orca screen reader. Keep `orca-ide` outside
managed Linux terminals, the existing managed-terminal/SSH `orca` shims, and `orca-dev` for
development. The permanent stub therefore needs the short resolver exercised by the spike.
- First-generation stubs are hybrid: a minimal safe bootstrap plus the guide pointer. Thin
them further only after pointer compliance and old-binary behavior are measured. If
`skills get` is unavailable, the hybrid must provide a bounded legacy workflow and tell the
user that updating Orca restores the full version-matched guide; it must not dead-end or
invite the agent to guess the missing command surface.
### C. Read-only detection (kept from Phase 1, slimmed)
Kept as-is:
- Bundled `skills/` packages + current manifest + released-snapshot registry + release
mapping, with the generation script and merge-queue monotonicity gate (static data + CI,
not runtime machinery).
- LF-normalized text identity / exact-byte binary identity (the Windows CRLF finding
stands: exact-byte matching would misclassify every Windows install as modified).
- Bounded inventory work limits, topology classification (symlink dedup, external links,
plugin caches and repo scopes excluded), and the launch / focus / post-install triggers.
- The skills-CLI round-trip CI on macOS/Linux/Windows — extended from current-install tests
to historical-fat-install → targeted global update → stub migration. The matrix covers
LF/CRLF and provider aliases as a positive convergence contract, plus independent-copy
observation that accepts only unchanged historical or exact-current bytes. Post-update
bytes, not exit status, decide.
Slimmed:
- Statuses collapse to: `current`, `outdated` (exact match of an older released snapshot),
`newer-known`, `unrecognized`, and `inaccessible`. Without a ledger, Orca cannot honestly
distinguish a locally modified official copy from unrelated same-named content;
`unrecognized` says it may be edited or from another source. All `managed-*` states, the
ledger, adoption eligibility, and attribution are removed.
- Status and action eligibility are separate. External links, read-only locations, plugin
caches, repo scopes, and unsupported topologies remain informational even when their bytes
match an official snapshot.
- Dismissal state for the nudge is a simple local dismissed-set keyed by
(physical identity, skill, bundled revision) in app state — not a consent ledger.
### D. Surfacing
- **Surfaces (venue decision 2026-07-14):** a lingering toast, an update modal, and the
existing Settings setup rails for CLI, Orchestration, Computer Use, and Per-Workspace
Environments. Their installed pills carry safe freshness status, while their existing
Update and Re-check actions remain the per-skill path. The Skills page was
de-linked by #4535 (2026-06-02) — its only entry, the sidebar toolbox menu, was removed —
so it is no longer a venue; the freshness surface moved off it entirely. The behavior
contracts below (name-scoped eligibility, no auto-run, dismissal keys, re-inventory
triggers) are unchanged; only the venue moved.
- **Per-placement rows** (read-only): name, status badge, one-line explanation. `newer-known`,
`unrecognized`, `inaccessible`, and unsupported-topology rows are informational. They live in
the modal's collapsed **Details** section (auto-expanded when a placement is blocked).
- **Name-scoped update eligibility:** eligibility is computed across all discovered placements
of a name, not per row. Offer a name only when at least one placement is `outdated` and every
placement is an exact `current` or `outdated` official snapshot in a topology the validated
rail actually converges. With skills CLI 1.5.17 that means the canonical global copy and
provider aliases to it. Independent provider copies are informational and poison the offer:
empirical copy-mode testing produced both stale and converged provider copies in otherwise
equivalent 1.5.17 environments, so that topology is not deterministic enough to offer. One
`newer-known`, unrecognized, external, read-only,
inaccessible, repo/plugin, independent-copy, or otherwise unsupported placement poisons the
update offer for that name entirely.
- The action combines only eligible outdated Orca names into
`npx skills update <names...> --global` and opens the update modal's editable terminal with
that command pre-filled, leaving execution to the user. Never use an unscoped bulk update and
never auto-submit the command. Re-inventory after terminal exit, modal close, or focus; only
observed bytes, not the skills CLI exit status, determine success. When the eligible set
empties and every placement is `current`, the modal shows an up-to-date state; if placements
remain outdated-but-blocked or unrecognized, it says so honestly instead.
- **One lingering, non-repeating nudge**: count only eligible outdated skill names and offer the
same targeted action, which opens the update modal. The toast lingers (no auto-close) until the
user opens the modal or explicitly dismisses it; ignoring it (app quit) records nothing, so a
still-outdated skill may prompt once more next launch. A later inventory that resolves or blocks
the offered tuple retracts the stale toast without recording a dismissal. An outdated name
poisoned by another placement remains visible in the modal's Details but never produces an
unsafe nudge action. Dismissal is recorded per (physical identity, name, bundled revision) only
on explicit dismissal, so a newly outdated official placement or genuinely newer stub revision
may prompt once more. No
toggle — nothing automatic happens that would need one.
### E. Migration (fat → stub)
1. **Implemented, pending release:** from a fresh main-based PR, add authoritative guide
sources, generated embedded data, `orca skills list/get`, aliases, generated-output checks,
and local/SSH/WSL/dev tests. Keep distributed skills fat and ship this release first.
2. From a separate PR, land slim read-only detection and Skills-page/nudge UI, including the
name-scoped targeted update action and the real migration-rail CI. Keep distributed
skills fat.
3. Run the pointer-compliance spike against the released guide-serving binary, not a checkout
artifact. The binary must be publicly released before a stub PR merges because the skills
CLI installs from repository main, independently of Orca's desktop release train.
4. In one PR, convert only `orca-cli` to a first-generation hybrid stub and keep any final
thinning of that stub in the same change. This bumps its registry revision like any content
change. Existing users see an `outdated` exact snapshot and may run the targeted global
update; users of pre-guide binaries retain the hybrid bootstrap.
5. Cut an RC before the stable release and use that validation window to measure compliance,
task success, old-binary behavior, and token cost. Ship the thin form in stable only if those
gates pass; otherwise retain the hybrid. Convert the remaining skills gradually in later PRs.
Users who ignore the nudge keep working with their existing fat skills.
## Spike gate (before any stub ships)
Using the released guide-serving binary, install the proposed hybrid `orca-cli` stub in a
test home and run real agents (Claude Code, Codex) on representative Orca tasks. Measure:
- how often the agent resolves the correct packaged/Linux/WSL/SSH/dev command and fetches the
guide before its first Orca command;
- task success versus the fat skill;
- old-binary failure behavior; and
- net token cost (stub preload + one fetch versus fat preload).
Then test a thinner stub against the same corpus. Nothing converts, and the hybrid stub does
not thin, until the relevant variant passes.
## Risks and open questions
- **npx rail reliability.** The skills CLI update path has had false "up to date" results,
global Windows failures, missing global lock tracking, lossy lock migration, and copy-mode
topology changes. The historical-fat → targeted-global-update → stub CI is a release gate,
not an early-warning job. Detection always re-checks bytes after the user updates, so a
failed or no-op update re-surfaces `outdated` instead of lying. Minimum validated version is
1.5.17: 1.5.16 failed the provider-alias convergence contract, while 1.5.17 copy convergence
still varies by environment. CI pins 1.5.17 and probes latest; monitor and contribute upstream
fixes before broadening eligibility.
- **Trigger-copy iteration slows.** Improvements to stub descriptions reach existing
installs only when users run the npx command. Acceptable at stub-change cadence; the
compiled guides (the content that matters) are exempt by construction.
- **Multi-file skills.** Current shipped packages are single-file. If a future skill needs
scripts/assets, either the binary serves them (`--full` / `--script`) or that skill
accepts the fat-file decay model. Decide when it happens.
- **Remote hosts.** Detection ships local-host-only. Stubs make remote _content_ a non-issue:
SSH/WSL launchers forward to the host's bundled CLI, so the guide matches the command
surface that will handle subsequent requests. Remote stub installs can lag on trigger
copy, which is the accepted residual. The WSL/SSH reconciler phases of the old design are
retired, not deferred.
- **Agent Skills spec evolution** (frontmatter fields, allowed-tools syntax) is the most
likely future cause of a real stub update wave; the nudge path covers it.
## Relationship to prior notes
- `skill-auto-update-design.md`: Problem statement, empirical CLI-behavior findings
(verbatim-LF mac/linux, CRLF Windows, XDG lock location, symlink topology, released-blob
provenance) and the Phase-1 detection design remain valid inputs. Phases 2–4 (background
writes, WSL, SSH reconcilers) are retired by this document.
- `skill-guide-indirection-design.md`: The stub/CLI contract and prior-art survey are
folded in here; its migration-via-in-app-updater section is superseded by §E.