Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

upskill

upskill lets you author and distribute AI-assistance content — rules, skills, and agents — across multiple AI coding clients (Claude Code, GitHub Copilot, opencode) from a single source of truth. One CLI, one set of files, per-client output generated on demand.

upskill add owner/repo               # install everything from a source repo
upskill update                       # pull latest, regenerate per-client files
upskill list                         # see what's installed
upskill new skill code-review        # scaffold a new SSOT skill (in a registry)

Two roles

RoleLives inCommands
ConsumerA project that uses AI assistanceadd, remove, update, list, doctor, search
AuthorA source-registry reponew, lint, fmt

Consumer projects only ever contain generated per-client files. Source registries hold the canonical SSOT items (RULE.md, SKILL.md, AGENT.md, *.bundle.yaml). The same repo can play both roles; SSOT and generated outputs do not mix in the same tree.

Where to next

Getting started

Install

Linux / macOS:

curl -fsSL https://raw.githubusercontent.com/driftsys/upskill/main/install.sh | sh

UPSKILL_VERSION pins a release tag (default: latest) and UPSKILL_INSTALL_DIR overrides the install location (default $HOME/.local/bin).

Windows (native — CMD / PowerShell):

cargo install upskill

Requires a Rust toolchain. The binary has no other runtime dependencies. HOME and USERPROFILE are both recognised as the global install root, so the standard Windows USERPROFILE variable works without any extra setup.

Windows (WSL): use the Linux install command above inside your WSL terminal.

Cross-platform alternative:

cargo install upskill

Or download a pre-built binary directly from the releases page.

upskill is a single static binary with no runtime dependencies.

Install your first source (consumer)

Inside a project where you want AI clients to pick up rules / skills / agents:

# Install everything from a source repo
upskill add driftsys/skills

# Or install a curated bundle (by name — resolves the .bundle.yaml automatically)
upskill add driftsys/bundles platform-baseline

# Pin to a tag, branch, or commit
upskill add driftsys/skills@v1.2.0

Generated files land in .claude/, .github/, and .agents/. The .upskill-lock.json file in your repo records what was installed and at which ref — commit it so other developers and CI regenerate the same content.

Verify state any time:

upskill list      # what's installed
upskill doctor    # any drift between lockfile and on-disk output?
upskill update    # re-fetch sources and regenerate

Configuring registries

To search content from corporate or custom registries, add them to your config:

# ~/.config/upskill/config.yaml
registries:
  - name: corp
    source: https://gitlab.com/mycompany/ai-skills
  - name: team
    source: myorg/team-skills

Then upskill search will include results from all configured registries. You can also search a specific registry:

upskill search auth --registry corp

Scaffold your first item (author)

Inside a source-registry repo (where SSOT items live), the recommended layout puts all items and bundles under a top-level skills/ directory (see Conventions):

mkdir -p skills && cd skills
upskill new skill code-review

This writes skills/code-review/SKILL.md with the minimum frontmatter the format spec requires (item directories live at one level under the source-registry root per format-spec §2.1). Open it, replace the TODO description and body, then run:

upskill lint --strict   # validate against the format spec
upskill fmt             # canonicalise YAML frontmatter

You can also scaffold rules (upskill new rule <name>) and agents (upskill new agent <name>). Bundles are plain *.bundle.yaml files that sit alongside the items they reference, also under skills/.

Man pages

upskill ships a man-page generator. Run it from the source tree:

just man             # or: cargo run --example mangen --release

That writes one .1 per command into target/man/:

target/man/upskill.1
target/man/upskill-add.1
target/man/upskill-doctor.1
…

Install system-wide:

sudo cp target/man/*.1 /usr/local/share/man/man1/
man upskill          # or: man upskill-add

Where to next

  • Commands — every verb explained, with flags.
  • Recipes — CI, private repos, pinning, cleanup.
  • Portable format — the on-disk SSOT contract the lint validates against.

Commands

upskill ships eleven commands. They split cleanly into consumer (run inside a project that consumes AI-assistance content) and author (run inside a source-registry repo).

CommandRolePurpose
addConsumerInstall content from any source.
removeConsumerRemove installed content.
remove-mcpConsumerUnregister an MCP server configured by a bundle.
updateConsumerPull latest, regenerate changed items.
listConsumerShow installed content from the lock file.
doctorConsumerVerify installation consistency.
searchConsumerLook up skills via the public registry.
indexConsumerBuild or manage the local registry index cache.
newAuthorScaffold a new rule, skill, or agent.
lintAuthorValidate SSOT files against the format spec.
fmtAuthorCanonicalise YAML frontmatter and format the markdown body.

The library’s strict source-manifest capture supplies values to the implemented pure item compiler, which derives complete per-client entrypoint and resource outputs. The inactive captured-rule desired-set producer and pure lifecycle reconciliation are implemented. Current commands keep their existing discovery, hashing and persistence behavior; they do not invoke capture or activate the owned lifecycle. Captured bytes do not establish source authenticity, an atomic snapshot or qualified filesystem containment. See the strict captured source manifest specification.

The library’s pure files::plan::plan_files API plans complete owned whole-file sets from explicit desired bytes and supplied observations. Current commands do not call it or change their lifecycle behavior. Opt-in source-backed rule orchestration connects desired sets, qualified observation, combined state and guarded execution/recovery; native Codex qualification and production activation remain Gate 6 work. The complete mixed library coordinator is described below. See the owned whole-file planning specification.

The library’s pure compile::item::compile_item API produces complete per-client entrypoint and resource files from validated captured bytes, preserving resource modes and rejecting unsupported active overrides. It does not activate lifecycle planning, execution, state changes or native Codex support. Current commands keep their existing installer behavior. See the captured item compilation specification.

The pure library operations::plan::plan_owned_artifacts API accepts one complete mixed Set per changed installation, containing dedicated files and instruction contributions, or explicit Remove. Callers supply ownership and exactly one observation per affected location. The combined result preserves manual bytes, aggregate modes, no-op actions and interleaved prior record order; cross-form collisions and affected ancestor paths conflict. Current commands do not invoke this coordinator. The opt-in source-backed rule library adapter now connects historical and explicitly selected Codex rule desired sets to lifecycle planning, qualified source/destination evidence, combined state and guarded execution/recovery. The Codex library route exposes reader reports separately from ownership; it adds no CLI flag. Production commands retain schema 1; production activation and native Codex qualification remain Gate 6 work. See the mixed owned-artifact planning specification.

Global flags

These work on every subcommand:

FlagEffect
--no-colorDisable colored output. Honored alongside NO_COLOR, UPSKILL_NO_COLOR, TERM=dumb.
-q, --quietSuppress informational stdout. Errors and exit codes are unchanged.
-h, --helpShow help.
-V, --versionShow version.

Consumer commands

upskill add <source> [items...]

Install content from any source.

upskill add owner/repo                              # GitHub shorthand
upskill add owner/repo:path/to/items                # subfolder
upskill add owner/repo@v1.2                         # pin to tag
upskill add owner/repo@main                         # pin to branch
upskill add owner/repo@abc123                       # pin to commit SHA
upskill add https://gitlab.com/owner/repo           # GitLab.com
upskill add https://git.example.com/owner/repo      # any https git host (self-hosted, Gitea, …)
upskill add https://gitlab.com/group/subgroup/repo  # nested groups (any depth)
upskill add ./path/to/local                         # local directory
upskill add owner/repo:platform.bundle.yaml         # bundle file (explicit path)
upskill add owner/repo platform-baseline            # bundle by name (resolves .bundle.yaml)

upskill add <source> installs everything the source contains. Append item names to filter:

upskill add driftsys/skills code-review secret-scanner

Default scope is --project (writes into .agents/... of the current repo), falling back to --global ($HOME/.agents/...) if you’re not inside a git repo. Pass either flag explicitly to override.

Client selection. By default add emits output for every client. Restrict it to the clients you actually use with one or more of --claude, --copilot, --vscode, --opencode (an allowlist — naming any restricts to exactly those):

upskill add owner/repo --claude               # only .claude/**
upskill add owner/repo --copilot --vscode     # only .github/** (+ VS Code MCP)

--vscode writes the shared .github/** rules/skills/agents (VS Code reads Copilot’s tree) plus VS Code’s own MCP config; --copilot --vscode writes .github/** once and forks only the MCP config. To persist a selection across runs, set clients: in config (.upskill/config.yaml for the project, ~/.config/upskill/config.yaml globally):

clients: [claude, opencode]

Precedence is flag > project config > global config > all clients. A per-invocation flag scopes that run only and never rewrites config. An item whose author audience: excludes every selected client is warn-skipped. See ADR-0012.

Supporting files in an item directory — anything besides the entrypoint and per-client override files, e.g. scripts/, references/, assets/ — are copied into each client’s output alongside the rendered item. For rules and agents that render to a flat file (Claude Code, GitHub Copilot), resources go into a sibling <name>/ directory and the body’s relative links are rewritten to match. See format-spec §2.4.

Bundle plugins. When a bundle declares a plugins: map (format-spec §3.7), add installs those client-native plugins by shelling out to each client’s own CLI (claude plugin install, copilot plugin install, code --install-extension, opencode plugin) — they are not rendered through the generation pipeline. If a client’s CLI is not on PATH, the plugin is skipped (not an error) and recorded as skipped in the lockfile; upskill doctor reports skipped and missing plugins. Removing the bundle cleans up its plugins.

Bundle MCP servers. When a bundle declares an mcps: map (format-spec §3.7), add configures those Model Context Protocol servers into each targeted client’s MCP configuration — CLI-first (claude mcp add ...), falling back to writing the client config file (e.g. .mcp.json) when the CLI is absent. MCP configuration is idempotent, and a single MCP failure or skip never fails the rest of the bundle install. Configured and warn-skipped servers are recorded in the lockfile; remove one with upskill remove-mcp. If an MCP server needs an environment variable that is unset, add warns so you can export it.

upskill update [name...]

Pull latest sources and regenerate changed items.

upskill update                       # update everything
upskill update code-review           # update one item
upskill update --dry-run             # preview changes without applying

update always fetches before regenerating; there is no separate sync. It also re-runs the generation pipeline against the current upskill version, so client-format updates land without a separate command.

update honours the same client-selection flags and clients: config as add (--claude, --copilot, --vscode, --opencode). With no flag and no config, update preserves each source’s recorded selection — a bare update of a --claude-only install stays Claude-only rather than re-expanding to all clients. Pass a flag (or set clients:) to change the selection on update.

Older installations may record gitlab:org/repo or gitlab+gitlab.example.com:org/team/repo:skills/team.bundle.yaml source labels. update reads unambiguous historical labels and writes the corresponding HTTPS labels after a successful reinstall. Installed aliases, pinned refs and bundle membership survive. update --dry-run, list and doctor leave recorded labels and generated bytes unchanged. New add commands still require modern syntax.

For an explicit port, historical state must separate the port from the namespace: gitlab+gitlab.example.com:8443:org/team/repo. Malformed or ambiguous authorities receive reconciliation guidance before fetch. See the historical grammar and limits before correcting a label; verify the intended HTTPS remote, ref and subfolder. This compatibility step retains schema 1 and does not establish output ownership.

upskill remove [name...] [--source <label>]

Remove installed items.

upskill remove code-review                         # remove one item
upskill remove code-review secret-scanner          # remove several
upskill remove --source github:driftsys/skills     # remove every item from a source
upskill remove --global code-review                # global scope

Bare upskill remove is rejected — be explicit. Ancillary files (CLAUDE.md, opencode.json, .vscode/settings.json) are not touched.

--source triggers a y/N confirmation prompt on a TTY (it removes every item from that label at once). Pass -y / --yes to skip. Non-interactive contexts (CI, pipes) skip the prompt automatically.

upskill remove-mcp <name>

Unregister an MCP server that a bundle install configured (see the mcps: note under upskill add).

upskill remove-mcp drawio            # remove from the current project
upskill remove-mcp drawio --global   # remove from the $HOME scope

<name> is the upskill-level MCP name recorded in the lockfile. remove-mcp unregisters the server from the client (claude mcp remove <name>, falling back to deleting the entry from the client config file when the CLI is absent) and drops the LockedMcp entry from the lockfile. Scope follows the same --project / --global rules as add, auto-detecting global when cwd is not inside a git repo.

upskill list

Show installed content from .upskill-lock.json, grouped by kind. Bundles, when present, are surfaced as a separate section.

The --available view (items discoverable from configured sources) is deferred for a future release; v0.2.0 ships the installed-state view only.

Pass --json for a stable machine-readable document:

{
  "rules":   [{ "kind": "rule",  "name": "...", "source": "...", "git_ref": null }],
  "skills":  [{ "kind": "skill", "name": "...", "source": "...", "git_ref": null }],
  "agents":  [{ "kind": "agent", "name": "...", "source": "...", "git_ref": null }],
  "bundles": [{ "name": "...", "source": "...", "git_ref": null, "items": [] }]
}

kind is one of "rule", "skill", "agent". git_ref is the pinned ref/tag/branch when the source is a git URL, otherwise null. source matches the lockfile label (local:/path or github:owner/repo etc.).

upskill doctor

Verify on-disk state matches .upskill-lock.json. Reports drift in six independent buckets:

  • Missing per-client output files — reinstall fixes (upskill add <source>).
  • SSOT hash drift on local: sources — upskill update fixes.
  • Lockfile entries with no recoverable source (the local path went away, or the named item was removed in the source) — upskill remove to clear.
  • Missing plugins — recorded as installed in the lockfile but no longer found in the client. Likely uninstalled out-of-band; upskill update reinstalls them. Causes exit 1.
  • Skipped plugins (informational) — recorded as skipped because the client CLI was not on PATH at install time. Install the CLI then run upskill update to install them. Does not cause exit 1.
  • MCP servers — each Claude-client MCP entry in the lockfile is checked against claude mcp list. A server in the lockfile but not registered in the client causes exit 1 (upskill update reconfigures it). A missing claude CLI or a failed query is advisory and does not cause exit 1.

Exits 0 when clean, 1 when any drift bucket is non-empty (missing outputs, stale hashes, orphan entries, missing plugins, or unregistered MCP servers). Skipped plugins and unverifiable MCP entries are informational warnings and do not affect the exit code. doctor never fetches; remote-source drift detection is update --dry-run.

Pass --json for a stable machine-readable document. Exit code is unchanged.

{
  "missing_outputs": [
    { "kind": "skill", "name": "...", "missing_files": ["..."] }
  ],
  "stale_hashes": [
    { "kind": "rule",  "name": "...", "source": "local:...",
      "stored_hash": "...", "current_hash": "..." }
  ],
  "orphan_entries": [
    { "kind": "agent", "name": "...", "source": "local:...",
      "reason": "local-path-gone" }
  ],
  "missing_plugins": [
    { "name": "superpowers", "client": "vscode",
      "identifier": "anthropic.superpowers", "bundle": "baseline" }
  ],
  "skipped_plugins": [
    { "name": "superpowers", "client": "claude",
      "identifier": "superpowers@anthropics/claude-plugins",
      "bundle": "baseline" }
  ],
  "mcp_entries": [
    { "name": "drawio", "client": "claude", "bundle": "baseline",
      "status": "not-registered" }
  ]
}

reason is "local-path-gone" or "item-missing-in-source". Hashes may be null when the SSOT can’t be hashed (e.g. unreadable). MCP status is one of "ok", "not-registered", "cli-not-found", or { "query-failed": { "stderr": "..." } }. The plugin and item arrays are always present (possibly empty); mcp_entries is omitted when no MCP servers are recorded.

upskill search <query>

Search the public skills registry and any configured registries.

upskill search code-review
upskill search code-review --limit 20
upskill search auth --registry corp
upskill search auth --kind rule
FlagDescription
--registry <name>Search only a specific configured registry.
--kind <kind>Filter results by item kind (skill, rule, agent).
--limit <n>Maximum number of results (default 10).

upskill index

Build or manage the local registry index cache.

upskill index [--registry <name>] [--clear]
FlagDescription
--registry <name>Rebuild only a specific registry.
--clearRemove all cached indexes.

Without flags, rebuilds the index for all configured registries.

Author commands

Run inside a source-registry working tree. Each refuses to run inside a consumer project (detected by .upskill-lock.json at the path’s root) so you don’t accidentally lint generated outputs or scaffold into the wrong tree.

upskill new <kind> <name>

Scaffold a new SSOT item directory. <kind> is one of rule, skill, or agent; <name> is both the item name and the directory name (lowercase letters, digits, hyphens; max 64 chars).

upskill new rule  no-direct-database-access
upskill new skill code-review
upskill new agent security-reviewer

Creates <name>/<KIND>.md relative to the current directory with the minimum frontmatter the format spec requires — there is no <kind> parent folder and no separate “destination” argument. The folder is the <name> you pass, so to place an item under skills/ you run the command from inside skills/ (see Conventions). Agents get mode: subagent and model: sonnet so the file is generation-ready out of the box. The scaffold round-trips through upskill fmt as a no-op and passes upskill lint --strict.

Co-location. Run new again with a different kind and the same <name> to add that kind as a sibling entrypoint inside the existing item directory (format-spec §2.1):

upskill new skill api-handler   # → api-handler/SKILL.md
upskill new rule  api-handler   # → api-handler/RULE.md (added alongside)

This expresses one capability across multiple kinds (for example a skill paired with its enforcing rule). Co-location is refused only when an existing skill entrypoint’s name: diverges from <name>; rule and agent siblings MAY carry a divergent name.

upskill new is an author command — it refuses to run inside a consumer project (one with a .upskill-lock.json).

upskill lint [paths...]

Validate SSOT files against the format spec. Six rules ship out of the box:

Rule IDSeveritySource
frontmattererrorformat-spec §3
name-matches-direrrorformat-spec §2.1
body-h1warningformat-spec §5.1
fence-langwarningformat-spec §5.2
body-formatwarningformat-spec §3.8
directiveerrorformat-spec §6.3
upskill lint                # lint everything in the working tree
upskill lint my-skill/      # lint a single item directory
upskill lint --strict       # CI mode: warnings become errors

upskill fmt [paths...]

Canonicalise YAML frontmatter (key order, indentation, alphabetised unknown keys) and format the markdown body via dprint (the same formatter the generation pipeline uses). The frontmatter↔body seam is normalised to a single blank line; YAML comments and prose wrapping are preserved.

upskill fmt                  # format everything in the working tree
upskill fmt my-skill/        # format a single item directory

Files whose frontmatter and body are already canonical are left untouched (no mtime thrash).

State files

.upskill-lock.json lives in one of two places depending on scope:

ScopeLocationCommitted?Purpose
Project<cwd>/.upskill-lock.jsonYesDeterministic regeneration in CI.
Global$HOME/.upskill-lock.jsonNoCross-repo continuity for global installs.

The lockfile carries a top-level schema: 1 field for forward compatibility.

An internal library projection can describe typed legacy records and candidate historical output paths without reading files or changing state. It preserves raw records and leaves requested selections unresolved. This prerequisite adds no CLI command and leaves existing commands and schema-1 persistence unchanged; it does not migrate state or adopt output files.

The pure library instruction planner can compute owned contribution changes from caller-supplied observations. It preserves untouched owners and manual bytes, verifies managed regions through the shared body reader, and retains exact before bytes even for content no-ops. Last-region removal retains manual bytes and keeps the file, which is empty only when no content remains. Affected whole files and multi-owner regions conflict; unrelated records survive. This library prerequisite adds no command. A separate pure Codex instruction producer now supports an explicitly supplied Codex CLI revision-1 rule contract. It is library-only: existing commands have no Codex selection flag. Shared readers are reported separately from ownership. The opt-in library with_prepared_codex route now resolves selected local/Git rule sources, exposes complete reader reports and applies guarded add/update/remove operations. It requires exactly Codex CLI revision 1; native qualification and production activation remain pending. The separate pure whole-file planner is implemented as described in the specification. The separate captured item compiler derives complete per-client entrypoint and resource outputs from captured bytes. Qualified scope roots, writer locking, destination observation and source observation are implemented as separate inactive library capabilities. Final-head hosted source qualification remains required. Reference resolution and per-owner association integration now support removal planning. Opt-in source-backed rule orchestration connects qualified source evidence and captured-rule desired sets to complete add/update planning, combined state persistence and guarded execution/recovery. Native qualification and production activation remain pending. The separate mixed coordinator composes explicit complete file and contribution sets; current commands and schema-1 persistence are unchanged.

Per-client output paths

Item kindClaude CodeGitHub Copilotopencode
Rule.claude/rules/<name>.md.github/instructions/<name>.instructions.md.agents/rules/<name>/RULE.md
Skill.claude/skills/<name>/.github/skills/<name>/.agents/skills/<name>/
Agent.claude/agents/<name>.md.github/agents/<name>.agent.md.opencode/agents/<name>.md

All output is copy (not symlink) — Windows portability without Developer Mode.

Exit codes

CodeMeaning
0Success
1General error
2Usage error (bad args)
130Interrupted (Ctrl+C)

The opt-in combined-state validator now checks reference authority and explicit per-owner consumer associations. Reference-driven removal planning now connects this state to shared-owner retirement. The inactive single-owner upsert coordinator connects complete add/update sets to graph and mixed-artifact planning. The separate inactive plan_shared_upsert API accepts explicit matching owner contracts for shared outputs. An opt-in guarded application boundary provides backup and executor recovery for supplied plans. Production command behavior is unchanged.

Recipes

CI usage

# Install without prompts (auto-detects NO_COLOR, non-TTY)
upskill add owner/repo

# Lint in CI (fail on warnings)
upskill lint --strict

In a non-TTY environment, upskill skips interactive prompts and disables coloured output when NO_COLOR is set.

Private repositories

upskill never injects credentials into clone URLs — it clones the bare https://<host>/... URL and relies entirely on git’s own configuration. A private source works whenever a manual git clone <url> would; set up whichever git mechanism you already use:

# Credential helper (stores the token in your OS keychain)
git config --global credential.helper osxkeychain   # macOS

# Or rewrite HTTPS → SSH for a host you have keys for
git config --global url."git@github.com:".insteadOf "https://github.com/"

gh auth setup-git and glab auth login both configure a git credential helper, which upskill then picks up transparently.

Any https git host works via the full URL form (https://git.mycompany.com/team/repo), including projects nested under groups to any depth (https://git.mycompany.com/group/subgroup/team/repo).

Pin a source to a specific version

upskill add owner/repo@v1.2.0

The pinned ref is recorded in .upskill-lock.json. upskill update re-fetches from the same ref unless you bump it explicitly.

Install a curated bundle

upskill add owner/bundles:platform-baseline.bundle.yaml

A bundle is a YAML manifest (*.bundle.yaml) that names the items it includes plus any other bundles it depends on. The dependency closure is resolved transitively before any items are written.

The lockfile records the bundle entry alongside the items, so you can later remove everything that came from it:

upskill remove --source github:owner/bundles:platform-baseline.bundle.yaml

Bisect drift

If a teammate’s .upskill-lock.json shows different content than yours after pulling main:

upskill doctor              # see which bucket the drift is in
upskill update --dry-run    # see which sources would change
upskill update              # apply

update is always-fetch and idempotent; running it twice produces no diffs the second time.

Author workflow

# Inside a source-registry repo, under the recommended skills/ root
# (see docs/conventions.md — items and *.bundle.yaml both live here).
cd skills/
upskill new skill my-skill
$EDITOR my-skill/SKILL.md

# Validate as you go (run from the registry root)
cd ..
upskill lint
upskill fmt

# Pre-publish guard rail
upskill lint --strict

upskill fmt is idempotent — files already in canonical form aren’t rewritten, so you can run it on every commit hook without churn.

Shipping an MCP that needs a local install

A bundle’s mcps: map configures an MCP server into the consumer’s client (see format-spec §3.7 and ADR-0010). For most servers that is all you need. This recipe covers the one case that needs more: a local server that is a bare binary with no self-fetching launcher, so the binary must be installed on the consumer machine.

First — do you even need an installer?

  • Remote server (remote: { type, url }) → no installer; it is already running.
  • Local server launched by npx -y <pkg>, uvx <pkg>, or docker run … → no installer; the launcher fetches and caches the server on first run.
  • Local server that is a bare binary (a compiled tool, a Homebrew formula) → continue below.

The pattern. upskill has no way to install the binary itself — it is a CLI, not an agent. Instead, ship the install knowledge as content and let the consumer’s AI agent run it:

  1. Author a per-MCP installer agent. Scaffold one with upskill new agent <name>-mcp-installer. Its body is the install recipe for that one server — detect the OS, check the version, install via the right channel (brew install …, download + chmod, cargo install …), run any one-time setup. Give it the Bash tool, and state in its body that it must get the user’s approval before running install commands.

  2. Add a trigger to the skill that uses the MCP. Near the top of the skill body, add one line of plain prose:

    If the <name> MCP server is not responding (a tool call fails because the binary is missing), dispatch the <name>-mcp-installer agent to install it, then retry.

  3. Bundle the three together so one install brings them all:

    # <name>-mcp.bundle.yaml
    schema: 1
    name: <name>-mcp
    description: <name> MCP server, its skill, and its local-install agent
    items:
      skills: [<name>-diagrams]
      agents: [<name>-mcp-installer]
    mcps:
      <name>:
        local: { command: <binary>, args: [...] }
    
upskill add owner/registry:<name>-mcp.bundle.yaml --claude

The --claude flag restricts this install to Claude Code (see client selection); drop it to target every client. This installs the skill, installs the <name>-mcp-installer agent into the client’s agents directory, and configures the MCP — in one step.

At runtime: the consumer uses the skill, the model reads the trigger line, and if the MCP tool is missing it dispatches <name>-mcp-installer — which installs the binary under the client’s own “allow this command?” prompts — then retries. upskill is never in this loop after add: it configures the server and ships the installer agent as content, but the client’s agent does the installing, gated by the client’s permission prompts.

Upskill conventions

The portable format is intentionally tool-agnostic: it defines what any conforming consumer must accept, but leaves many choices open (which path holds items, where bundles live, etc.). This page records the specific conventions the upskill project itself follows when authoring and consuming registries, so that:

  • upskill new <kind> <name> drops items directly where they belong.
  • Examples in this book, in fixtures, and in the driftsys/skills source registry all match what you see on disk.
  • Readers cloning a registry know exactly where to look without scanning frontmatter.

These conventions are non-normative: a registry that deviates is still conforming, and the upskill CLI MUST accept it. They exist to keep upskill-aligned registries legible to each other and to give new authors a default that just works.

Conventional layout

The root folder of an upskill source registry is skills/. Items and bundle files sit together under it, flat:

<source-registry-root>/
└── skills/
    ├── platform-baseline.bundle.yaml     # bundles
    ├── android.bundle.yaml
    ├── license-awareness/               # rule item
    │   └── RULE.md
    ├── code-review/                     # skill item
    │   └── SKILL.md
    ├── security-reviewer/               # agent item
    │   └── AGENT.md
    └── api-handler/                     # co-located rule + skill
        ├── RULE.md
        └── SKILL.md

Why this shape:

  • Single discovery root. Anyone cloning the registry sees SSOT content in one place; no frontmatter scan needed to find items.
  • Flat bundle placement. Bundles sit next to the items they reference, so requires: and items: lists are visually adjacent to their targets.
  • Matches upskill new. Running upskill new <kind> <name> from inside skills/ produces skills/<name>/<KIND>.md with no follow-up moves.
  • Holds all three kinds. Despite the directory name, skills/ carries rules and agents as well. Kind is determined by the entrypoint filename (format-spec §2.1), not by the parent directory.

When bundles outgrow the flat layout

Once a registry holds enough bundles that *.bundle.yaml files start to drown out the item directories in a single ls, move the bundles into a sibling bundles/ subdirectory:

<source-registry-root>/
└── skills/
    ├── bundles/
    │   ├── platform-baseline.bundle.yaml
    │   ├── android.bundle.yaml
    │   └── rust-embedded.bundle.yaml
    ├── license-awareness/
    │   └── RULE.md
    ├── code-review/
    │   └── SKILL.md
    └── security-reviewer/
        └── AGENT.md

This trades visual adjacency between a bundle and the items it lists for a clean separation between manifests and content. Rough heuristic: stick with the flat layout until scanning skills/ for an item becomes harder than scanning it for a bundle.

upskill discovers bundles by scanning for the .bundle.yaml suffix (format-spec §2.2) and is indifferent to which of these two sub-layouts the registry uses.

Item naming

Item names are namespace-prefixed so they stay globally unique within each per-client flat directory (.claude/skills/<name>/, .claude/rules/<name>.md, etc.). Two items with the same (kind, name) would otherwise collide on disk when co-installed from different sources.

Default to repo/org scope (<org>-<descriptor>); switch to bundle scope (<bundle>-<descriptor>) when one org ships multiple bundles that could reuse generic descriptors. This repo uses the upskill- prefix — e.g. upskill-writing-rules, upskill-prompt-design. See the upskill-writing-bundles skill for the full decision rule.

Scaffolding under the convention

mkdir -p skills && cd skills
upskill new skill code-review     # → skills/code-review/SKILL.md
upskill new rule license-checks   # → skills/license-checks/RULE.md
upskill new agent security-review # → skills/security-review/AGENT.md

Bundles are plain *.bundle.yaml files you create alongside the items — there is no scaffolder for them; copy the format-spec §3.7 example as a starting point.

What these conventions do not change

  • Portable format conformance. <item-root> and <bundle-root> are MAY-level in the format spec. A registry that uses content/ or skills-src/ instead of skills/ is still conforming.
  • Generation output. Consumer-side paths (.claude/skills/<name>/..., .github/skills/..., .agents/skills/...) are unrelated to source-side layout. They are specified in format-spec §7.
  • The .agents/ reservation. Source registries SHOULD NOT use .agents/ as their item root, to prevent confusion with the consumer-side opencode canonical-store path (format-spec §7.3). This applies whether the registry follows the upskill conventional layout or any other.

upskill — Specification

Upskill your coding agents.

Production-architecture direction is specified by the customization delivery contract. Independent Codex/OpenCode rules use a selected native or hook route beside unchanged team AGENTS.md. The native-first amendment selects a trusted Codex hook with its declared recorded-history limitation and permits explicit OpenCode paths only after Q5 PASS; it does not mandate a plugin where a native route works. Experimental contribution APIs and generated-file tests do not activate that route or schema 2. Current released command behavior below remains separate from those planned gates (AD-0040 amendment).

1. Overview

upskill is a single-binary CLI that lets authors create, manage, and prolong AI-assistance content — rules, skills, agents — across multiple AI coding clients (Claude Code, GitHub Copilot, opencode) from a single source of truth.

The central abstraction is generation: the SSOT (Single Source of Truth) on disk is portable; per-client output is produced on demand by a generation pipeline.

1.1 Three item kinds

KindPurposeEntrypoint file
RuleAlways-on behavioural guidance.RULE.md
SkillOn-demand procedural content.SKILL.md
AgentPersona definition with tool scope.AGENT.md

The on-disk contract is defined by the portable format spec. This document describes upskill’s behaviour against that contract.

1.2 Two scopes

ScopeLives atUse
Project<cwd>/.agents/Content shared across the team via git.
Global$HOME/.agents/Content available to a single user, all repos.

1.3 Two roles

  • Source registry — repository where SSOT items are authored. Holds the canonical RULE.md / SKILL.md / AGENT.md files and *.bundle.yaml manifests. Author commands (new, lint, fmt) operate here.
  • Consumer project — repository where generated outputs are installed for use by AI clients. Holds only generated per-client files. Consumer commands (add, remove, update, list, doctor, search) operate here.

The same repository can be both, but SSOT and generated outputs do not mix in the same tree.

2. CLI surface

No overlap between verbs.

CommandRolePurpose
addConsumerInstall items or bundles from any source.
removeConsumerRemove installed content.
updateConsumerPull latest, regenerate changed items.
listConsumerShow installed content from the lock file.
doctorConsumerVerify installation consistency.
searchConsumerLook up skills via the public registry.
indexConsumerBuild or manage the local registry index cache.
newAuthorScaffold a new rule, skill, or agent.
lintAuthorValidate SSOT files against the format spec.
fmtAuthorCanonicalise YAML frontmatter (key order, quoting).

2.1 add

upskill add <source> [items...] [--global|--project]

<source> accepts (parity with npx skills add):

  • owner/repo — GitHub shorthand
  • owner/repo@ref — pinned ref (branch, tag, or commit SHA)
  • owner/repo:path/to/item — subfolder
  • owner/repo:path/to/name.bundle.yaml — bundle file (resolves transitively, see §2.6)
  • owner/repo@ref:path — combined
  • https://github.com/owner/repo[...] — full HTTPS URL
  • https://<host>/<path>[...] — any other https git host: GitLab (including self-hosted instances and subgroups at any nesting depth), Bitbucket, Gitea, or a plain git server
  • ./path, ../path, /abs/path, ~/path — local paths

upskill add <source> installs everything the source contains. Optional items... after the source filter to a subset. There is no interactive picker; install-all-by-default fits bundle curation.

Default scope is --project (write into the current repo’s .agents/...), falling back to --global ($HOME/.agents/...) when the current directory is not inside a git repo. Either flag can be passed explicitly.

2.2 update

upskill update [name...] [--global|--project] [--dry-run]

Always fetches the latest source before regenerating. Absorbs the role of a separate sync command — there is no --offline flag in v1; offline use is covered by passing local paths to add / update.

update covers two distinct behaviours:

  • Absorb client drift. When Copilot or Claude Code change a path or frontmatter field, update regenerates outputs from the same SSOT.
  • Track upstream evolution. update git-pulls source repos and regenerates items whose source hash changed.

Persisted schema-1 gitlab: and gitlab+<host>: source labels remain readable for updates. Unambiguous historical labels decode to generic HTTPS sources, preserving namespace, ref and subfolder. Applying update canonicalizes the source labels of reinstalled items and bundles, even when their content is unchanged. update --dry-run, list and doctor do not rewrite lockfiles or generated output. Aliases, recorded selection, bundle membership and unrelated item/plugin/MCP records remain intact. This does not introduce schema 2 or infer artifact ownership.

The supported historical authority grammar and reconciliation limits are defined in the ADR-0011 amendment. Unsupported or ambiguous labels fail before fetch with the recorded label and guidance to reconcile a verified HTTPS URL, ref and subfolder. Modern add still rejects the removed GitLab shorthands with exit 2; use a full HTTPS URL.

2.3 remove

upskill remove [name...] [--source <label>] [--global]

Removes installed items from the matching scope. Either name one or more items, or pass --source <label> to remove every item that came from a single source. Bare upskill remove is rejected — be explicit. Ancillary files (CLAUDE.md, opencode.json, .vscode/settings.json) are not touched.

2.4 list / doctor

  • list — show installed content from .upskill-lock.json, grouped by kind. Bundles are surfaced in a separate section. The --available view (items discoverable from configured sources) is deferred for a future release.
  • doctor — verify on-disk state matches the lock file. Five independent buckets: missing per-client outputs (reinstall fixes), SSOT hash drift on local: sources (update fixes), lockfile entries with no recoverable source (remove fixes), installed plugins missing from the client (update reinstalls), and skipped plugins (informational — CLI was absent at install time). Exit 0 when clean, 1 when any drift bucket is non-empty. Skipped plugins are warnings and do not affect the exit code.

2.5 new / lint / fmt

Author commands. Run inside a source-registry working tree.

  • new <kind> <name> — scaffold a new SSOT item directory (<kind> is one of rule, skill, agent).
  • lint [paths...] [--strict] — validate SSOT files. --strict is the CI mode (warnings become errors).
  • fmt [paths...] — canonicalise YAML frontmatter only. Markdown body formatting is dprint’s responsibility.

2.6 Bundles

A bundle is a *.bundle.yaml manifest that names a curated set of items and (optionally) other bundles to install together. Bundles contain no content of their own; they reference items by name. Frontmatter shape is defined in format-spec §3.7.

upskill add <source>:path/to/foo.bundle.yaml installs a bundle. Implementation behaviour:

  • Items. Every entry in items.rules, items.skills, items.agents is resolved against the same source repository and installed. Unresolved names error out before any write.
  • Transitive requires:. When the bundle declares requires:, every bundle in the transitive closure is installed. A bare { name } entry is resolved against the same source; a { name, source } entry is resolved cross-source (source reuses the upskill add DSL), and the closure MAY span sources. Each bundle and item is recorded in the lockfile under its own source. Circular requires: is rejected; the same bundle name resolving to two different sources within one closure is an error.
  • Lockfile. Each installed bundle is recorded as a top-level bundles[] entry in .upskill-lock.json alongside its resolved items. The same item may appear in multiple bundle items[] lists; it is installed once.
  • Removal. upskill remove --source <label> removes every item that came from that source, including those introduced via bundles. A bundle-name-targeted removal (remove <bundle-name> removing only items exclusive to that bundle) is not yet implemented.

2.7 Aliases

add does not alias to install. remove does not alias to uninstall. The unified verb names are canonical.

3. Generation pipeline

SSOT → per-client output runs on the developer’s machine, on every install or update. No CI generation step. No committed dist/ artifacts.

3.1 Per-client output paths

Item kindClaude CodeGitHub Copilotopencode
Rule.claude/rules/<name>.md with paths:.github/instructions/<name>.instructions.md with applyTo:.agents/rules/<name>/RULE.md (canonical-store)
Skill.claude/skills/<name>/SKILL.md.github/skills/<name>/SKILL.md.agents/skills/<name>/SKILL.md (canonical-store)
Agent.claude/agents/<name>.md.github/agents/<name>.agent.md.opencode/agents/<name>.md

Per-client frontmatter mapping is defined in format-spec §7 and Appendix B.

All installation is file copy or generated output. No symlinks anywhere. One code path; Windows portability without Developer Mode or core.symlinks=true.

3.3 Markdown formatting

Generated output passes through embedded dprint-plugin-markdown (exact-pinned at =0.21.1) before writing. This guarantees idempotence under the same formatter in dprint.json. Authors who run dprint locally see no diffs on generated files.

3.4 Ancillary file handling

Three files outside the per-item path tree are managed idempotently:

  • CLAUDE.md at repo root — created once with @AGENTS.md content if absent. Never overwritten.
  • .vscode/settings.json — chat.instructionsFilesLocations is set; other keys preserved.
  • opencode.json: checked on rule-bearing installs. The entry ".agents/rules/**/RULE.md" is added to instructions[] when absent, including when a later install finds that it was removed. An existing entry leaves the file unchanged; unrelated entries and keys are preserved (implementation, call site). The installer relies on client glob expansion; this is a shipped write contract, not qualification of OpenCode 2.1.4 runtime behavior (retained retry).

Planned ancillary transition (2026-10-24 decision, 2026-10-25 reconciliation): the behavior above remains shipped behavior. The separate step-8 code PR removes bridge creation and the OpenCode glob write only after Q5 explicit-path PASS. Existing generated entries are to be reported by doctor, never automatically deleted; that report is required future behavior, not a current capability. VS Code’s write remains pending its deferred profile decision. No docs-only adoption changes these commands (AD-0040, Q5 deferred retry, handoff step 8).

4. State files

.upskill-lock.json records what was installed, from where, at what resolved git ref, with what content hashes. Same schema (schema: 1), two possible locations:

ScopeLocationCommitted?
Project<cwd>/.upskill-lock.jsonYes
Global$HOME/.upskill-lock.jsonNo

The lockfile plays the same role as package-lock.json: deterministic regeneration on another developer’s machine and in CI for project scope, and across-repo continuity for global scope.

{
  "schema": 1,
  "items": [
    {
      "kind": "skill",
      "name": "code-review",
      "source": "github:driftsys/skills",
      "ref": "v1.2.0",
      "hash": "sha256:..."
    }
  ],
  "bundles": [
    {
      "name": "platform-defaults",
      "source": "github:driftsys/bundles",
      "ref": "v0.4.0",
      "items": ["code-review", "secret-scanner"]
    }
  ],
  "plugins": []
}

4.1 Pure legacy-state projection

See Pure legacy-state projection for the canonical requirements and architecture.

4.2 Pure owned instruction planning

See Pure owned instruction planning for the canonical requirements and architecture.

4.3 Strict captured source manifests

See Strict captured source manifests for the canonical requirements and architecture.

4.4 Owned whole-file operation plans

See Owned whole-file operation plans for the canonical requirements and architecture.

4.5 Captured item compilation

See Captured item compilation for the canonical requirements and architecture.

4.6 Mixed owned-artifact operation plans

See Mixed owned-artifact operation plans for the canonical requirements and architecture.

4.7 Qualified scope roots and advisory writer locking

See Qualified scope roots and advisory writer locking for the canonical requirements and architecture.

4.8 Qualified destination observations

See Qualified destination observations for the canonical requirements and architecture.

4.9 Qualified source tree observations

See Qualified source tree observations for the canonical requirements and architecture.

4.10 Explicit file permission selection

See Explicit file permission selection for the canonical requirements and architecture.

4.11 Ordered recovery checkpoint model

See Ordered recovery checkpoint model for the canonical requirements and architecture.

4.12 Qualified file execution and recovery

See Qualified file execution and recovery for the canonical requirements and architecture.

4.13 Combined lifecycle state foundation

See Combined lifecycle state foundation for the canonical requirements and architecture.

4.14 Captured rule desired sets

See Captured rule desired sets for the canonical requirements and architecture.

4.15 Authoritative lifecycle reference reconciliation

See Authoritative lifecycle reference reconciliation for the canonical requirements and architecture.

4.16 Reference-driven shared-owner removal planning

See Reference-driven shared-owner removal planning for the canonical requirements and architecture.

4.17 Captured lifecycle selection preflight

See Captured lifecycle selection preflight for the canonical requirements and architecture.

4.18 Complete single-owner lifecycle upsert planning

See Complete single-owner lifecycle upsert planning for the canonical requirements and architecture.

4.19 Per-owner recorded contract evidence

See Per-owner recorded contract evidence for the canonical requirements and architecture.

4.20 Explicit shared-output reconciliation

See Explicit shared-output reconciliation for the canonical requirements and architecture.

4.21 Complete selected-set preflight reporting

See Complete selected-set preflight reporting for the opt-in library diagnostic contract and its production activation limits.

5. Source format and authentication

Source format: see §2.1.

upskill never injects credentials into clone URLs. Clones use the bare https://<host>/... URL and rely entirely on git’s own configuration — credential helpers, url.<base>.insteadOf rewrites, and SSH. A private repository works whenever a manual git clone <url> would; configure git the same way. upskill resolves no tokens and reads no token env vars.

Any https host is treated as an opaque git remote (https://git.mycompany.com/team/repo), including projects nested under groups to any depth (https://git.mycompany.com/group/subgroup/team/repo).

6. CLI compliance

6.1 Exit codes

CodeMeaning
0Success.
1General error (network, parse, I/O).
2Usage error (invalid args or flags).
130Interrupted (Ctrl+C / SIGINT).

6.2 Conventions

  • POSIX/GNU long flags (--flag-name) and short flags (-f).
  • Stdout for data, stderr for progress and errors.
  • Honors NO_COLOR and TTY detection (no color in pipes/CI).
  • Global -q / --quiet suppresses informational stdout. Errors on stderr and exit codes are unaffected.
  • Single static binary, no runtime dependencies.

6.3 Environment variables

VariablePurpose
NO_COLORDisable colored output.
HTTPS_PROXYHTTP proxy for network requests.
XDG_CACHE_HOMEOverride cache directory for registry indexes.

7. Out of scope

  • Runtime invocation of installed content (each AI client’s responsibility).
  • Hosting a registry server or central marketplace.
  • Content quality review (delegated to AI workflows outside upskill).
  • Telemetry, analytics, or usage tracking.
  • Publishing to non-Git destinations (npm, crates.io, PyPI).

8. References

Edit the canonical source.

Read the contracts and architecture by concern.

Portable Format for AI-Assistance Content

Version: 0.6.0 Status: Draft Date: 2026-05-13


Abstract

This specification defines a portable, tool-agnostic format for authoring rules, skills, and agents targeting AI coding assistants. It also defines a bundle manifest format for distributing curated sets of these items. The format is inspired by and compatible with the Agent Skills open standard (agentskills.io) and extends it to cover always-on rules and agent definitions, which the open standard does not address.

The specification intentionally leaves implementation details (installation mechanics, caching, registry protocols, CLI surface) to the implementing tool. The scope is the on-disk content contract: file layout, frontmatter schemas, body conventions, conditional directives, and per-client override mechanics.

Audience

Authors of AI-assistance content (rules, skills, agents) targeting multiple AI coding clients from a single source. Implementers of tools that consume this format to generate client-specific outputs. Maintainers of CI pipelines that validate content against this specification.

Conformance keywords

The key words MUST, MUST NOT, SHOULD, SHOULD NOT, and MAY in this document are to be interpreted as described in RFC 2119 when, and only when, they appear in all uppercase as shown.

Client-name conventions

This document uses Claude Code, GitHub Copilot, Codex and opencode in prose. The lowercase source family identifiers claude, copilot, codex and opencode appear in audience lists (§3.1) and conditional content directives (§6). Historical passthrough blocks (§3.5) use claude, copilot and opencode; the restricted Codex instruction producer rejects native extension maps.


1. Terminology

Item: a rule, skill, or agent. The three kinds share a base schema and differ in semantic role and a small number of kind-specific fields.

Rule: always-on behavioral guidance, loaded into every session of compatible clients. Rules constrain behavior (“never do X”, “always check Y”).

Skill: on-demand procedural content, activated by name or by a client’s automatic discovery mechanism. Skills teach a procedure (“to accomplish X, follow these steps”).

Agent: a persona definition with its own system prompt and tool scope, invocable as a subagent or primary agent depending on the target client. Agents establish identity and role (“you are a security reviewer; when invoked, do X”).

Bundle: a manifest file grouping items (and optionally other bundles) by reference for distribution. Bundles contain no content themselves.

Client: a target AI coding assistant. This specification recognizes three initial clients: Claude Code, GitHub Copilot (VS Code + CLI + cloud agent), and opencode. Implementations MAY extend this list.

SSOT: single source of truth. The canonical form of an item, authored per this specification, from which client-specific outputs are derived.

Client-specific output: a file or configuration entry written for a particular client’s expected location, layout, and frontmatter conventions. Generated from the SSOT by an implementing tool.

Source registry: a git repository (or local path used as one) that holds SSOT items in their canonical form. Source registries are the only place SSOT items exist. Authors edit items in source registries; consumers fetch from them.

Consumer project: a developer’s working repository where they run upskill add (or equivalent) to install items. Consumer projects only ever contain generated, client-specific outputs — never SSOT items. The same applies to a consumer’s global install location (~/.agents/ and per-client directories under ~/.config/, ~/.claude/, etc.): generated content only.


2. File layout

2.1 Item directory structure

Each item is a directory containing at minimum one entrypoint file. Kind is determined by the entrypoint filename:

<item-root>/
├── <name-a>/
│   └── RULE.md
├── <name-b>/
│   └── SKILL.md
├── <name-c>/
│   └── AGENT.md
└── <name-d>/                # optional co-location: multiple kinds in one item directory
    ├── SKILL.md             # name omitted or name: <name-d> (skill must match the directory)
    └── RULE.md              # name: <name-d> or a divergent name (rule/agent name MAY differ)

Constraints:

  • An item directory MUST contain at least one entrypoint file (RULE.md, SKILL.md, or AGENT.md).
  • An item directory MAY contain more than one entrypoint when the kinds represent a tightly-coupled set for one capability (for example, a skill paired with its enforcing rule, or an agent paired with a skill it preloads). Co-location is optional; solo-kind item directories are the common case. Co-location expresses a symmetric, inseparable unit; a directed “A needs B” relationship is expressed with requires (§3.7) instead. See ADR-0009.
  • Effective name and relaxed naming. Each entrypoint resolves to an effective name:
    • A skill name: is optional. When present it MUST equal the directory name (the Agent Skills standard mandate). When absent, the skill’s effective name is the directory name.
    • A rule or agent name: comes from frontmatter and MAY differ from the directory name. When absent, the effective name is the directory name. A divergent rule/agent name is legal and is not a diagnostic.
  • Identity is (kind, effective-name) — layout-independent, never a path. A directory MAY hold independently-named kinds (for example, a skill named after the directory alongside a rule whose name diverges). The directory name is the co-location grouping key and the fallback default for any entrypoint that omits name:.
  • The effective name MUST consist of lowercase letters (a-z), digits (0-9), and hyphens (-).
  • The effective name MUST NOT start or end with a hyphen.
  • The effective name MUST be at most 64 characters.
  • Kind is determined by the entrypoint filename: RULE.md for a rule, SKILL.md for a skill, AGENT.md for an agent. Implementations MUST NOT infer kind from any other source (parent directory name, frontmatter field, etc.).
  • <item-root> is the SSOT path within a source registry repository. SSOT items live only in source registries; consumer projects (where developers run upskill add) only ever hold generated, client-specific outputs and never SSOT items.
  • Within a source registry, <item-root> MAY be any path that fits the team’s organisation (content/, skills-src/, skills/, etc.). The upskill project recommends skills/; see Conventions for the full recommended layout. Source registries SHOULD avoid .agents/ as their <item-root> to prevent confusion with the consumer-side opencode canonical-store path (§7.3).

Agent Skills standard compatibility. A skill item directory <item-root>/<name>/SKILL.md is a valid Agent Skills directory per agentskills.io: the standard requires <dir>/SKILL.md with name matching the parent directory and permits “any additional files or directories” in the skill directory. A sibling RULE.md or AGENT.md is therefore standard-compliant additional content and is ignored by agentskills.io-only tooling.

2.2 Bundle files

Bundles are flat YAML manifest files, not directories:

<bundle-root>/
├── platform-baseline.bundle.yaml
├── android.bundle.yaml
└── rust-embedded.bundle.yaml
  • A bundle manifest is a pure YAML file (no --- delimiters, no markdown body).
  • The filename stem (before .bundle.yaml) MUST match the name field in the manifest.
  • Bundle files MAY live anywhere within a source registry — alongside item directories under <item-root> (§2.1), in a sibling directory, or in a dedicated bundles/ directory. Implementations discover bundles by scanning for the .bundle.yaml suffix and MUST NOT depend on a specific bundle-root path. A YAML file matching the suffix but lacking a top-level integer schema: key is silently skipped in discovery (the schema field is the bundle contract gate); explicit-path operations against such a file MUST surface a parse error. The upskill project recommends placing bundles alongside item directories under skills/; see Conventions.
  • A bundle MAY have an optional sibling Markdown file <name>.bundle.md carrying human-readable documentation (install examples, adoption path, caveats). Implementations ignore <name>.bundle.md; it exists for humans browsing the registry. See ADR-0007.

2.3 Per-client override files

For the three historical generation clients (claude, copilot, opencode), any entrypoint MAY have optional per-client body overrides alongside it in the same item directory:

<name>/
├── RULE.md                    # canonical, always required
├── RULE.claude.md             # optional — Claude Code body override
├── RULE.copilot.md            # optional — GitHub Copilot body override
└── RULE.opencode.md           # optional — opencode body override

The same pattern applies to skills (SKILL.claude.md, etc.) and agents (AGENT.claude.md, etc.). When an item directory holds multiple kinds (§2.1), each kind has its own set of override files keyed by entrypoint name (RULE.claude.md, SKILL.claude.md, AGENT.claude.md); they coexist in the same directory without ambiguity.

Override file rules:

  • Override files contain body content only; they MUST NOT include YAML frontmatter.
  • Frontmatter is always taken from the canonical entrypoint.
  • The <client> suffix MUST be one of claude, copilot or opencode. The four audience/directive family identifiers in §3.1 and §6.1 do not extend this override vocabulary. codex is not a supported override suffix; the bounded Codex instruction producer rejects rule overrides.

Current implementation status. The resolution order below is the specified format contract, not current installer behavior. The historical installer renders the canonical body and does not apply override bodies. The captured compiler rejects an active matching override, including one hidden by ignore; it does not fall back to canonical content for that requested client. See AD-0025 and debt #324. Override execution requires a separate implementation and compatibility tests.

Specified body-resolution order for a given client. For historical generation client C (claude, copilot or opencode), a conforming override implementation would resolve the body as follows:

  1. If <KIND>.<C>.md exists, use its body verbatim. Directives in the canonical body are NOT processed for client C.
  2. Otherwise, use the canonical body with <!-- @client: --> directives (§6) processed for client C.

An override replaces the whole canonical body; it does not append to or compose with it. §6.4 cross-references this specified rule and its implementation status; this section is the authoritative statement.

Sharing a procedure across client-specific orchestration

For small differences, keep the common procedure in SKILL.md and surround only the client-specific orchestration instructions with inline client directives (§6). These directives are processed during upskill generation; a native client reading the source directory directly does not perform that preprocessing.

For large differences, a candidate authoring pattern puts the common procedure in a supporting resource and gives each client wrapper an explicit instruction to read it before using that client’s agent mechanism:

review/
├── SKILL.md
├── SKILL.claude.md
├── SKILL.copilot.md
└── references/
    ├── review-procedure.md
    ├── code-reviewer.md
    └── test-reviewer.md

For example, each wrapper can say: “Read references/review-procedure.md and follow its review criteria. Launch two reviewers using this client’s agent mechanism.” The example illustrates the specified override format; the current installer/compiler boundaries above still apply. The canonical body must also be complete for clients without an override. There is no SKILL.codex.md override suffix.

This shares the procedure through runtime file loading. Supporting resources are copied alongside each generated entrypoint (§2.4); their contents are not embedded into the body or processed for client directives. Embedding a common core would require a separately specified composition/include feature. Neither an override nor a resource reference supplies that feature.

Shared authoring is distinct from shared native discovery. Keep this variant family outside scanned shared catalogs; expose each logical skill once per client through a qualified route. A directly discovered .agents/skills entry must already be consumption-ready. Runtime resource loading must be qualified for relative paths, permissions, missing files and package relocation. A read instruction does not register subagents, grant tool permissions or configure MCP servers. See the delivery contract.

2.4 Supporting resources

Items MAY contain supporting files in their directory:

<name>/
├── SKILL.md
├── templates/
│   └── handler.ts.tmpl
├── scripts/
│   └── validate.sh
└── references/
    └── patterns.md
  • Supporting files are referenced from the entrypoint body using standard markdown relative links.
  • Implementations MUST preserve supporting files and their directory structure during generation (copy them alongside the generated entrypoint into the client-expected location).
  • An item MAY declare an ignore list (§3.1) to scope which supporting files are copied. The list is .gitignore-style and subtractive only — there is no include/allowlist form. A file matching any ignore pattern is not copied into the client-expected location. ignore is author-declared and is stripped from generated output.
  • When an item directory holds multiple kinds (§2.1), supporting resources are shared: any entrypoint in the directory MAY reference them via relative links, and implementations MUST copy them alongside each generated entrypoint that references them.

3. Frontmatter schema

Item entrypoint files (RULE.md, SKILL.md, AGENT.md) begin with YAML frontmatter delimited by --- markers, followed by a blank line and then markdown body content. Bundle manifests (*.bundle.yaml, §2.2) are pure YAML — the same schema, but the file content IS the YAML, with no --- wrapping and no body.

3.1 Common fields (all kinds and bundles)

FieldTypeRequiredDescription
schemaintegerYESProtocol version of this specification. Current: 1.
namestringvariesStable identifier. Required for bundles; optional for items (effective name defaults to the directory name). See §2.1.
descriptionstringYESWhat this item does and when to use it. Max 1024 characters. Skill-specific guidance: see §3.3.
licensestringnoSPDX identifier or custom (proprietary, LicenseRef-*).
audiencestring[]noTarget families (claude, copilot, codex, opencode). When present, output is limited to listed families and supported selected surfaces. When absent, all supported selected families are targeted.
metadatamapnoFree-form governance block. See §3.6.
requiresmapnoDirected item dependencies (rules/skills/agents). See §3.7. Stripped from output.
ignorestring[]no.gitignore-style subtractive copy-scope (§2.4). Stripped from output.

Example:

---
schema: 1
name: license-awareness
description: Use when reviewing third-party code, AI suggestions, or external snippets to check licensing compatibility and avoid incorporating incompatible licensed code
license: proprietary
metadata:
  version: "1.0.0"
  author: platform-security
---

Field semantics:

  • schema: implementations MUST reject files with schema greater than the highest version they support and SHOULD report a clear upgrade message. See §8 for evolution rules.
  • name: the effective name (§2.1) is used as the /slash-command name in clients that support it and as the identifier in bundle manifests. When name is omitted on an item, the directory name is the effective name; a rule or agent name MAY differ from its directory.
  • description: for skills, this is the activation hint — the model reads it during discovery and decides whether to load the full body. For rules and agents, it serves as documentation. Front-load the key use case. Convention: start with “Use when…” for skills and agents.
  • license: if the item might be shared outside the authoring organization, SHOULD be populated.
  • audience: the canonical mechanism for restricting an item to a subset of clients. Conditional body directives (§6) limit body content per client; passthrough blocks (§3.5) emit client-specific frontmatter; audience is the only mechanism that filters whether the item is generated at all. The three are complementary: use audience to scope the item, directives to scope body content within an emitted item, and passthrough blocks to add client-specific frontmatter to an emitted item.
  • metadata: see §3.6. Implementations MUST preserve unknown keys within metadata.
  • requires: directed prerequisite items. Each requires.<kind> entry is either a bare name (same source, resolved by (kind, name)) or a { name, source } map for a cross-source dependency, where source reuses the upskill add source DSL. See §3.7 for resolution, cycle, and conflict rules.
  • ignore: .gitignore-style subtractive copy-scope (§2.4). Both requires and ignore are SSOT-only authoring fields and are stripped from every client output (the same treatment as schema, metadata, and license).

3.2 Rule-specific fields

Rules MAY include path-scoping:

scope:
  paths:
    - "src/api/**/*.ts"
    - "src/handlers/**/*.ts"
FieldTypeRequiredDescription
scopemapnoScoping constraints.
scope.pathsstring[]noGlob patterns. If absent or empty, rule is always-on.

When scope.paths is present, implementations:

  • SHOULD emit the rule with path-scoping for clients that support it (Claude Code’s paths: array, GitHub Copilot’s applyTo: comma-joined string).
  • SHOULD emit the rule unconditionally for clients that lack path-scoping (opencode), accepting the token-cost trade-off.

3.3 Skill-specific fields

No additional required fields beyond §3.1.

Skill description length. For skills, description is the activation hint: discovering clients read it on every selection decision and the text contributes to runtime context cost. The §3.1 1024-character cap MUST be respected, but skill authors SHOULD aim for ~200 characters or fewer. This is tighter than rules and agents, where description serves only as documentation.

The Agent Skills open standard defines optional fields (allowed-tools, when_to_use, disable-model-invocation, context, agent, argument-hint, paths, etc.) which implementations MAY pass through to client outputs when generating for clients that support them. This specification does not redefine those fields; see agentskills.io/specification for their semantics.

Implementations SHOULD allow unknown top-level fields in skill frontmatter to enable pass-through of client-specific or standard-defined fields.

3.4 Agent-specific fields

mode: subagent
model: sonnet
tools:
  - read
  - grep
  - glob
  - bash
preload-skills:
  - security-baseline
FieldTypeRequiredDefaultDescription
modestringnosubagentprimary, subagent, or all. Primary consumer is opencode; see notes below.
modelstringnosonnetShort alias. Spec-guaranteed aliases listed below.
toolsstring[]noclient-supported §4Capability-level tool names. See §4. Default semantics defined below.
preload-skillsstring[]no(none)Skill names to load at agent startup. Primary consumer is Claude Code; see notes below.

mode is opencode-primary. opencode’s agent definition has a real top-level mode: field with subagent / primary / all semantics that change agent selection behavior. Claude Code derives mode from file location (.claude/agents/ is always subagent); GitHub Copilot has no equivalent concept. Implementations MUST emit mode: for opencode; they MAY omit it for other clients. Authors who need opencode-specific mode behavior with no analog elsewhere SHOULD prefer opencode.mode in the passthrough block (§3.5) over a top-level mode: field, to make the intent explicit.

preload-skills is Claude-Code-primary. Claude Code emits this list as skills: in agent frontmatter so the named skills are loaded at agent startup. GitHub Copilot and opencode have no equivalent frontmatter field. Implementations MUST emit skills: for Claude Code; for GitHub Copilot and opencode, implementations SHOULD render the preloaded skills as a prose ## Skills section appended to the agent body (no frontmatter field), so the agent is informed which skills it relies on.

preload-skills implies requires.skills (soft). Listing a skill in preload-skills implies requires.skills for skills present in the same source: those skills are auto-installed alongside the agent. A preloaded skill absent from the same source is treated as a runtime hint and is neither auto-installed nor an error (this differs from an explicit requires entry, which errors when missing).

model aliases. This specification guarantees the following short aliases:

AliasFamily
sonnetAnthropic Claude Sonnet (latest)
opusAnthropic Claude Opus (latest)
haikuAnthropic Claude Haiku (latest)

Implementations MUST resolve these aliases to a current client-appropriate model identifier. Implementations MAY accept additional aliases or pass through fully-qualified model identifiers (anthropic/claude-sonnet-4-6, openai/gpt-5, etc.); these are not guaranteed portable across implementations.

tools default. When tools: is absent, implementations MUST emit, for each target client, the set of §4 capabilities documented as supported by that client (the non-— cells in the §4 table). When tools: is present, only the listed capabilities are emitted, subject to the §4 generation rules (capability dropped with a warning when the target client has no mapping).

Implementations map these fields to each client’s agent definition format:

  • Claude Code: .claude/agents/<name>.md with tools: as capitalized names, model: as literal, skills: from preload-skills. mode is implicit by file location and not emitted.
  • GitHub Copilot: .github/agents/<name>.agent.md with tools: filtered per §4 mapping; mode omitted; preload-skills rendered as a prose ## Skills section in the body (no frontmatter field).
  • opencode: .opencode/agents/<name>.md with mode:, permission: map (per §7.3), and passthrough fields; preload-skills rendered as a prose ## Skills section in the body (no frontmatter field).

3.5 Client-specific passthrough blocks

Items MAY include top-level blocks named after specific clients for fields that have no neutral equivalent:

copilot:
  excludeAgent: code-review
opencode:
  temperature: 0.2
  color: "#8b5cf6"

Rules:

  • Implementations MUST merge these blocks into the client-specific output only for the matching client.
  • Implementations MUST NOT emit passthrough blocks for non-matching clients.
  • Implementations MUST NOT validate the contents of passthrough blocks (they are opaque to the spec).
  • Known passthrough block names: claude, copilot, opencode. Additional names follow the same pattern for new clients.

3.6 Metadata block

The metadata field is a free-form map for governance, versioning, and implementation-specific extensions.

Recommended conventions (not required by this specification):

KeyTypeConvention
versionstringSemver, quoted (e.g., "1.0.0"). MUST be quoted to avoid YAML float coercion.
authorstringTeam or individual identifier.
updatedstringISO 8601 date of last content change.

Audience targeting is a top-level field with spec-defined semantics; see audience in §3.1.

Implementations:

  • MUST preserve all keys within metadata during processing.
  • MUST NOT require any specific key within metadata beyond what the implementation itself needs.
  • SHOULD validate version as semver when present.

3.7 Bundle schema

Bundles are source-registry artifacts. They reference items by name; the items themselves live in source registries (potentially the same repo, potentially others). Consumer projects do not contain bundle files — an implementation records which bundles a consumer project has installed in its own state file (e.g. .upskill-lock.json for the upskill CLI).

<bundle-root>/platform-baseline.bundle.yaml:

schema: 1
name: platform-baseline
description: Baseline rules, skills, and agents for all repositories
license: proprietary

items:
  rules:
    - license-awareness
    - security-baseline
    - commit-style
  skills:
    - pr-summary
  agents:
    - security-reviewer

requires: []

plugins:
  superpowers:
    claude:
      source: anthropics/claude-plugins
      marketplace: claude-plugins
      plugin: superpowers

metadata:
  version: "1.2.0"
  author: platform-dx

An optional sibling <bundle-root>/platform-baseline.bundle.md MAY carry human-readable documentation; the parser ignores it (§2.2).

FieldTypeRequiredDescription
schemaintegerYESSame as §3.1.
namestringYESSame as §3.1. Matches filename stem.
descriptionstringYESSame as §3.1.
licensestringnoSame as §3.1.
itemsmapYESWhat this bundle provides.
items.rulesstring[]noRule names.
items.skillsstring[]noSkill names.
items.agentsstring[]noAgent names.
requiresarraynoBundle dependencies. See below.
pluginsmapnoClient-native plugins. See below.
mcpsmapnoMCP server descriptors. See below.
metadatamapnoSame as §3.6.

requires entries are maps. Each entry references another bundle by name, optionally pinned with a semver version constraint, and optionally located in another source with source (the upskill add source DSL — owner/repo@ref, https URLs, local paths). Absent source resolves the required bundle in the same source registry as the requiring bundle:

requires:
  - { name: "platform-baseline", version: "^1.0.0" } # same source
  - { name: "rust-baseline" } # same source, any version
  - { name: "markspec-core", source: "driftsys/markspec@v0.8.0" } # cross-source

The single-form (map-only) shape avoids the polymorphic-string-or-map alternative until that flexibility has a documented author need. source mirrors the item-level requires.<kind> cross-source locator (§3.7, ADR-0009).

Implementations:

  • MUST resolve items.* entries against configured item sources by matching the name field.
  • MUST report unresolved item names as errors.
  • MUST resolve requires transitively: installing a bundle implies installing every bundle in its transitive requires closure, then unioning the items contributed by all reached bundles. Item-level conflicts (the same item name resolving to different sources or versions) MUST be reported as errors.
  • MUST resolve a { name, source } entry by fetching that source (each distinct source fetched once per resolution) and locating the named bundle in it; the closure MAY span sources. Each bundle and each item is recorded in the lockfile under its own source. The same bundle name resolving to two different sources within one closure MUST be reported as an error.
  • MUST reject circular requires chains as errors (cycle detection keyed by source and name).

plugins map

The optional plugins map declares client-native plugins that accompany this bundle. Unlike rules, skills, and agents (which are SSOT content rendered per-client by the generation pipeline), plugins are installed via each client’s native CLI. See ADR-0008 for design rationale.

Each entry in the plugins map is keyed by an upskill-level plugin name (used in the lockfile, CLI output, and upskill remove). The value is a map of per-client install descriptors:

plugins:
  superpowers:
    claude:
      source: anthropics/claude-plugins
      marketplace: claude-plugins
      plugin: superpowers
      install_url: https://github.com/obra/superpowers#install
    copilot:
      source: obra/superpowers-marketplace
      marketplace: superpowers-marketplace
      plugin: superpowers
      install_url: https://github.com/obra/superpowers#install
    vscode:
      extension: anthropic.superpowers
      install_url: https://marketplace.visualstudio.com/items?itemName=anthropic.superpowers
    opencode:
      module: superpowers-opencode
      install_url: https://opencode.ai/plugins/superpowers

Per-client descriptor fields:

ClientFieldTypeRequiredDescription
claudesourcestringYESMarketplace source (passed to claude plugin marketplace add): owner/repo, URL, or path.
claudemarketplacestringYESMarketplace name for the install ref <plugin>@<marketplace>. Distinct from source (see below).
claudepluginstringYESPlugin identifier (passed to claude plugin install).
claudeinstall_urlstringnoURL shown in warn-skip message when CLI not found.
copilotsourcestringYESMarketplace source (passed to copilot plugin marketplace add).
copilotmarketplacestringYESMarketplace name for the install ref <plugin>@<marketplace>. Distinct from source.
copilotpluginstringYESPlugin identifier (passed to copilot plugin install).
copilotinstall_urlstringnoURL shown in warn-skip message when CLI not found.
vscodeextensionstringYESExtension ID (passed to code --install-extension).
vscodeinstall_urlstringnoURL shown in warn-skip message when CLI not found.
opencodemodulestringYESModule name (passed to opencode plugin).
opencodeinstall_urlstringnoURL shown in warn-skip message when CLI not found.

source and marketplace are distinct and both required for the claude/copilot auto-install form. source is what marketplace add consumes (owner/repo, a URL, or a path); marketplace is the registered marketplace name the CLI requires in the install ref <plugin>@<marketplace>. The CLI derives the name from the marketplace’s manifest, so it generally differs from source — e.g. source anthropics/claude-plugins registers a marketplace named claude-plugins, and the plugin installs as superpowers@claude-plugins. Installing with <plugin>@<source> fails (Plugin not found in marketplace <source>), so the name MUST be supplied explicitly rather than inferred from source.

A plugin entry MAY declare any subset of client blocks. A plugin that only exists for Claude Code carries only a claude: block; clients without a matching block skip installation silently.

Additional descriptor forms:

A client block MAY use one of these alternative forms instead of the standard auto-install form. The mode is determined by which required field is present:

ModeDiscriminating fieldsAvailable forBehavior
Auto-installsource+marketplace+plugin / extension / moduleall clientsShell out to client CLI
Instructions-onlyinstructions_urlall clientsPrint info notice; no automated action
Config-writeplugin_uriopencode onlyWrite URI to project opencode.json

Instructions-only form:

plugins:
  example:
    opencode:
      instructions_url: https://example.com/install-docs
      summary: |
        Add "example@git+https://example.com/repo.git" to the
        plugin[] array in opencode.json, then restart opencode.
FieldTypeRequiredDescription
instructions_urlstringYESURL to manual install documentation.
summarystringnoShort text (1-5 lines) printed during install.

Config-write form (opencode only):

plugins:
  example:
    opencode:
      plugin_uri: "example@git+https://example.com/repo.git"
      install_url: https://example.com/install-docs
FieldTypeRequiredDescription
plugin_uristringYESPlugin URI appended to opencode.json plugin[] array.
install_urlstringnoURL shown in output for reference.

Disambiguation: serde attempts variants in declaration order (Install first). If both an auto-install field and an instructions-only field are present, the auto-install form takes priority. upskill lint SHOULD warn about ambiguous descriptors.

Install behavior:

ClientCLI commands
claudeclaude plugin marketplace add <source>, then claude plugin install <plugin>@<marketplace> --scope <scope>
copilotcopilot plugin marketplace add <source>, then copilot plugin install <plugin>@<marketplace>
vscodecode --install-extension <extension>
opencodeopencode plugin <module>

Claude’s --scope derives from upskill’s project/global context:

upskill scopeclaude --scope
projectproject
globaluser

CLI-missing policy (warn-skip): If the target client’s CLI is not on PATH, the implementation MUST print a warning and continue installing the rest of the bundle. If install_url is present, it MUST be included in the warning message. The rules, skills, and agents portion of the bundle MUST install regardless of plugin installation success.

Lockfile recording: Each installed or warn-skipped plugin MUST be recorded in the lockfile with its client, identifier, scope, and install status so that remove, update, and doctor can reconcile state. The status field distinguishes outcomes:

statusMeaning
"installed"Plugin successfully installed via the client CLI or config-write.
"skipped"Client CLI was not on PATH at install time (warn-skip).
"instructions"Instructions-only — no automated install; consumer must follow manual steps.

Plugins that fail for other reasons (non-zero exit from a present CLI) MUST NOT be recorded — these are transient errors and should not pollute the lockfile.

Lockfile plugin entry shape:

{
  "name": "superpowers",
  "client": "claude",
  "identifier": "superpowers@anthropics/claude-plugins",
  "scope": "project",
  "bundle": "baseline",
  "status": "installed"
}

The status field defaults to "installed" when absent (backward-compatible with pre-v0.6.x lockfiles that lacked this field).

Implementations:

  • MUST shell out to the native client CLI for auto-install plugin descriptors — MUST NOT manipulate client config files directly, except for the opencode config-write mode which writes to project-scope opencode.json per the documented plugin registration path.
  • MUST treat plugin install as idempotent (re-running on an already-installed plugin is a no-op).
  • MUST use warn-skip when the target client CLI is not found (ErrorKind::NotFound).
  • MUST NOT fail the entire bundle install when a single plugin install fails or is skipped.

mcps map

The optional mcps map declares MCP (Model Context Protocol) servers that accompany this bundle. Configuring an MCP server requires no content generation — instead, upskill writes the server into each targeted client’s MCP configuration at install time. See ADR-0010 for design rationale.

Each entry in the mcps map is keyed by an upskill-level MCP name (used in the lockfile, CLI output, and upskill remove-mcp). The value carries a transport descriptor and an optional list of declared environment variables:

mcps:
  drawio: # remote server over HTTP
    remote:
      type: http # http | sse
      url: https://mcp.draw.io/mcp
      headers: # optional; values MUST use ${VAR} references for secrets
        Authorization: "Bearer ${DRAWIO_TOKEN}"
    requires-env: [DRAWIO_TOKEN]

  my-tool: # local server spawned over stdio
    local:
      command: npx
      args: ["-y", "my-tool-mcp"]
      env: # optional; values MUST use ${VAR} references for secrets
        API_KEY: "${MY_TOOL_API_KEY}"
    requires-env: [MY_TOOL_API_KEY]

Exactly one of remote: or local: MUST be present per entry; both or neither is a validation error. remote.type MUST be one of http or sse. remote.url and local.command MUST NOT be empty.

Transport descriptor fields:

TransportFieldTypeRequiredDescription
remotetypestringYESProtocol: http or sse.
remoteurlstringYESEndpoint URL the client connects to.
remoteheadersmapnoHTTP headers. Values pass through verbatim (use ${VAR}).
localcommandstringYESLauncher command (e.g. npx, uvx, docker, bare binary).
localargsstring[]noArguments passed to the command.
localenvmapnoEnvironment variables. Values pass through verbatim.
(either)requires-envstring[]noEnv var names the server requires (declared, not valued).

Secrets and ${VAR} indirection. Values in headers and env that reference secrets MUST use ${VAR} syntax only. Implementations MUST pass these values through verbatim to the client config — they MUST NOT expand ${VAR} references, read the underlying values, or store literal secret values anywhere (not in config files, not in the lockfile, not in SSOT). The requires-env list declares which variable names are needed; implementations SHOULD warn at install time (e.g., during upskill add) when a declared variable is unset in the current environment, without reading its value.

Per-client install behavior: A bundle’s mcps: entry is configured for every MCP target — Claude Code, GitHub Copilot, VS Code, and opencode — CLI-first with a config-write fallback per target. VS Code is an MCP target but not a generation client: it shares .github/** rules/skills/agents output with Copilot and forks only on MCP, so the MCP target set (claude, copilot, vscode, opencode) is modelled separately from the generation Client enum (claude, copilot, opencode).

TargetPrimary (CLI)Fallback config fileRoot key
Claude Codeclaude mcp add <name> --scope <scope> [-e K=${V}] -- <cmd> [args] (local) / claude mcp add --transport <http|sse> <name> <url> --scope <scope> [-H …] (remote).mcp.jsonmcpServers
GitHub Copilotcopilot mcp add … (no --scope)~/.copilot/mcp-config.json (user scope)mcpServers
VS Codecode --add-mcp '<json>'.vscode/mcp.jsonservers
opencode— (no mcp add verb; config-only)opencode.jsonmcp

Per-target config shapes differ:

  • Claude Code / GitHub Copilot — { command, args?, env? } (local) or { type, url, headers? } (remote).
  • VS Code — local servers are typed "stdio"; remote servers carry their http/sse value in type, plus url and headers. The root key is servers, not mcpServers.
  • opencode — type is "local" or "remote". For local servers command is a single array combining the command and its args (["npx", "-y", "my-tool-mcp"]) and the env map is named environment (not env). Remote servers carry url and headers.
  • GitHub Copilot scope caveat — Copilot has no documented project-scope MCP config file (.github/mcp.json is not a Copilot CLI config source). The config-write fallback therefore always targets the user-scope ~/.copilot/mcp-config.json; project scope is configured via the copilot mcp add CLI or warn-skipped.

Claude’s --scope derives from upskill’s project/global context:

upskill scopeclaude --scope
projectproject
globaluser

CLI-missing policy (warn-skip): If the target client CLI is not on PATH, the implementation MUST fall back to writing the client config file. If the config-write also fails, it MUST print a warning and continue installing the rest of the bundle. The rules, skills, and agents portion of the bundle MUST install regardless of MCP configuration success.

Lockfile recording: Each configured or warn-skipped MCP server MUST be recorded in the lockfile with its client, scope, bundle, and status:

statusMeaning
"installed"Configured successfully via CLI shellout or config-write.
"skipped"Client CLI was not on PATH and no config-write target applied (warn-skip).

Lockfile MCP entry shape:

{
  "name": "drawio",
  "client": "claude",
  "scope": "project",
  "bundle": "drawio-diagrams",
  "status": "installed"
}

The mcps array is additive. Implementations that do not support this field MUST ignore it (serde default). The lockfile schema field stays at 1 — no version bump.

Removal: upskill remove-mcp <name> unregisters the named server from every target it was configured for and drops the matching LockedMcp entries from the lockfile. Targets with a native remove verb (Claude claude mcp remove, Copilot copilot mcp remove) are CLI-first, falling back to deleting the entry from the target’s config file when the CLI is absent; VS Code and opencode are config-file only.

Reconciliation: upskill doctor reconciles each lockfile MCP entry against its target. Claude is queried via claude mcp list; the config-write targets (Copilot, VS Code, opencode) are checked against their config file. A server present in the lockfile but absent from a present config file (or from the Claude client) is reported as NotRegistered and causes doctor to exit non-clean (exit 1). Claude also reports CliNotFound when the claude CLI is absent and QueryFailed when the list query fails; a config-write target whose config file is absent reports ConfigMissing and one that is unreadable reports ConfigUnreadable — both are informational “cannot verify” states that are surfaced but do not fail cleanliness (no false-positive exit 1), since the server may be configured via the client’s own CLI. Doctor does not check requires-env variables — the unset-env warning is emitted by upskill add at install time.

Implementations:

  • MUST prefer the client’s native MCP CLI verb over direct config-file writes.
  • MUST fall back to writing the client config file when the CLI is not found.
  • MUST treat MCP configuration as idempotent (re-configuring an existing server is a no-op or safe overwrite).
  • MUST NOT fail the entire bundle install when a single MCP configuration fails or is skipped.
  • MUST NOT expand ${VAR} references in header or env values.

Item requires resolution

Items (rules, skills, agents) MAY declare directed dependencies in their requires field (§3.1). This is distinct from bundle requires, which links bundles: item requires links individual items. See ADR-0009.

Implementations:

  • MUST install an item’s transitive requires closure: installing an item implies installing every item reachable through requires.
  • MUST resolve each requires.<kind> entry by (kind, name).
  • MUST reject circular item requires chains as errors.
  • MUST report the same (kind, name) resolving to a different source or ref as an error (the same rule already stated above for bundle item-level conflicts).

Each requires.<kind> entry is a bare name (same source) or a { name, source } map (cross-source), where source reuses the upskill add source DSL. Same-source resolution (bare-name entries against the entry item’s own already-fetched source) and cross-source resolution ({ name, source } entries) are both implemented: a cross-source entry is fetched via the same machinery as upskill add (a distinct source is fetched once and cached for the duration of the resolution), the transitive closure MAY span sources, and each dependency-pulled item is recorded in the lockfile with its own source (and ref) plus a required_by provenance list. Cycle detection is keyed by (canonical-source-label, kind, name); the same (kind, name) resolving to two different sources within one closure — or conflicting with an existing different-source install — is an error. There is no version-range solving.

3.8 Frontmatter canonicalisation

An implementation MAY provide a formatting command (e.g., upskill fmt) that canonicalises SSOT frontmatter and the markdown body. Frontmatter canonicalisation produces a deterministic key order, making diffs predictable and reducing merge conflicts. Body canonicalisation applies the same markdown formatter used for generated output (§7.4) and normalises the frontmatter↔body seam to a single blank line, so authored source matches generated output. A linter MAY report a body-format finding when a body is not canonical.

Canonical key order. When canonicalising frontmatter, implementations MUST emit keys in the following order (unlisted keys appear at the end in their original relative order):

KindKey order
Ruleschema, name, description, audience, license, scope, metadata, requires, ignore, claude, copilot, opencode, extras
Skillschema, name, description, audience, license, tools, preload-skills, metadata, requires, ignore, claude, copilot, opencode, extras
Agentschema, name, description, audience, license, mode, model, tools, preload-skills, metadata, requires, ignore, claude, copilot, opencode, extras
Bundleschema, name, description, license, items, requires, plugins, mcps, metadata, extras

Comment preservation. Canonicalisation:

  • MUST preserve YAML comments (#). Comments immediately preceding a key (no blank-line separation) travel with that key during reordering.
  • MUST preserve preamble comments. A block of comments at the top of the frontmatter, separated from the first key by at least one blank line, remains pinned to the top.
  • MUST NOT round-trip through a YAML serialiser that discards comments; line-level block reordering is the expected implementation strategy.

Validation. After reordering, the canonicalised output MUST parse identically to the original (same keys, same values). Implementations SHOULD validate this by parsing both the input and the output and comparing the resulting data structures.

Idempotency. Running canonicalisation twice on the same input MUST produce identical output.


4. Capability-level tool vocabulary

Agent frontmatter and item bodies reference tools by neutral capability names rather than client-specific identifiers. This decouples content from any single client’s naming conventions.

CapabilityMeaningClaude CodeGitHub Copilotopencode
readRead file contentsRead—read
writeCreate or overwrite filesWrite—write
editModify existing filesEdit—edit
bashExecute shell commandsBashshellbash
grepSearch file contentsGrep—grep
globMatch file pathsGlob—glob
web-fetchRetrieve URL contentWebFetchfetch—
web-searchSearch the webWebSearchweb_search—

Implementations maintain the mapping from capability names to client-specific identifiers. The mapping table above is informational; clients may change their tool names independently of this specification.

For MCP tools, which are server-specific and not covered by this vocabulary, body content SHOULD use the ServerName:tool_name form (colon-separated, preserving case). This form is readable across clients; client-specific internal forms (e.g., mcp__server__tool) SHOULD NOT appear in SSOT content.

This vocabulary is intentionally small. It covers the tools common to all three initial clients. Future specification versions may extend it. For tools not covered, item bodies SHOULD describe the desired capability in natural language rather than naming a client-specific tool.

Generation behavior for the agent tools: field. When generating a client-specific agent output, implementations process each entry in the SSOT tools: list per the table above:

  • Capabilities with a documented client mapping are emitted using the client’s name.
  • Capabilities without a documented mapping for a client (— cells above) are dropped from that client’s output, and implementations SHOULD log a warning at generation time identifying the dropped capability and the affected client. They are not passed through under the SSOT name — emitting an unrecognized identifier would produce a field the client does not consume.

Authors who need client-specific tool names beyond the documented mappings use that client’s passthrough block (claude:, copilot:, opencode: per §3.5). The passthrough mechanism is the supported way to surface non-spec tooling in generated output.

opencode caveat. opencode does not consume a tools: array; it consumes a permission: map, with capabilities mapped per a permission model where write rolls into edit. The §4 table shows the opencode capability identifier for reference, but the actual emission into opencode agent frontmatter follows the permission-map shape documented in §7.3 and Appendix B.


5. Body content conventions

Item bodies are markdown content following the frontmatter. These conventions ensure generated output is portable and passes standard markdown linting (markdownlint default ruleset).

5.1 Heading structure

  • Body content MUST NOT contain an H1 (#) heading. When a client requires an H1, implementations MUST derive it from the item’s name field (verbatim, no transformation). description is not used for H1 derivation; using it would make the formatting guarantee in §7.4 unenforceable.
  • Body headings MUST start at H2 (##) and MUST NOT skip levels (no H2 → H4 without an intervening H3).

Rationale: generated output prepends an H1 derived from the item metadata. An H1 in the body would produce duplicate H1s, violating MD025 (single-title/single-h1).

5.2 Fenced code blocks

All fenced code blocks MUST specify a language hint. Use text when no specific language applies.

Rationale: MD040 (fenced-code-language) requires language hints. Generating output that violates this rule forces downstream consumers to either suppress the lint rule or fix the output.

5.3 Tool references in prose

Body content SHOULD describe tools by capability rather than by client-specific name:

  • YES: “Search the codebase for occurrences of the deprecated function”
  • YES: “Use GitHub:create_issue to file a tracking issue” (MCP tool in portable form)
  • NO: “Use the Read tool to open the file” (Claude-Code-specific)
  • NO: “Use #tool:read” (GitHub-Copilot-specific)

5.4 File references

Body content SHOULD reference supporting files using standard markdown links with relative paths from the item directory:

See [approved licenses](./allowed-licenses.txt) for the canonical list.

Client-specific import syntax (@path for Claude Code, #file:path for GitHub Copilot) MUST NOT appear in canonical bodies.

5.5 Prohibited constructs in canonical bodies

The following constructs MUST NOT appear in always-rendered body content — that is, the portion of the canonical entrypoint body that reaches every client. They are explicitly permitted inside:

  • <!-- @client:X --> … <!-- @endclient --> directive blocks (§6), when X matches the construct’s client. The directive processor strips the entire block from outputs targeting any other client, so the construct never reaches a client that would not understand it.
  • Per-client override files (§2.3) — RULE.claude.md, SKILL.copilot.md, AGENT.opencode.md, etc. — when the filename suffix matches the construct’s historical generation client (claude, copilot or opencode). This is a format permission for the specified matching-client override selection, not implemented loading: the historical installer ignores override bodies and captured compilation rejects active matching overrides as recorded in §2.3. codex has no override-file form.
ConstructWhy prohibited (in always-rendered content)Client
$ARGUMENTS, $0, $1Substitution variableClaude Code
${workspaceFolder}, ${file}Substitution variableGitHub Copilot
!`command`Shell pre-executionClaude Code
@path/to/fileFile importClaude Code
#tool:nameTool referenceGitHub Copilot
#file:pathFile referenceGitHub Copilot
ultrathinkExtended thinking triggerClaude Code

If an item needs one of these constructs in content that would otherwise reach every client, wrap it in a matching-client directive block (§6) or move it to a matching override file for a historical generation client (§2.3). Items that target only one client end-to-end MAY instead be marked single-client via audience (§3.1).


6. Conditional content directives

Canonical bodies MAY include client-conditional content via HTML comment directives. These allow minor per-client variations without requiring separate override files.

6.1 Syntax

<!-- @client:claude -->

Content rendered only for Claude Code output.

<!-- @endclient -->
  • Opening delimiter: <!-- @client:<client-list> --> on its own line.
  • Closing delimiter: <!-- @endclient --> on its own line.
  • <client-list> is a comma-separated list of client identifiers, optionally prefixed with ! for negation.

Known audience/directive family identifiers: claude, copilot, codex, opencode. Override suffixes remain limited to the three historical generation clients in §2.3; codex does not add an override suffix.

These identifiers describe source families. They do not add selectable surfaces or change historical default clients. The library-only Codex instruction subset is documented in generation.

6.2 Forms

Positive (include for listed clients):

<!-- @client:claude,copilot -->

This content appears in Claude Code and GitHub Copilot output, not in opencode.

<!-- @endclient -->

Negative (include for everyone except listed clients):

<!-- @client:!opencode -->

This content appears everywhere except opencode output.

<!-- @endclient -->

6.3 Processing rules

Implementations:

  • MUST strip non-matching blocks entirely, including the delimiter lines and any surrounding blank lines that would otherwise produce consecutive blank lines (violating MD012).
  • MUST preserve the content of matching blocks, removing only the delimiter lines.
  • MUST validate that all opened directives are closed (balanced blocks).
  • MUST validate that family identifiers in directives are from the known set.
  • MUST reject nested directives. If nesting is detected, report an error.
  • SHOULD process directives before any other body transformation (formatting, rendering).

6.4 Interaction with override files

Override-file precedence over directives is defined by §2.3 (per-client override file rules); this subsection exists only as a cross-reference. When generating output for a historical generation client (claude, copilot or opencode), the specified resolution order applies only once override execution is implemented. The current historical installer ignores override bodies; the captured compiler rejects active matching overrides under AD-0025. The bounded Codex instruction producer rejects rule override entrypoints and processes directives only in the canonical body.


7. Generation: client-specific output

Delivery clarification (2026-10-19): the client output layouts below describe format/generation mappings, not exclusive native discovery or qualified runtime support. Compatibility directories, imports, globs and shared paths can expose those outputs to other clients. Future production activation must satisfy the delivery/discovery/activation contract, including explicit intended consumers, negative/duplicate controls and ownership preservation. Independent Codex/OpenCode rule delivery must not be inferred from these historical file layouts or implemented by rewriting team AGENTS.md. The user-decided native-first transition uses one shared always-on Claude/Copilot file when both are selected, with per-item reader consent and no second Copilot copy. Explicit OpenCode paths await Q5 PASS; its shipped glob and other output tables remain current until a separate code change. Managed package defaults and new layout ownership remain pending adoption/implementation (amendment inventory).

This section describes the expected output per client: which files an implementation writes for each item kind, and how SSOT frontmatter maps onto each client’s frontmatter shape. It is informational; client conventions may change independently of this specification. Implementations SHOULD verify output against actual client behavior.

This section is deliberately scoped to “what file and frontmatter each client reads”. Adjacent behavior — bridging files, IDE configuration mutation, third-party config-file management — is installer behavior owned by the implementing tool and intentionally out of this specification.

7.1 Claude Code

Item kindOutput pathFrontmatter mapping
Rule.claude/rules/<name>.mdpaths: array from scope.paths (if non-empty).
Skill.claude/skills/<name>/SKILL.mdname:, description: pass through. Agent Skills extended fields pass through.
Agent.claude/agents/<name>.mdname:, description:, model:, tools: (capitalized).

7.2 GitHub Copilot

Item kindOutput pathFrontmatter mapping
Rule.github/instructions/<name>.instructions.mdapplyTo: (comma-joined from scope.paths, default "**"), name:, description:. copilot.excludeAgent if present.
Skill.github/skills/<name>/SKILL.mdname:, description: pass through. Follows Agent Skills open standard.
Agent.github/agents/<name>.agent.mdname:, description:, model:, tools: (only documented §4 mappings — bash → shell, web-fetch → fetch, web-search → web_search — survive; unmapped capabilities dropped). GitHub Copilot-specific fields from passthrough block.

7.3 opencode

Item kindOutput pathFrontmatter mapping
Rule.agents/rules/<name>/RULE.mdname:, description:. opencode-specific fields from passthrough block. scope.paths dropped (no per-rule scoping per §3.2).
Skill.agents/skills/<name>/SKILL.mdname:, description:. Agent Skills extended fields and opencode-specific fields from passthrough block. opencode walks this directory.
Agent.opencode/agents/<name>.mdname:, description:, mode:, model:, permission: map (allow per capability; write rolls into edit). opencode-specific fields from passthrough block.

7.4 Formatting guarantee

Generated output MUST be formatted such that:

  • It is idempotent under the implementation’s chosen markdown formatter (e.g., dprint).
  • It passes markdownlint-cli2 with the default ruleset (or a documented subset pinned by the implementation).

Implementations SHOULD format generated markdown as the final step before writing, using an embedded or invoked formatter, so that developers running the same formatter locally see no diffs on generated files.


8. Schema versioning

The top-level schema field declares the specification version the file was authored against.

Current version: 1.

8.1 Rules for implementations

  • MUST reject files with schema greater than the highest version the implementation supports.
  • MUST produce a clear error message identifying the required specification version and suggesting an upgrade of the implementing tool.
  • MUST accept files with schema equal to or less than the highest supported version.
  • When processing older schema versions, MUST NOT silently apply newer defaults that would change the file’s meaning.

8.2 Rules for specification evolution

  • MINOR additions (new optional fields, new optional top-level blocks) MAY occur within the same schema version. Implementations MUST ignore unknown fields they don’t support.
  • BREAKING changes (removed fields, renamed fields, changed semantics of existing fields, new required fields) MUST increment the schema version.
  • Each published schema version MUST have a stable reference document with a permanent URL.

9. Conformance

An implementation is conformant with this specification if:

  1. It correctly parses all frontmatter fields defined in §3, accepting unknown fields within metadata and client passthrough blocks without error.
  2. It validates name against §2.1 constraints and rejects non-conforming names with a clear error.
  3. It rejects files with unsupported schema versions per §8.
  4. It correctly processes @client: directives per §6.3, including balanced-block validation.
  5. It resolves bundle items references against available item sources and reports unresolved names as errors.
  6. It preserves supporting files from item directories in generated output per §2.4.
  7. Generated output satisfies the formatting guarantee per §7.4.

A reference test corpus SHOULD accompany mature versions of this specification. For v0.1-draft, conformance is assessed against authored examples in the implementation’s own test suite.


10. Non-goals

This specification does not define:

  • Client file layouts or frontmatter conventions beyond what §7 documents as informational context. These are owned by each client’s maintainer and may change independently.
  • Installation, caching, synchronization, or registry protocols. These are implementation concerns.
  • CLI command surfaces, configuration files, or user-facing workflows.
  • Validation of semantic content quality. This is reserved for AI-review or other semantic analysis mechanisms outside the scope of format validation.
  • Formatting conventions beyond what §5 requires for portability and §7.4 requires for generated output.
  • Signing, provenance, supply-chain verification, or trust models. These are important but separate concerns for a future specification.
  • Access control, classification enforcement, or governance workflows. Implementations MAY use metadata fields to integrate with external governance systems, but the mechanisms are not specified here.

11. Open questions for future versions

The following topics are explicitly deferred and tracked for future specification work:

  1. Scope patterns: whether scope.paths should support negation (!test/**), boolean combinations, or named scopes.
  2. Tool vocabulary: whether the §4 table should be expanded, whether MCP tool references need a more structured form, and how to handle tools that exist in only one client.
  3. Agent mode semantics: whether primary / subagent / all is sufficient given divergent agent models across clients, or whether mode should be client-passthroughed entirely.
  4. Bundle constraints: whether requires should support semver ranges, platform predicates (requires-client: [claude >= 2.1]), or conditional items.
  5. Per-client partial overrides: whether override files should support section-level overrides (replacing only a specific heading’s content) rather than full-body replacement.
  6. Skill activation control: whether disable-model-invocation and user-invocable from the Agent Skills standard should be surfaced as neutral fields or remain client passthroughs.
  7. Multi-repo item sources (RESOLVED): the item requires field with a { name, source } locator (§3.7) defines the cross-source resolution contract — source reuses the upskill add source DSL, conflicts reuse the same-name-different-source rule, and cycles are keyed by (canonical-source-label, kind, name). Both same-source and cross-source resolution are implemented. See ADR-0009.
  8. Content hashing: whether items should carry a content hash for integrity verification during distribution, independent of metadata.version.

Appendix A: Complete frontmatter examples

A.1 Rule — always-on, all clients

---
schema: 1
name: license-awareness
description: Use when reviewing third-party code, AI suggestions, or external snippets to check licensing compatibility and avoid incorporating incompatible licensed code
license: proprietary
metadata:
  version: "1.0.0"
  author: platform-security
---

## Licensing rules

- Never paste code from external sources without verifying the license.
- When AI suggests code matching a recognizable upstream project, flag it before incorporating.
- See [approved licenses](./allowed-licenses.txt) for the canonical SPDX list.
- AGPL, SSPL, and BUSL code requires explicit approval from legal.

A.2 Rule — path-scoped, with GitHub Copilot passthrough

---
schema: 1
name: api-conventions
description: Use when writing or modifying API handler files to ensure consistent request validation, error formatting, and response shape
license: proprietary
scope:
  paths:
    - "src/api/**/*.ts"
    - "src/handlers/**/*.ts"
copilot:
  excludeAgent: code-review
metadata:
  version: "2.1.0"
  author: platform-api
---

## API handler conventions

- All endpoints include input validation using the project's schema library.
- Use the standard error response format defined in `src/api/errors.ts`.
- Never return raw database errors to the client.

A.3 Skill — on-demand, with directive

---
schema: 1
name: create-api-endpoint
description: Use when scaffolding new REST API endpoints with Zod validation, following project conventions for handler structure, error handling, and test coverage
license: proprietary
metadata:
  version: "1.0.0"
  author: platform-api
---

## Create a new API endpoint

Follow these steps to scaffold a complete endpoint:

1. Create the handler file at `src/api/handlers/{resource}.ts`.
2. Define the request schema using Zod in `src/api/schemas/{resource}.ts`.
3. Register the route in `src/api/routes.ts`.
4. Create the test file at `src/api/__tests__/{resource}.test.ts`.

<!-- @client:claude -->
When creating files, use the Write tool for new files rather than Edit.
<!-- @endclient -->

## Validation pattern

All inputs MUST be validated against the Zod schema before processing.
See [validation patterns](./references/validation-patterns.md) for examples.

A.4 Agent — subagent with tools and preloaded skill

---
schema: 1
name: security-reviewer
description: Use when reviewing code for injection flaws, authentication issues, secret leaks, and insecure data handling
license: proprietary
audience:
  - claude
  - copilot
  - opencode
mode: subagent
model: sonnet
tools:
  - read
  - grep
  - glob
  - bash
preload-skills:
  - security-baseline
opencode:
  temperature: 0.2
metadata:
  version: "1.0.0"
  author: platform-security
---

## Security reviewer

You are a security-focused code reviewer. When invoked:

1. Identify injection vectors (SQL, command, path traversal, XSS).
2. Verify authentication and authorization checks on every sensitive endpoint.
3. Scan for hardcoded secrets, tokens, API keys, or credentials.
4. Flag insecure data handling (unencrypted PII, weak cryptographic choices, improper logging of sensitive data).

For each finding, report severity (critical/high/medium/low), file and line location, and a concrete remediation.

A.5 Bundle — with dependency

---
schema: 1
name: android
description: Rules, skills, and agents for Android application development teams
license: proprietary
items:
  rules:
    - android-lifecycle
    - kotlin-coroutines
    - compose-conventions
  skills:
    - create-viewmodel
    - migrate-to-compose
  agents:
    - android-reviewer
requires:
  - platform-baseline
metadata:
  version: "0.8.0"
  author: android-platform
---

## Android bundle

Layered on top of `platform-baseline`. Provides Android-specific development
conventions, Jetpack Compose patterns, and a specialized code reviewer.

A.6 Agent — with per-client override file

Directory structure:

agents/security-reviewer/
├── AGENT.md                   # canonical
├── AGENT.claude.md            # Claude Code body override
└── AGENT.copilot.md           # GitHub Copilot body override

This is a format illustration, not current installer behavior. Under the specified §2.3 resolution order, AGENT.md supplies frontmatter for all clients; the Claude and Copilot overrides replace the canonical body for their respective clients. OpenCode uses the canonical body with directives processed. The current historical installer instead renders the canonical body for all three clients; the captured compiler rejects an active matching override under AD-0025. Neither path currently implements the illustrated override selection.


Appendix B: Client frontmatter mapping reference

This table summarizes how SSOT fields map to each client’s frontmatter when generating output. This is informational and subject to change as clients evolve.

SSOT fieldClaude Code outputGitHub Copilot outputopencode output
namename:name:name:
descriptiondescription:description:description:
scope.pathspaths: (array)applyTo: (comma-joined string)(not supported; always-on)
mode(implicit in file location)(agent definition)mode:
modelmodel: (alias)model: (full ID)model: (provider/ID)
tools (agent)tools: capitalized namestools: mapped only (§4); unmapped droppedpermission: map (allow per capability; write rolls into edit)
preload-skillsskills: in agent——
copilot.*(stripped)Merged into frontmatter(stripped)
opencode.*(stripped)(stripped)Merged into frontmatter
claude.*Merged into frontmatter(stripped)(stripped)
metadata.*(stripped from output)(stripped from output)(stripped from output)

Appendix C: Directive quick reference

<!-- @client:claude -->
Include only in Claude Code output
<!-- @client:copilot -->          Include only in Copilot output
<!-- @client:opencode -->         Include only in opencode output
<!-- @client:claude,copilot -->
Include in Claude Code and GitHub Copilot, not opencode
<!-- @client:!opencode -->
Include everywhere except opencode
<!-- @endclient -->
Close any directive block (required)

This specification is intentionally tool-agnostic and lets registries pick any <item-root> (§2.1) and any <bundle-root> (§2.2). The upskill project records its specific recommendation — a flat skills/ directory holding both items and bundles, with a nested skills/bundles/ variant for large registries — in the user guide:

→ Conventions

That recommendation is non-normative; a conforming registry MAY deviate and conforming tooling MUST NOT reject deviations.

Rules: one directive per line, no nesting, balanced open/close, known client identifiers only.

Edit the canonical source.

Generation and compilation

Architecture reconciliation (2026-10-25): the delivery, discovery and activation contract controls intended production routes and reader intent. Existing Codex AGENTS.md contribution APIs described here are experimental library boundaries, not production independent-rule delivery. Independent Codex/OpenCode rules remain beside unchanged team guidance through a selected native or hook route. The native-first amendment permits explicit OpenCode paths after Q5 PASS and selects the trusted Codex hook with its declared recorded-history limitation. Reusable ownership, preflight and recovery mechanisms do not select or qualify a runtime route, and schema-2 production activation remains blocked (AD-0040 amendment).

This concern owns the requirements and implemented architecture below. Moving these records does not change their implementation or approval status. Library-only capabilities remain separate from production CLI activation; each section records its own limits. Accepted decisions remain in ADRs.

Requirements

Captured item compilation

The opt-in compile::item library API compiles one captured item into complete per-client file sets without rereading source paths, writing destinations or mutating inputs:

compile_item borrows a SourceManifest and ItemCompileRequest and returns anyhow::Result<CompiledItem>. The exact public declarations are in compile::item. ItemCompileRequest contains kind: ItemKind, folder_name: String, alias: Option<String> and clients: Vec<Client>. CompiledItemFile contains path: String, bytes: Vec<u8> and mode: Option<u32>. CompiledClientItem contains client: Client and files: Vec<CompiledItemFile>. CompiledItem contains kind: ItemKind, source_name: String, installed_name: String, source_manifest_sha256: String, outputs: Vec<CompiledClientItem> and audience_excluded: Vec<Client>.

All four structures derive Debug, Clone, PartialEq and Eq, without serialization. ItemKind is the existing pipeline kind and Client is the current Claude, Copilot and OpenCode generation vocabulary. The result owns its values and records the exact validated supplied manifest digest as source identity; that digest does not establish provenance or identify generated content.

Compilation first validates every manifest path, ordering, digest and mode, including files later excluded or ignored. File/descendant pairs such as a and a/b fail even when ignored. Requested clients must be explicit, nonempty and unique. The canonical root entrypoint for the requested kind must exist, contain UTF-8 and parse as its existing typed frontmatter with a supported schema. The folder, resolved source name and optional alias must each be one portable relative component. Skills use the folder name; rules and agents use their optional declared name, otherwise the folder. An invalid declared rule or agent name fails. An alias controls installed paths and resource namespaces while rendered frontmatter retains the source identity under the existing renderers.

Outputs and audience_excluded follow Client::ALL order regardless of request order. Each requested client omitted by canonical audience is explicitly excluded; a wholly excluded selection succeeds with empty outputs. This does not imply removal of existing files.

Resource selection reuses the existing root-only canonical entrypoint and known <KIND>.<client>.md override exclusions and the requested kind’s subtractive ignore semantics. Nested equivalents, unrelated client-suffixed filenames, unknown-client suffixes, hidden files, empty files and binary files remain resources unless ignored. A root override for the requested kind and an emitted client fails with an unsupported-override error even if ignored. Overrides for other kinds or non-emitted clients remain excluded source inputs. Body override precedence is deferred.

Each included output contains exactly one rendered entrypoint and every retained resource, sorted by portable UTF-8 relative path. Compilation reuses existing renderers, directives, passthrough, formatting and frozen historical layout. For flat outputs it rewrites copied-resource links using the source name, then re-prefixes source namespaces to the alias when different, including previously namespaced links. Directory-backed outputs retain relative links. Duplicate or file/descendant output paths fail; errors return no successful partial result.

Resource bytes and optional permission modes equal captured evidence exactly. Generated entrypoints carry mode: None; this does not authorize permission changes. A qualified writer must establish destination permission policy and persist observed mode evidence separately.

Current commands, installer behavior and lockfile persistence remain unchanged. Returning an OpenCode rule file does not install its instruction registration. The API supplies no installation roots, ownership, destination observations, reference claims or application instructions and makes no native support claim. Opt-in source-backed orchestration connects rule compilation to requested-selection reconciliation, qualified source/destination evidence, writer locking, durable state and execution/recovery. Registry/bundle discovery, production activation and native Codex qualification remain pending. The separate mixed coordinator in mixed owned-artifact operation plans accepts explicit desired ownership and observations; compilation does not produce those lifecycle inputs. This prerequisite completes neither M1-A2 nor M1-A3.

See the compiler design, AD-0024, AD-0025 and AD-0026.

Captured rule desired sets

The opt-in lifecycle::desired::compile_rule_desired_set API validates a complete captured rule source and its explicit installation/source/consumer binding, then projects every existing compiler output into a complete desired file set. Exact bytes, resource modes, aliases, raw roots and explicit consumers survive. Requested surfaces remain recorded after audience exclusion, including when the desired set is empty. Empty output is not an installation removal.

Consumers must exactly account for requested surfaces and match the explicitly selected Claude, Copilot or OpenCode compiler family. No Codex binding or native conformance claim is introduced. Active scoped OpenCode output is rejected rather than silently losing nonempty SSOT scope.paths.

These are pure supplied-value results with no I/O, ownership adoption, lockfile rewrite or production command activation. Physical qualification, permission selection, reference/shared-owner reconciliation and application remain separate. See captured rule desired sets.

compile_rule_desired_sets returns all complete sets in request order only when all requests compile and pass preflight. Otherwise Blocked contains contextual request-index/installation-ID diagnostics and no successful subset. Duplicate installation IDs and equal or ancestor/descendant selected paths under the same scope and raw root spelling conflict, including equal bytes. Diagnostics sort by request indices then message. Empty input returns Ready([]). Different raw roots and scopes remain separate; physical aliases and case equivalence require later qualification. Each failed compilation contributes one contextual error rather than exhaustive diagnostics within that rule.

Architecture

Generation and installation

This record describes the existing module boundaries. It does not extend implemented client support. See ADR-0001 and ADR-0003 for the architecture decisions.

The Rust crate provides both a library and CLI. src/main.rs and src/cli.rs provide the CLI surface and orchestration. Domain work belongs in library modules; presentation stays at the CLI boundary.

ResponsibilityModules
Content model and parsingsrc/model/, src/parse/
Source classification and retrievalsrc/source.rs, src/fetch.rs
Bundle compositionsrc/bundle.rs
Per-client renderingsrc/generate/
Installation and installed-state operationssrc/pipeline/, src/lockfile.rs, src/ancillary.rs
Pure legacy-state interpretationsrc/legacy/
Native integrationssrc/plugin.rs, src/mcp.rs
Author commandssrc/lint.rs, src/fmt.rs, src/scaffold.rs

The central flow is source content to parsed items to per-client output. Bundles compose items and native integrations. Generated content is copied to its target location; the lockfile records installed state. The portable format owns field and layout details; this record does not duplicate those contracts.

Current content generators are Claude Code, Copilot and OpenCode. Generation and MCP target sets differ, as recorded in ADR-0012. ADR-0013 adds design commitments for Codex, Cursor and Gemini CLI; it does not establish implemented adapters.

Captured item compilation

This record describes the implemented pure producer in compile::item. captured item compilation owns its requirements. The producer consumes one supplied captured manifest and returns complete current-generation outputs; the current installer does not call it. AD-0024 records the bounded compatibility choice and its alternatives.

Public boundary

compile_item borrows a SourceManifest and ItemCompileRequest, returning an owned CompiledItem through anyhow::Result. The four public structures are ItemCompileRequest, CompiledItemFile, CompiledClientItem and CompiledItem. They derive Debug, Clone, PartialEq and Eq without serialization. The request supplies the existing ItemKind, folder name, optional installed alias and explicit generation clients. Each client result contains files with portable relative paths, exact bytes and optional modes; the outer result retains kind, resolved source identity, installed name, supplied manifest digest and explicit audience exclusions. The source module owns capture and manifest validation. No source path is passed to the compiler.

The Client vocabulary covers Claude, Copilot and OpenCode generation. It does not establish TargetSurface conformance, native support, required target semantics or incidental-reader isolation. Results contain no roots, owners, destination observations, reference claims or application instructions.

Validation and identity

Compilation starts with complete manifest validation, including files later ignored or reserved. An additional topology check rejects duplicate paths and strict slash-component file/descendant pairs throughout the supplied source. The folder and optional alias are validated as single portable components using the ownership path predicate and slash rejection. Requested clients must be nonempty and unique.

The compiler finds the requested kind’s canonical root entrypoint, decodes it as strict UTF-8 and parses its existing typed frontmatter. A private ParsedItem enum dispatches typed naming, audience, ignore and rendering policy. Shared effective_name uses the folder for skills and a declared name, if present, for rules and agents; the resolved name is validated before use. The installed name is the alias when present. Existing renderers receive the resolved source name, so aliasing changes paths and resource namespaces while retaining rendered frontmatter identity.

source_manifest_sha256 copies the validated manifest identity, covering all captured files regardless of output selection. It establishes neither source provenance nor rendered-content identity. Inputs remain unchanged, and any error returns no successful partial compilation.

Resources and unsupported overrides

The producer shares the existing crate-private resource classifier and subtractive ignore filter with the installer through narrow re-exports in pipeline. Their bodies and historical call paths are unchanged. Only recognized canonical entrypoints and known override names at the root are reserved. Nested equivalents, hidden files, unrelated names, unknown-client suffixes and empty or binary files remain resources unless ignored. Selection retains original captured values in manifest order.

For each requested client, canonical audience exclusion is recorded explicitly. An entirely excluded request returns empty outputs and all requested exclusions; it grants no removal authority. Before rendering an included client, compilation rejects a matching root override for the requested kind, independently of ignore patterns. Other-kind and non-emitted-client overrides remain reserved source inputs. AD-0025 records the unsupported boundary; body override precedence remains unimplemented.

Rendering and output

Existing typed renderers retain directives, passthrough, client frontmatter and Markdown formatting. Frozen historical layout helpers compute entrypoint and resource locations. Flat output first rewrites copied resource links under the source name and then re-prefixes that namespace to a different alias. Both existing link passes are preserved, including the second pass for links already namespaced with the source name. Directory-backed output keeps relative links with its entrypoint and resources.

Generated native paths are converted by accepting only normal UTF-8 components and joining them with /, followed by portable path validation. Each included client contains one rendered entrypoint and all retained resources, sorted by portable path. Duplicate and strict file/descendant output locations fail. Clients and exclusions follow Client::ALL, independently of request order.

Resources retain exact captured bytes and optional modes. Generated entrypoints carry mode: None; AD-0026 records why destination permission policy requires separate qualification.

Evidence and downstream integration

tests/compile_items.rs pins all nine kind/client pairs with independent expected rendering and paths, complete resource vectors, nonuniform resource modes, explicit audiences for all clients, both flat alias link passes, source identity despite reserved or ignored output, invalid inputs, impossible topology and source deletion after capture. The tests establish supplied-value compilation and compatibility, without filesystem execution or native-target conformance.

The producer prepares complete source-derived sets. Opt-in source-backed orchestration supplies explicit roots, consumers and qualified evidence to lifecycle planning and guarded application. The mixed coordinator still accepts explicit complete file/contribution sets; compilation alone grants no ownership or write authority. Qualified source/destination observations, writer locking, permission selection, combined state and execution/recovery are connected through the opt-in library adapters. Final-head hosted qualification remains required. Registry/bundle discovery, production activation and native Codex qualification remain pending. Returning an OpenCode rule file does not install its separate instruction registration. Current commands, installer behavior and schema-1 persistence remain unchanged; M1-A2 and M1-A3 are incomplete.

Captured rule desired sets

The opt-in lifecycle::desired library binds complete captured rule compilation to an explicit installation root and explicit target consumers. It produces supplied-value desired files, not ownership, execution authority or native conformance evidence. Production commands and lockfiles remain on schema 1.

Installation and source binding

compile_rule_desired_set accepts an Installation, a complete captured SourceManifest, a source folder name and explicit CompilerBinding values. It validates the installation through the combined-state validator and validates the entire capture. The captured manifest digest must equal recorded source evidence. Git and local revision constraints retain the combined-state contract; these strings do not authenticate source origin.

Only rule installations are accepted. The existing captured-item compiler resolves source identity and aliases, performs audience filtering, handles resources and ignores, and renders exact per-family output. Its source name, installed name and manifest identity must match the installation. No second output-layout table is introduced, and source paths are never reread.

Explicit consumers

Compiler familyAllowed explicit consumer identities
Claudeclaude.cli
Copilotcopilot.cli, copilot.vscode-local
OpenCodeopencode.cli

Bindings require one entry per selected family, nonempty unique consumer lists and exact accounting for requested surfaces. Copilot gains both consumers only when both were explicitly requested. Codex CLI and IDE have no compiler binding here and fail explicitly, even if author audience would exclude them. Family identity matching is not a native discovery or scope qualification claim.

Audience-excluded families are projected onto only their explicitly bound consumers. Their requested surfaces and complete installation record survive. An empty desired set is valid after exclusion and is never converted into an installation removal by this producer.

An active OpenCode output with nonempty SSOT scope.paths is rejected because the historical renderer drops that restriction. Absent/empty scope is accepted; an audience-excluded OpenCode family has no output whose scope could be widened. Existing compiler and production rendering behavior remain unchanged.

Complete desired files

Every compiled entrypoint and resource becomes a DesiredArtifact::File under the supplied root, preserving its raw spelling and scope. Bytes, resource filtering, alias link rewriting and optional captured modes are retained exactly. Generated entrypoint modes remain unspecified. Physical permission selection and qualification remain later application work.

Files are ordered by their portable relative path and consumers by surface name. Repeated paths within an installation conflict. These lexical checks do not resolve symlinks, aliases, case equivalence or physical containment.

Explicit contract production

compile_rule_desired_set_with_contracts accepts the same captured compilation request plus explicit TargetProfile values and returns ContractedDesiredSet containing the unchanged desired set and per-artifact owner contracts. Profiles must exactly cover requested surfaces, be unique, match the surface’s family and carry nonzero revisions. The producer never obtains revisions from defaults.

Compilation reuses compile_rule_desired_set, including source identity, alias/resource handling and audience filtering. Each generated file contract binds the exact root/path/installation, captured source-manifest digest, rendered byte hash, optional mode and only that output’s consumers with supplied revisions. Consumer profiles sort by surface name. Audience exclusions create no artifact contracts, including an empty desired set. The separate Codex instruction producer implements a restricted library-only subset. The original single and batch producers keep their existing behavior and shared-path rejections.

The contract producer and acceptance examples cover explicit profile validation and exact captured bindings. These are recorded supplied contracts, not native qualification.

Complete batch preflight

compile_rule_desired_sets compiles every independent request and returns either Ready with all sets in input order or Blocked with contextual diagnostics. There is no successful subset in a blocked outcome. An empty batch is ready with no sets. Compilation reports one error per failed request, without claiming all errors within that rule.

Duplicate installation IDs conflict even at disjoint roots. Successful requests also conflict when selected file paths are equal or ancestors of one another under the same scope and raw root spelling. Equal bytes do not establish a shared-owner contract. Different raw roots or scopes remain separate at this lexical boundary; physical aliases and case equivalence need later qualification. Diagnostics contain both request indices and installation IDs and sort by indices, then message. This boundary does not reconcile existing state or references.

Interfaces and acceptance evidence

The single-rule producer defines CompilerBinding, RuleDesiredSetRequest and DesiredInstallationSet. The batch boundary exposes DesiredSetDiagnostic, DesiredSetsOutcome and compile_rule_desired_sets. These supplied-value interfaces neither serialize plans nor authorize effects.

The public acceptance tests compare literal paths, binary resource bytes, modes and checked-in renderer goldens. They cover source identity and fallback names, exact requested and excluded consumers, OpenCode scope refusal, raw root retention, complete ordered batch payloads, component-boundary topology, duplicate IDs and contextual diagnostic ordering. The validation note records local checks and the limits of the independent review.

Remaining lifecycle boundary

This producer validates each installation in a minimal direct-reference graph; it does not reconcile the authoritative graph across the batch or establish cross-request installation-identity consistency beyond duplicate ID rejection. Separate reference reconciliation and removal planning now establish graph retention and shared-owner retirement with authoritative per-owner consumers. Pure selection preflight now aggregates legacy/native conflicts for all/literal-name requests. The pure single-owner upsert coordinator now joins complete sets, selected graph changes and supplied destination observations. Source/scoped selectors and CLI precedence, complete instruction compiler adapters and lifecycle integration remain task 2b. Explicit shared upsert now consumes recorded agreement through a separate inactive path. The mixed planner’s affected multi-owner rejection remains intact. Guarded backup/application/recovery, observational adapters and native activation remain separate tasks.

Captured Codex instruction boundary

lifecycle::desired::codex::compile_codex_instructions is a pure library API for one explicitly requested codex.cli profile at revision 1. It accepts a validated captured RULE.md, its installation and folder binding, and an instruction location. It emits ContractedDesiredSet and a separate InstructionReaderReport. It performs no source reads, environment lookup, source orchestration or native qualification.

The supported rule is unconditional and has no effective resources after the existing ignore filter. Nonempty scope, rule override entrypoints, native maps and unknown top-level fields fail closed with typed capability errors. Metadata, license and dependency declarations remain source information; the API does not resolve dependencies. Source and manifest mismatches, malformed names and roots, and impossible file/descendant source topology remain validation errors even when a conflicting resource would be ignored. Canonical family-filtered Markdown must be nonempty, representable, newline-terminated and free of managed markers.

Location contract revision 1 accepts project AGENTS.md or default global .codex/AGENTS.md, bound to the installation’s declared root and scope. It rejects custom, override and external destinations. The caller must separately qualify physical roots and the actual default native home; this producer does not resolve CODEX_HOME.

Project readers are codex.cli, codex.ide, copilot.cli, copilot.vscode-local and opencode.cli, ordered by surface name. Global readers are codex.cli and codex.ide. The report records codex.cli as requested and the others as potential incidental readers. VS Code Local exposure includes the chat.useAgentsMdFile setting condition. Readers derive from the reviewed location contract; callers cannot omit them. Every reader must be audience-eligible and receive identical canonical Markdown after family directives. Equality establishes this structural subset, not equal runtime behavior. Codex audience exclusion retains installation history with no artifacts or contracts and reports NotEvaluated compatibility.

Only codex.cli enters artifact consumers, owner profiles and retention associations. The reader report creates no ownership or references. Contribution IDs are lowercase SHA-256 of the JSON tuple ("upskill-codex-contribution-v1", installation_id), independent of body, alias and source revision. Owner contracts bind the original manifest and canonical body digest. Updates retain the ID and change the body digest. Separate installation identities remain separate contributions; no cross-owner sharing is inferred.

Interface and validation order

The public declarations live in lifecycle::desired::codex. CodexInstructionRequest borrows the installation, captured manifest, folder name and instruction location, and supplies the intentional target profile. CompiledCodexInstructions owns the contracted desired set and reader report. The report retains the location, location-contract revision, ordered reader profiles and roles, and compatibility result. Emitted content reports UnconditionalMarkdown; excluded content reports NotEvaluated.

The producer validates the rule installation, intentional profile and selection, combined-state binding, complete manifest and source topology before checking the manifest digest and destination binding. It then validates names, parses the canonical entrypoint and rejects unsupported scope, extensions, overrides and effective resources. These checks precede audience exclusion. Included content passes control-character checks, family directive processing and Markdown formatting for each reader. Contribution validation runs before the owner contract and artifact are returned. Errors carry installation context; capability rejections retain a downcastable CapabilityError in the error chain.

The shared Audience vocabulary and process_for_family support Claude, Copilot, Codex and OpenCode families. Cursor, Gemini and unknown directive names remain errors. Existing process(body, Client) delegates through the family processor, preserving the historical three-client selection and rendering.

The acceptance target covers reader conflicts, ownership separation and captured lifecycle preservation of manual text, another installation and file mode during update/removal. AD-0039 records the decision. Production CLI selection, schema 1 and historical generation layouts stay unchanged. Selected Codex rule sources now integrate with the opt-in library lifecycle route, including complete preflight conflict reporting for selected rule/instruction sources. Complete all-kind adapters, production activation, native user/IDE/Linux/OpenCode qualification and staging accounting in #316 remain later gates.

Edit the canonical source.

Sources and captured evidence

Architecture reconciliation (2026-10-25): the delivery, discovery and activation contract controls intended production routes and reader intent. Existing Codex AGENTS.md contribution APIs described here are experimental library boundaries, not production independent-rule delivery. Independent Codex/OpenCode rules remain beside unchanged team guidance through a selected native or hook route. The native-first amendment permits explicit OpenCode paths after Q5 PASS and selects the trusted Codex hook with its declared recorded-history limitation. Reusable ownership, preflight and recovery mechanisms do not select or qualify a runtime route, and schema-2 production activation remains blocked (AD-0040 amendment).

This concern owns the requirements and implemented architecture below. Moving these records does not change their implementation or approval status. Library-only capabilities remain separate from production CLI activation; each section records its own limits. Accepted decisions remain in ADRs.

Requirements

Strict captured source manifests

The separate library module source_manifest provides capture_source(&Path) -> anyhow::Result<SourceManifest> and pure validate_manifest(&SourceManifest) -> anyhow::Result<()>. It does not change commands, legacy source hashing/discovery, source formats or lockfile writes. This supplies the captured values consumed by the implemented item compiler to derive complete per-client entrypoint and resource outputs. The pure planner for explicitly supplied whole-file sets is implemented in owned whole-file operation plans. Captured-rule desired sets are implemented in captured rule desired sets; opt-in lifecycle reconciliation and source-backed orchestration connect these sets to guarded application.

SourceManifest contains sorted CapturedSourceFile records and a lowercase content_sha256. Each file contains a UTF-8 relative path joined with /, owned bytes, their lowercase content_sha256, and optional mode. Capture includes every observed regular file without ignore rules or entrypoint exclusions, including nested entrypoints, overrides, empty and binary files. Empty directories have no record. Each file is read once; on Unix its observed mode is PermissionsExt::mode() & 0o7777, and on other hosts mode is absent. Mode evidence does not authorize applying permissions or executing content.

Capture reconstructs the root from Path::components() before metadata lookup, so trailing separators and dot suffixes do not hide an observed root symlink. It rejects observed root symlinks and non-directory roots, every observed entry symlink and special entry, and enumeration, metadata or file-read errors. Errors return no partial manifest. Relative paths reject empty, dot and parent components, backslashes, colons, control characters and non-UTF-8 names. Hard-linked regular files are represented by each path without a topology claim.

The manifest hash is SHA-256 over these exact bytes, in order:

  1. ASCII upskill:source-manifest:v1 followed by one NUL byte.
  2. File count as an unsigned 64-bit big-endian integer.
  3. For each file in Rust string path order: path byte length as unsigned 64-bit big-endian, UTF-8 path bytes, content byte length in the same encoding, exact content bytes, then mode-presence byte (0 absent or 1 present). A present mode follows as an unsigned 32-bit big-endian integer.

Length conversion is checked. Per-file hash fields are verified but are not additional digest inputs. Root spelling, timestamps and enumeration order are excluded. Pure validation borrows the public values without mutation or sorting and checks lexical paths, strict ascending uniqueness, optional mode range 0..=0o7777, exact file hashes and the aggregate hash. Public records and valid hashes do not establish trusted provenance or source authenticity.

Standard synchronous filesystem observation is not an atomic snapshot, descriptor-relative containment or protection against concurrent replacement. Root ancestors may be symlinks. Lexical validation does not qualify destination case collisions, reserved names, aliases or filesystem limits. The separate selected-source boundary records immutable Git evidence. Opt-in source-backed orchestration connects captured-rule desired sets with lifecycle state and guarded execution. Production commands retain schema 1; native Codex qualification and activation remain Gate 6 work. Qualified scope roots and writer locking (qualified scope roots and advisory writer locking) and the inactive destination observation boundary (qualified destination observations) are implemented separately. Qualified source snapshots (qualified source tree observations) add retained source evidence without changing this freely mutable value interface.

See the source manifest design and its decision records for implementation boundaries and rationale.

Qualified source tree observations

The inactive library boundary exposes QualifiedScope::observe_source_tree(&self) -> anyhow::Result<SourceSnapshot<'_>>. An opaque, non-Clone snapshot borrows exactly one independently selected qualified source scope. Its manifest(&self) -> &SourceManifest accessor borrows plain captured values, and verify_unchanged(&self) -> anyhow::Result<()> checks the original evidence without refreshing it. There is no public constructor, mutable accessor, serialization, write API or implicit writer lock. Debug output contains no captured content. Unsupported hosts expose the type and return explicit errors.

Capture covers every descendant: hidden and ignored names, .git contents, binary and empty files, and empty directories. Names must be UTF-8 and satisfy the portable relative-path rules before recursion or reading. Hard-linked regular source paths remain distinct manifest records; this does not change destination single-link eligibility. Exact sorted directory membership, topology, identities and stamps remain private evidence, including empty directories absent from the manifest. Repeated directory identities, symlinks and special entries reject.

Every access is relative to a pinned directory descriptor and one exact stored component. Inspection uses SYMLINK_NOFOLLOW; opens use NOFOLLOW and CLOEXEC, DIRECTORY for directories and NONBLOCK for regular entries. Selected/opened identity and type must agree. The shared native qualification policy applies to the root and every opened descendant, and requires one unchanged source mount binding. The source mount may differ from any destination mount.

File acquisition compares metadata before open, immediately before read and after read, then requires exact byte count and nonnegative observed size to agree. Stamps retain device/inode, complete native mode, link count, size and nanosecond mtime/ctime; atime is ignored. Directory stamps must remain stable across enumeration and descendant capture. Permission evidence is Some(mode & 0o7777). Canonical assembly shares the existing strict-manifest v1 hash framing and pure validation, preserving legacy capture signatures, diagnostics and digests.

Before returning, capture performs full revalidation. Every later check verifies retained descriptors, reopens the bound root, acquires a fresh complete tree and compares membership, identities, stamps, exact bytes and modes with original evidence. The fresh tree is discarded. Both checks after one mutation must reject and leave the manifest unchanged. Root and ancestor-alias rebinding, same-byte inode replacement, same-descendant directory replacement, file/empty-directory addition or removal, rename, mode and fractional timestamp changes reject. Binding is checked again after the walk.

This read-only sampled boundary changes no CLI, schema, native adapter, source fetch, lifecycle state, permission policy, application or recovery behavior. Recoverable failures retain operation/path context and original causes and return no partial snapshot. Ordinary infallible allocation may abort; OS termination is outside Result guarantees. Checks provide neither an atomic snapshot nor exclusion against external editors, relocation or hostile ABA changes. Native Linux/macOS qualification uses existing profiles; Windows qualification, privileged mounts/devices and physical I/O or exhaustion faults remain separate evidence limits. Final-head hosted Linux/Windows gates remain required. See AD-0030 and the source observation design.

Local APFS source coverage at a393263 included 12 public acceptance tests and 31 private tests, with independent regular-descendant profile/mount qualification before reading, complete permission-bit mapping, held-writer capture/revalidation and non-content Debug checks. Independent link-count and ctime omission isolation remains a coverage limit; both fields participate in production stamps.

Scoped revalidation diagnostic tests retain source-root/operation context, complete relative paths in tree comparisons and original causes, including repeated public rejection without refreshing the manifest. Public root and ancestor binding failures retain source-root/verification context and the original scope cause. Retained nested-file diagnostics are independently pinned with unchanged parent stamps and membership. A final-binding control now rebinds an ancestor only after fresh capture/comparison and independently pins the last revalidation check. Empty-root and directory-only positive fixtures exercise qualified capture and revalidation of valid empty manifests.

On 2026-10-13, CI run 38346202172 and CodeQL run 38346200154 succeeded for PR head a3932633e9cc9a269f8ba8d8b6dd2ec26d0b828e. The tested merge commit 604dcda92bbc3c08d4ce10b695b18f95fb4d5e83 and that PR head share tree 48b716136a7822428e6ed163be2689beaaf9ddfa. Native Linux job 115128098194 executed the source missing mount-ID mask and non-UTF-8 cases. Windows job 115128098069 executed scope rejection and compiled the opaque source API; its uncalled source helper did not establish source-method runtime coverage. A new private unsupported-host test now calls the source method and requires contextual rejection. Its native Windows execution and all hosted checks for the eventual final PR head remain merge gates. No qualified Windows backend is implemented.

Selected lifecycle rule sources

The opt-in applying source prerequisite resolves one explicitly selected local or Git rule item root and retains its qualified source identity through capture and compilation. Captured source evidence remains bound to the resolver that supplied its source label and actual Git commit. Compilation returns complete desired artifacts and matching owner contracts for explicitly supplied installation, consumer and target bindings. Branches, tags and full commit SHAs record the resolved checkout commit rather than treating a requested ref as immutable.

Resolution rejects final local symlinks and Git subfolder escapes. Private Git metadata stays outside the captured item tree; inherited repository-local Git overrides cannot redirect checkout writes. Authentication continues through Git’s native global/system configuration, credential helpers, SSH and askpass. Revalidation checks the retained source tree and checkout revision without refreshing evidence. The boundary performs no destination observation, registration, output or backup writes. Preview uses supplied captured evidence and does not call this resolver. The source-backed orchestration boundary now connects explicit rule roots to captured command preparation. Discovery, Codex qualification and schema-2 production activation remain pending.

Architecture

Strict captured source manifests

This record describes the implemented source_manifest library boundary. strict captured source manifests owns its requirements. Capture observes source files; validation checks supplied values without I/O. Neither function participates in current command dispatch. AD-0019 records that separation.

Public interface and evidence

capture_source takes a borrowed filesystem path and returns an owned SourceManifest through anyhow::Result. Its files vector contains CapturedSourceFile values with a portable relative path, exact owned bytes, lowercase SHA-256 of those bytes, and optional observed permission bits. The manifest also carries its framed SHA-256. Public values derive debugging, cloning and equality; they have no serialization or trusted-provenance wrapper. Callers can alter them, so consumers must validate supplied values.

validate_manifest borrows the manifest without mutation or sorting. For each record it checks the shared ownership lexical-path rule, strict ascending path order and uniqueness, mode range, and exact byte digest, then checks the complete manifest digest. Invalid input returns contextual errors rather than a partial validated result. Ownership uses the same extracted crate-private path predicate at its original validation position with unchanged diagnostics.

Observation and assembly

Capture reconstructs the root from standard path components before metadata inspection. This lexical step removes redundant trailing separators and dot suffixes without canonicalizing the root or resolving its ancestors. Root metadata must identify an existing directory rather than an observed symlink.

A private synchronous ObservationIo boundary supplies directory entries, symlink metadata and file reads. Directory enumeration preserves individual entry results inside an outer enumeration result, so traversal can report both directory-open and iterator errors. Metadata and read errors also propagate with operation and path context; no failure returns a partial manifest. The private fault delegate exercises these paths after an earlier successful file read.

Traversal rejects observed symlinks before following or reading them, validates relative names before either directory recursion or regular-file capture, and rejects other entry types before opening them. It applies no ignore rules or entrypoint exclusions. Hidden files, gitignore-matched files, binary and empty files, nested entrypoints and overrides are captured. Empty directories create no records; invalid directory names still fail validation. Hard-linked regular files produce a record for each path, without a topology guarantee.

Each regular file is read once. Unix permission evidence comes from its observed metadata masked to 0o7777; other hosts record no mode. These bits authorize no permission application or execution. The complete vector sorts by Rust string path ordering, receives its framed digest and passes pure validation before return. AD-0020 records the versioned digest choice; its exact encoding belongs to specification strict captured source manifests.

Integration and evidence limits

Captured bytes let later planning avoid rereading mutable source paths. They do not establish authenticity, an immutable Git revision or a single observation instant. Ordinary synchronous standard-library APIs provide no atomic snapshot, descriptor-relative containment or protection against concurrent replacement. Root ancestors can be symlinks. Lexical path checks do not qualify aliases, case collisions, reserved names or destination filesystem limits.

Legacy hashing and discovery retain their existing runtime behavior and persisted digest meanings. No CLI, lockfile, source-schema or native-client activation is introduced. The separate pure whole-file planner is implemented for explicitly supplied complete file sets. The separate captured item compiler derives complete per-client entrypoint and resource outputs from these captured values. The inactive captured-rule desired-set producer binds complete rule outputs. Lifecycle reconciliation and execution remain later work. The separate selected-source boundary records immutable Git revision evidence. Qualified scope roots, writer locking, destination observation and qualified source observation are implemented separately. Source snapshots retain revalidation evidence around this unchanged plain manifest format. The mixed coordinator now composes complete explicit file/contribution sets from supplied ownership and observations; it does not derive those lifecycle inputs from compiler outputs.

tests/source_manifests.rs pins literal file and manifest vectors, path/content/mode changes, complete inclusion, validation diagnostics and supported platform rejection fixtures. Private module tests pin fault propagation and read counts. Actual non-UTF-8 filesystem-name fixtures are limited to Unix excluding macOS: the development macOS filesystem rejected their creation, so a private pure path-conversion fixture covers rejection there. FIFO and socket tests do not claim device-entry or concurrent-containment qualification. Platform-gated tests describe coverage, not proof that each host has executed them.

Qualified observation companion

The inactive qualified source boundary adds SourceSnapshot, borrowing one qualified scope and exposing this same manifest only by shared reference. It retains complete directory topology and native descriptor evidence for later non-refreshing checks. Its canonical assembly shares the strict source digest implementation; the existing ambient capture_source behavior and freely mutable plain values remain unchanged. This implements source observation only. Hosted qualification, lifecycle effects, permission policy, application and recovery remain separate gates.

Qualified source tree observation

The inactive library boundary is implemented independently of command dispatch. Hosted Linux qualification and the Windows scope/compile boundary passed at a393263 on 2026-10-13. Hosted checks for the eventual final PR head, including the new Windows source-method runtime assertion, remain merge gates. Decision: AD-0030.

Intent

Complete the source-observation prerequisite of the delegated owned-content lifecycle. Existing strict manifests capture exact bytes for pure compilation but use ambient paths and contain freely mutable values. Introduce an inactive, read-only filesystem capability that binds a complete captured tree to an existing qualified scope and can reject later namespace or value changes. Success is independently tested acquisition and immutable revalidation, with no change to current command dispatch, persisted digests, or the legacy capture API.

Selected approach

Reuse the existing qualified scope and native mount profile policy. A separate source snapshot traverses every descendant relative to pinned directory handles, retains complete directory membership and file evidence, and exposes the existing plain SourceManifest by shared borrow. This is selected over ambient recursive rereads, which do not bind entry access to inspected parents, and over filesystem snapshot or probe creation, which would add writes and platform dependencies.

A source scope is selected independently from any destination scope and may be on a different qualified mount. InstallationRoot’s scope label remains caller metadata; selecting a source root grants no ownership or effect authority.

Interface

QualifiedScope::observe_source_tree returns a borrowing SourceSnapshot. SourceSnapshot::manifest borrows its SourceManifest; verify_unchanged returns success only when the captured evidence still agrees. The public interface is defined in the source module.

Export SourceSnapshot from upskill::filesystem. It borrows exactly one QualifiedScope, is opaque and non-Clone, and provides a non-content Debug view. No mutable manifest accessor, public constructor, serialization, write method, writer-lock acquisition, or successful partial capture is provided. Cloning the plain manifest cannot forge or refresh the snapshot’s private evidence. Capture performs a complete revalidation before returning; later checks never refresh captured values or retained baselines. Other operating systems expose the same opaque type and return explicit unsupported errors.

Root and mount binding

Reopen the selected root using QualifiedScope::reopen_directory and verify its binding before and after capture/revalidation. Retain its descriptor identity. Apply the existing descriptor-based Mount::of policy to the root and every opened descendant. Require the selected root mount binding throughout the tree and on later checks. Share the existing native qualification implementation by moving it to a narrowly scoped private filesystem module; do not duplicate its profile policy or weaken destination checks. Keep rustix pinned at 1.1.4. Unknown profiles, missing returned Linux mount-ID evidence, mount changes and unsupported hosts return contextual errors. Source qualification does not require sharing the destination mount and makes no network-filesystem promise.

Complete descriptor-relative enumeration

Enumerate each retained directory using rustix directory iteration. Ignore only the native dot and dot-dot entries. Record and sort every other exact stored name, including hidden and ignored names, empty directories and .git contents. Reject non-UTF-8 or nonportable relative names before recursion/read. Preserve strict source-manifest path order; enumeration order is not semantic.

For every entry, inspect with SYMLINK_NOFOLLOW and open exactly that single stored component relative to its inspected parent with NOFOLLOW and CLOEXEC. Directory opens require DIRECTORY; regular-file opens use NONBLOCK so a replacement FIFO cannot block capture. Compare selected and opened identities and kinds, qualify their mount, and retain the handles. Reject symlinks and special entries without following or reading them. Reject repeated directory identities to avoid directory aliases/cycles. All entry and iterator failures abort without returning a partial manifest.

Regular hard links are allowed as distinct source paths, matching strict capture: source observation authorizes no mutation. Record each path’s exact bytes/mode and link-count stamp; duplicate file identities do not collapse records. This is deliberately different from destination final-file eligibility. Directory membership and identity evidence includes empty directories even though the manifest has no empty-directory records.

Retain each directory’s identity, native mount, exact name set and a stamp covering mode, link count, size and nanosecond mtime/ctime. Require the directory stamp to remain unchanged across its own enumeration and descendant capture. Ignore atime. A conservative rejection after metadata-only changes is permitted.

Stable regular-file acquisition

Inspect the descriptor immediately before reading. Its stamp contains device, inode, complete native mode, link count, size and nanosecond mtime/ctime. Read exact bytes through the descriptor, recheck the stamp, and require byte length to equal nonnegative observed size. Compare pre-open metadata with the opened stamp as well. Capture permission evidence as Some(mode & 0o7777).

A narrow private read delegate on the actual production path supports independent metadata-change, short-read and propagated-error controls. Production directly returns File::read_to_end’s result; no broad filesystem mock, global hook, async runtime or new dependency. Recoverable filesystem/read/descriptor errors retain operation/path context and original causes. Ordinary infallible allocations can abort, and OS termination is outside Result guarantees.

Assemble the manifest through a shared crate-private source_manifest helper that computes the unchanged v1 per-file/framed digests and invokes the existing pure validation. The existing capture_source and validate_manifest signatures, legacy traversal rules, digest bytes and diagnostics remain unchanged.

Revalidation

Verify retained descriptor identities/stamps/mounts and reopen the complete tree from the currently bound root. Compare each directory’s exact membership, path, identity and stamp, plus each file’s path, identity, stamp, bytes and mode against the original evidence. Detect added/removed/renamed files or empty directories, replaced directories with identical descendants, same-byte file replacement, symlinks/special substitutions, mode-only changes and fractional timestamp changes. A changed selected root or ancestor alias binding rejects. Revalidate the root binding again after the complete check. Fresh temporary observations are compared and discarded; never replace original manifest or retained evidence. Repeated verification after one mutation must fail on both calls with captured values unchanged. Checks sample sequentially and provide neither an atomic tree snapshot nor exclusion against external editors or hostile relocation/ABA changes.

Components and verification

The private backend owns the retained Evidence, directory tree, regular-file records, native stamps and exact membership comparison. Shared qualification retains the destination policy. Manifest assembly provides the unchanged v1 framing and pure validation.

Public acceptance tests cover stable complete capture, captured compilation, namespace and value changes, immutable stale checks, rejected entry kinds, permissions, held-writer operation and non-content Debug output. Private tests isolate acquisition stability, counts, errors, identity, membership, exact values, fractional timestamps, qualification wiring, descriptor flags and cleanup.

Limits

This is a sampled read-only source stabilization prerequisite. It does not record an immutable Git revision, establish authenticity, lock external source writers, activate CLI/schema-2, write migration backup/state, establish permission policy, apply files, implement recovery or complete M1-A2/M1-A3/M1. Real privileged mounts, device nodes, physical I/O/exhaustion failures and hostile namespace behavior are unexercised unless separately evidenced. Durable status must remain honest.

Historical execution results are retained in the filesystem validation record.

Applying selected lifecycle rule sources

The opt-in Gate 5 library resolves explicitly selected local or Git rule item roots through lifecycle::sources::ResolvedRuleSource. It retains a qualified source scope and any private temporary checkout. This boundary performs no registration, destination observation, output writes or backup writes. Read-only preview does not use it. Registry discovery remains separate; the source-backed adapter now supplies complete literal selection before resolution.

capture() returns a private association between strict captured bytes and the resolved source metadata. CapturedRuleSource::compile accepts an explicit installation binding, compiler consumers and target profiles, and produces a complete desired set with matching owner contracts. Requested branches, tags and full commit SHAs become the actual immutable checkout commit in installation evidence. This historical compiler supports Claude, Copilot and OpenCode. The explicit CapturedRuleSource::compile_codex path accepts a CodexSourceBinding for exactly Codex CLI contract revision 1 and returns desired instruction contributions, owner contracts and the complete location reader report. Both compilers construct source evidence from the retained capture and revalidate the original tree and actual checkout revision before compilation.

The retained snapshot() can be supplied to later executor revalidation. verify_unchanged() samples the original source tree and checkout revision without refreshing evidence. These checks do not provide an atomic filesystem snapshot or exclusion against external writers. Local final symlinks and Git subfolder escapes are rejected. Git metadata is kept outside the captured work tree, and the temporary checkout lives until the resolver is dropped.

Implementation evidence and the separate permission-selection requirement are recorded in the selected source validation note.

Resolution and retained evidence

Local resolution makes relative paths absolute and canonicalizes only their ancestors, leaving the selected final component for qualified symlink rejection. Git resolution uses synchronous init, shallow fetch and detached checkout with structured command arguments. A private temporary directory owns separate tree and metadata directories. Explicit subfolders must contain only normal relative components and remain inside the checkout after canonicalization. The resolver obtains the immutable revision through HEAD^{commit} and retains the temporary directory until it is dropped.

Each Git subprocess removes the repository-local override set enumerated by git rev-parse --local-env-vars, including index, object, common-directory and repository-scoped config variables. A private coverage test compares that set with the installed Git. Native authentication configuration remains available; the resolver does not inject credentials.

CapturedRuleSource borrows its resolver and opaque qualified source snapshot. Capture checks the Git revision before and after observing the source tree; revalidation checks it around snapshot verification. Compilation revalidates, binds the original manifest hash and resolved commit to the installation, then calls the existing contracted desired-set producer. The borrowed snapshot() accessor supplies source evidence to a later executor without transferring ownership or extending the checkout lifetime.

Permission evidence is not executor policy. Generated rule entrypoints and their contracts carry mode: None; resources retain observed permission bits. A later applying adapter must select modes through files::permissions::select_file_mode using qualified destination evidence and explicit resource execution policy, and keep the desired artifacts and owner contracts consistent before planning.

The implementation lives in src/lifecycle/sources.rs, with public acceptance coverage in tests/lifecycle_sources.rs.

Requested bindings and scoped application

normalized_source_label derives a requested source binding without fetching or capturing the tree. Local labels canonicalize ancestors and preserve the final selected entry for qualification; Git labels retain their requested ref. The resolved commit remains separate evidence. The orchestration adapter uses this label to reject reuse of an installed alias by a different binding before source resolution.

The source-backed adapter retains every resolver, temporary checkout and full CapturedRuleSource through a scoped prepared-operation callback. Immediately before consuming the captured command operation, it verifies the original tree and actual Git revision for every capture. The opaque preparation accepts no replacement source or compiled output. This connects compiler-produced rule sets to existing execution without changing preview or production dispatch.

Explicit Codex source integration

lifecycle::orchestration::with_prepared_codex resolves selected rule roots using CodexSourceOptions: target precedence inputs, add defaults, one explicit profile and resource execution policy. The effective selection must be exactly [CodexCli], and the profile must be Codex CLI revision 1. Unsupported profile or selection returns an Instructions CapabilityError before any source resolution. Recovery and recorded removal run before adapter validation.

The destination follows the declared installation root: project AGENTS.md or user .codex/AGENTS.md. Qualified access to a declared user root does not identify real HOME/CODEX_HOME or certify native loading. Generic project rules must agree across all five declared readers; global rules have two Codex readers and may use a Codex-only audience. Audience-excluded rules retain source history without output and still expose a complete NotEvaluated reader report.

SourcePrepared::instruction_readers() borrows reports sorted by installation identity. Reports are presentation data: only desired sets, owner contracts and direct references enter the captured upsert. The callback may clone the reports, discard preparation or consume apply(). Historical and remove preparations have no newly compiled reports. All captures, including actual Git revisions, are checked again immediately before application without refreshing evidence.

The separate opt-in route rejects unsupported retained representations and cross-adapter conversion before resolving any selected source. Retained owners must match the declared session root in both scope and raw path spelling; canonical instruction destinations derive from that declared root. Production commands, schema 1 and client defaults remain unchanged. Independently observable selected-set failures are collected by the library-only complete preflight reporting contract, including compiled reader reports on blocked outcomes. A failed source supplies no speculative downstream diagnostics, and a blocked request cannot invoke the prepared callback or apply a successful subset. Native qualification, all-kind adapters and production activation remain later gates. Acceptance examples are in lifecycle_codex_sources.rs and lifecycle_codex_orchestration.rs.

Edit the canonical source.

State and ownership

Architecture reconciliation (2026-10-25): the delivery, discovery and activation contract controls intended production routes and reader intent. Existing Codex AGENTS.md contribution APIs described here are experimental library boundaries, not production independent-rule delivery. Independent Codex/OpenCode rules remain beside unchanged team guidance through a selected native or hook route. The native-first amendment permits explicit OpenCode paths after Q5 PASS and selects the trusted Codex hook with its declared recorded-history limitation. Reusable ownership, preflight and recovery mechanisms do not select or qualify a runtime route, and schema-2 production activation remains blocked (AD-0040 amendment).

This concern owns the requirements and implemented architecture below. Moving these records does not change their implementation or approval status. Library-only capabilities remain separate from production CLI activation; each section records its own limits. Accepted decisions remain in ADRs.

Requirements

Pure legacy-state projection

The internal library boundary legacy::project_legacy(&Lockfile) describes typed schema-1 state without I/O or mutation. It retains an owned snapshot and indexed item and bundle rows, including order, duplicates, installed aliases, source names, recorded refs and SSOT hashes, dependency/group links, and raw client strings. Explicitly tagged plugin and MCP rows preserve their original targets, scopes, statuses and bundle references. Typed preservation does not recover unknown JSON fields or the original serialization bytes.

Persisted source labels are decoded with the existing compatibility parser. A parsed source or recorded ref/hash does not prove an immutable source revision or ownership of generated bytes. Requested selection remains explicitly unresolved for every item, including singleton effective client lists. Empty client lists expand to the fixed historical Claude, Copilot and OpenCode clients; unknown spellings remain in the snapshot and produce structured diagnostics.

Candidates contain only a historical client, a relative entrypoint and a relative resource search base. The frozen nine kind/client layouts are shared with the current pipeline and use the installed alias. They do not inspect roots, files or resources, establish ownership, or authorize adoption or deletion. Bundle names and ancillary files do not produce candidates. Invalid installed names suppress candidates: empty names, dot, dot-dot, slash, backslash, colon and control characters are invalid; spaces and Unicode remain representable. Invalid sources retain otherwise known layout facts. Duplicate identities and identical entrypoints identify all affected item indices without selecting a winner. Diagnostics retain malformed rows for inspection. Unsupported schema values retain all typed records but produce no interpreted sources, clients or destinations.

This prerequisite leaves commands and schema-1 persistence unchanged. Verified sources, reproducible historical rendering, resource enumeration, adoption, requested-selection reconciliation, schema-2 lifecycle preservation, exact backup, containment, preflight, writer locking and recovery remain later work.

See the projection design and AD-0015 for implementation boundaries and rationale.

Combined lifecycle state foundation

The opt-in lifecycle::state library validates a schema-2 installation and reference graph without I/O. Direct references root the graph; bundle members and dependencies must resolve, be reachable and contain no dependency cycle. Installations retain requested surfaces even when no artifact is generated. Duplicate identities and inconsistent source revision evidence are rejected. Artifact owners and roots must match authoritative installations. Explicit per-owner consumer associations must match every artifact selector exactly; their union must equal the artifact consumers. The strict schema-2 codec validates before parsing results are returned and before serialization. It emits deterministic pretty JSON with one trailing newline without mutating input. Typed schema-1 projection creates no owners and preserves historical rows, labels and statuses in their original order; exact JSON bytes and unknown legacy fields require the later migration backup.

This library does not change lockfile::CURRENT_SCHEMA, command dispatch or production lockfiles. It does not establish filesystem ownership or native support. See combined lifecycle state.

Per-owner recorded contract evidence

The opt-in combined state accepts optional explicit rule-owner contracts. Missing historical evidence stays unknown, is omitted when empty and gains no inferred revision; schema-1 projection produces no contracts. Each row binds exact scope, raw root, path, optional contribution selector and installation ID to captured source evidence, normalized artifact content and complete consumer/revision pairs. File content includes hash and optional mode; instruction content includes its body hash without aggregate whitespace bookkeeping.

Every present row must match an authoritative owner-consumer association, installation source evidence and owned artifact. Duplicate/orphan identities, inexact bindings or consumer coverage, duplicate/empty profiles and zero revisions reject. Unknown serialized fields reject. Deterministic encoding sorts contracts and profiles without changing the supplied state.

Agreement validates the whole state and selects an exact artifact identity. Unknown identities error. It reports missing evidence and source/profile conflicts in artifact-owner order. Matching requires all owners’ full source manifest digests and complete surface/revision maps to agree; profile vector order is immaterial. Equal bytes and differing consumer subsets do not suffice.

Upsert receives the complete explicit contract subset for changed installations; omitted changed evidence becomes unknown. Untouched rows survive exactly, replacement identities preserve relative order among surviving rows and new rows append in identity order. Evidence for unchanged installations rejects. Complete resulting state validation precedes successful planning. Empty desired outputs and reference graph retirement remove only associated retired evidence; surviving shared owners retain their contracts.

This inactive supplied-value boundary grants no shared-mutation, native, filesystem or source-authenticity authority. The original upsert and public mixed planner retain affected multi-owner rejection; explicit shared-output reconciliation consumes agreement through a separate entrypoint. Production commands and schema 1 are unchanged. Combined lifecycle delivery evidence is recorded in the Gate 5 acceptance note. See the owner-contract design and AD-0034.

Architecture

Combined lifecycle state

The opt-in lifecycle::state library describes schema-2 state for the future owned-content lifecycle. Current commands still use schema 1. These records do not migrate state, fetch sources, claim ownership or write output files. The combined lifecycle state foundation owns the requirements; lifecycle::state exposes the records and public functions.

Public records and validation

CombinedState holds the schema number, installations, bundles, references, owned artifacts, per-owner consumers, optional owner contracts, optional origin evidence and an optional typed legacy snapshot. An Installation reuses ArtifactOwner and InstallationRoot, and records its complete requested target surfaces and SourceEvidence. A BundleNode records its ID, source, name and optional source revision. Reference distinguishes direct installations, direct bundles, bundle membership and bundle dependencies. OwnerConsumers binds an installation ID to a root, relative path, optional contribution selector and explicit consumers. Existing ownership, target and schema-1 types retain their wire representations. No new dependency is required.

validate_state checks the complete envelope and reuses ownership::validate_artifacts for artifact shape. associations supplies the internal owner/selector checks within that same public validation boundary. Parsing and serialization both call this validator. Inputs remain unchanged on rejection.

Reference authority

Installations bind an explicit ID to source, item kind, source name, installed alias, root, requested surfaces and captured source-manifest identity. Git records require their resolved revision to match the owner binding; local records have no revision. These recorded strings do not prove source authenticity. Manifest digests must contain exactly 64 lowercase hexadecimal characters; installation sources and names are nonempty. Roots must be absolute, and requested surfaces must be nonempty and unique. Git evidence requires a nonempty revision; an optional bundle revision cannot be empty.

Node IDs are caller supplied, nonempty ASCII alphanumeric, hyphen or underscore strings, unique within each node kind. Bundle and installation namespaces are typed, so the same string may occur in both. Identity allocation for lifecycle operations remains later work.

Direct installation and bundle references are roots of a directed graph. Bundle membership and dependencies retain existing installations without duplicating artifacts. Validation rejects duplicate IDs and edges, missing endpoints, bundle cycles and unreachable nodes. An iterative topological traversal rejects bundle cycles without recursive graph traversal, and traversal from direct references establishes reachability. Installations with no generated output remain valid when referenced, for example after audience exclusion.

Distinct IDs cannot describe the same source/kind/name/alias installation under the same exact root. Requested surfaces and resolved revisions may change on update without changing that identity. Root comparisons are lexical and retain raw spellings; they establish no filesystem equivalence or containment.

The combined graph is the single authority for new installation bindings and references. Reusing existing ownership and legacy types preserves their reviewed contracts. Independent writable sidecars and automatic legacy adoption were rejected because they would separate authority or invent ownership evidence.

Delivery boundary

This foundation is separate from command activation, source resolution, compiler reconciliation, migration backup, application and recovery. Gate 5 requires all lifecycle slices; Gate 6 supplies target qualification and activation.

Owners and consumers

Every artifact owner must exactly match its authoritative installation, including revision and alias. Artifact roots must match the installation scope and raw root spelling. Existing artifact validation still rejects ambiguous whole-file/contribution locations and malformed content metadata.

Each artifact selector and owner has exactly one explicit consumer association. Whole files use no contribution selector; Markdown regions use their recorded contribution ID. Associations must name existing owners, contain unique nonempty surfaces within that owner’s requested selection, and have no surplus entries. The union of these associations equals the artifact consumer set exactly. Validation never infers individual consumers from that union. Optional owner contracts bind explicit source, normalized content and revision evidence to these associations; absent rows stay unknown.

Codec and retained legacy data

parse_state and serialize_state both validate the complete envelope. Schema-2 records reject unknown fields. Encoding sorts owned graph records, selectors, owners and consumer lists in a cloned value and emits pretty JSON with one trailing newline. It does not change the supplied state.

project_legacy requires schema 1 and retains its typed Lockfile snapshot in legacy, with an empty owned graph. Legacy arrays, duplicate rows, labels, recorded refs, optional fields and integration statuses keep their original values and order. They are unresolved historical data, never ownership evidence. Unknown original JSON fields and serialization bytes are outside this typed projection. The applying lifecycle must preserve them in an exact first-write backup; this pure slice does not create that backup. The text codec accepts UTF-8 &str; future byte-loading callers must reject invalid UTF-8 explicitly.

An empty envelope has schema: 2, empty installations, bundles, references, artifacts and owner_consumers arrays, and legacy: null. New references are tagged as direct-installation, direct-bundle, bundle-installation or bundle-dependency; source evidence is tagged git or local. Legacy fields keep their existing schema-1 names rather than being normalized to new tags.

Verification boundary

tests/lifecycle_state.rs exercises graph sharing, every reference kind, reachability and cycles, identity collisions, source revision/hash bindings, audience-excluded installations, exact owner/consumer associations, lexical roots and selectors, strict decoding, deterministic owned ordering and typed legacy retention. Independent controls omit validation or sorting checks and require assertion failures after each omission, followed by exact restoration. The validation record records the checked revision and scope of the evidence.

This completes the pure state slice. Separate captured desired-set production, reference reconciliation and shared-owner removal planning are implemented. Complete single-owner upsert planning is also implemented. A separate explicit shared upsert now reconciles supplied owner contracts and observations. The opt-in guarded application boundary connects complete artifact plans and combined state to checkpointed execution, including the exact original-state backup and recovery path. Observational lifecycle adapters, native and complete lifecycle qualification remain required; this record does not declare Gate 5 or M1 complete.

Per-owner recorded contract evidence

The inactive lifecycle::contracts boundary records supplied contract evidence inside authoritative CombinedState. It supports rule installations and exposes pure validation and agreement checking. Production commands and schema-1 lockfiles do not consume it. It adds no dependencies or native effects.

Representation and validation

CombinedState.owner_contracts is an optional vector, defaulting to empty and omitted from serialization when empty. Historical absence stays unknown; schema-1 projection creates no contracts. OwnerConsumers continues to describe consumer associations without implying adapter revisions or contract evidence.

An OwnerContract binds exact scope, raw root spelling, path, optional instruction contribution selector and installation ID to a captured source-manifest digest, normalized content and explicit ConsumerContract surface/revision pairs. Families follow stable target surfaces. File content includes its recorded hash and optional mode. Instruction content includes the body hash and excludes aggregate whitespace bookkeeping, allowing preserved manual bytes or removal of neighboring contributions without invalidating an untouched body’s evidence.

Whole-state validation checks every present row against an authoritative owner-consumer association, installation source evidence and owned artifact. It rejects duplicate identities, orphan evidence, changed roots/selectors, inexact source/content bindings, empty or duplicate profiles, inexact consumer coverage and zero revisions. Rows need not cover every owner. Strict unknown-field rejection remains. Serialization clones the state, sorts rows by scope/raw root/path/selector/installation identity and profiles by surface, and leaves the input unchanged.

Agreement boundary

check_contract_agreement validates the complete state, then locates an artifact by exact supplied root, path and contribution selector. An unknown identity is an error. It walks artifact owners in their recorded order, reports every owner missing evidence, and compares evidenced owners with the first evidenced owner. Source-manifest and target-profile conflicts are separate diagnostics and can both occur for an owner. Profile vector order is immaterial.

Matching requires each owner to have evidence, identical full captured-manifest digests and identical complete surface/revision maps. Normalized content already matches the artifact through validation. Different consumer subsets block even when output bytes match. This supplied agreement establishes no native behavior, source authenticity, physical identity, new ownership or execution authority.

Lifecycle propagation

UpsertRequest.contracts supplies the complete explicit evidence subset for changed installations. Omitted changed rows become unknown. Untouched rows retain their values and relative order; replacements occupy corresponding surviving positions after removals. New rows append in identity order. Supplied evidence for unchanged installations and duplicate rows reject. The complete resulting state is validated before a plan is returned, so stale or invalid proof exposes no partial plan.

Empty desired sets remove output evidence while retaining referenced installation history. Graph retirement removes retired installations’ contracts. Reference removal preserves surviving shared owners’ contracts and removes retired owners’ contracts, including instruction contributions and final empty aggregates.

The original plan_upsert and public mixed-planner sharing rejections remain intact. The separate shared upsert consumes agreement for affected desired final multi-owner artifacts. Complete selected recompilation can replace unknown history; unknown unselected proof is never inferred. Agreement alone grants no shared-mutation or execution authority. This supplied-value path does not integrate qualified source/destination observations, complete Gate 5 or qualify native clients.

Components

Pure owner release with no desired value at an identity preserves untouched survivors and their unknown evidence without requiring agreement. Supplied physical observations and all physical checks still apply.

Pure legacy-state projection

The public library boundary legacy::project_legacy accepts a borrowed typed Lockfile and returns an owned LegacyProjection. It has no scope-root input, I/O, state writer or CLI dispatch. AD-0015 records the choice of this intermediate boundary; the specification owns its requirements.

Snapshot and interpreted rows

The snapshot is an exact typed clone, including original vector order and every current field. It does not preserve unknown JSON fields or serialization bytes. Items and bundles retain their original index and cloned record. Plugins precede MCP entries in a tagged integration vector; each integration index addresses its original plugin or MCP vector. Native targets, scopes and statuses remain facts, without runtime verification or permission to operate on the target.

Unsupported schema values retain all these records, produce a global diagnostic, and return without interpreting sources, clients or destinations. For schema 1, source decoding uses parse_install_source_label; parse errors retain the raw record and actionable parser message. Successful parsing does not verify source availability, a revision or generated bytes.

Every item has RequestedSelection::Unresolved. Effective generation clients cannot recover requested families lost through author audience filtering, and a Copilot client does not distinguish CLI from VS Code Local. Integration targets remain separate from requested content selection.

Historical clients and layout

legacy::layout fixes the historical client set to Claude, Copilot and OpenCode. An empty recorded list expands to those three in that order. Explicit known clients retain first-occurrence order and produce one candidate each; unknown spellings produce diagnostics and remain in the cloned record. An unknown-only list produces no candidates.

The nine kind/client path mappings use the installed alias. Current pipeline::output wrappers delegate to the same native PathBuf helpers through explicit current-client conversion. Those helpers retain raw-name behavior for existing pipeline callers. Only the projector validates names before converting paths to forward-slash strings: empty names, dot, dot-dot, slash, backslash, colon and control characters suppress candidates; spaces and Unicode remain representable.

Each candidate carries a client, relative entrypoint and relative resource search base. Skills and OpenCode rules are directory-backed; flat entrypoints use a sibling name directory for resources. These paths are relative to a later caller’s established legacy scope root. They do not establish existence, containment or ownership, enumerate resources or permit recursive adoption. Malformed sources do not erase otherwise known layout facts. Bundles and ancillary files have no candidates.

Diagnostics and later consumers

Diagnostics are structured data located at a global, item or bundle position. The projector continues across malformed rows. Separate borrowed-key groups collect duplicate (kind, name) identities and identical entrypoint candidates across distinct rows, retaining every affected index. Collision locations anchor at the first affected row; no record wins or disappears. Disjoint client lists can duplicate an identity without sharing an entrypoint. Resource-tree overlap analysis requires later verified resource enumeration.

The projection has no serialization contract, ownership record, artifact hash, mode, source-verification result or generic operation-blocking flag. Later planners determine which unresolved facts an operation requires. Source verification, historical rendering, resource enumeration, adoption, requested selection reconciliation, schema-2 lifecycle preservation, exact backup and transactional execution remain outside this boundary.

tests/legacy_projection.rs pins typed preservation, all nine literal layouts, client order and candidate identity, source compatibility, malformed and duplicate rows, unsupported schemas and root-independent paths. Existing pipeline output and resource tests cover the unchanged wrappers. These checks do not establish native Windows execution, native discovery or actual artifact ownership.

Durable lifecycle origin

Requirements

Later writes must preserve initial-state origin and retained typed legacy history. Fresh initialization requires absent state and backup and creates private state without inventing historical bytes. Migration binds the exact original schema-1 bytes; reopened writes reject changed backup evidence. Missing historical provenance remains unknown and blocks bound application.

Architecture

Combined state optionally carries a strict origin record. fresh identifies an originally absent lockfile and requires no legacy snapshot. migrated requires a retained schema-1 snapshot and binds the original exact backup bytes with backup_sha256 and backup_length. The constructor StateOrigin::from_schema1_bytes hashes original bytes, including whitespace and unknown schema-1 fields. Existing unbound internal schema-2 records remain valid for inspection; an applying adapter cannot infer original provenance from them.

Origin survives complete upsert and reference-retirement planning. The bound application boundary preserves it across writes, verifies the canonical backup against that durable evidence and refuses changed origin or legacy history. Fresh initialization has no fabricated schema-1 backup. See AD-0038 and the origin validation record.

Edit the canonical source.

Lifecycle planning

Architecture reconciliation (2026-10-25): the delivery, discovery and activation contract controls intended production routes and reader intent. Existing Codex AGENTS.md contribution APIs described here are experimental library boundaries, not production independent-rule delivery. Independent Codex/OpenCode rules remain beside unchanged team guidance through a selected native or hook route. The native-first amendment permits explicit OpenCode paths after Q5 PASS and selects the trusted Codex hook with its declared recorded-history limitation. Reusable ownership, preflight and recovery mechanisms do not select or qualify a runtime route, and schema-2 production activation remains blocked (AD-0040 amendment).

This concern owns the requirements and implemented architecture below. Moving these records does not change their implementation or approval status. Library-only capabilities remain separate from production CLI activation; each section records its own limits. Accepted decisions remain in ADRs.

Requirements

Authoritative lifecycle reference reconciliation

The inactive lifecycle::reconcile::reconcile_references API accepts validated combined state and explicit installation/bundle upserts plus reference edits. It returns graph records and removed IDs without I/O or input mutation. Only actual direct roots and transitive bundle edges retain nodes; empty output or artifact/consumer count does not remove a referenced installation.

All supplied nodes and edges are validated before iterative pruning, including unreachable dangling edges and cycles. Explicit upserts and added references must survive reconciliation; otherwise the request is rejected. Duplicate, absent, already-existing or overlapping reference edits conflict. Existing installation IDs cannot change source/kind/name/alias or scope/raw root; existing bundle IDs retain source/name. Valid evidence/revision and requested surface updates remain subject to later complete artifact planning.

Surviving rows retain order; new nodes and references append in ID/enum order. Removed IDs follow prior order. Graph results contain neither artifacts nor the legacy snapshot and do not authorize deletion or state persistence. Later reconciliation must preserve tagged legacy data and unrelated owners, then validate the complete final combined state. No CLI or schema-1 behavior changes. See lifecycle reference reconciliation.

Reference-driven shared-owner removal planning

The inactive lifecycle::retire::plan_reference_removal API accepts complete combined state, exact reference removals and supplied file observations. It reconciles graph retention first, then retires rule installations whose final reference ended. Unsupported owned kinds cannot retire through this boundary; unselected kinds and tagged legacy rows remain unchanged.

Surviving artifact owners retain their order. Consumers are recomputed from surviving per-owner associations. Every affected physical location requires one exact observation, including metadata-only changes. Invalid, duplicate, missing or extraneous evidence and affected topology overlaps conflict. Whole-file hash/mode changes and edited or malformed owned instruction regions conflict before returning a plan.

Whole files remain unchanged while an owner survives and are individually removed after final-owner retirement. Each affected aggregate is edited once, retaining manual bytes, surviving bodies and the observed valid mode. Empty aggregates remain present. Region metadata is updated and the complete final state is validated. Referenced empty installations survive; empty retired installations need no file action.

This pure boundary grants no qualified execution or native compatibility claim. Add/update sharing is a separate opt-in supplied-value boundary in explicit shared-output reconciliation. The opt-in guarded application boundary now covers first and subsequent schema-2 writes, exact schema-1 backup validation, captured-source freshness, and recovery through the qualified executor journal. The opt-in source-backed rule adapter resolves explicit local/Git sources. Production commands and native qualification remain later work.

Captured lifecycle selection preflight

The inactive lifecycle::selection::select_owned_nodes library boundary validates complete supplied combined state, then resolves all-record or literal-name selection. Names match installed aliases, bundle names and historical native names/declaring bundle labels. Selected owned bundles include their complete reference-graph dependency and installation closure, retaining prior node order.

Every selected legacy row, unsupported owned kind, unknown name or ambiguous name blocks the complete selection. Unselected malformed history survives and does not block explicitly disjoint owned work. Native history declaring any selected owned bundle name blocks, including transitively selected bundles. Duplicate legacy rows retain separate category/index diagnostics. Named requests must contain nonempty unique names and must not be empty.

Ready results contain owned installation/bundle identifiers. Blocked results contain descriptive candidates and all typed diagnostics; no subset is ready to apply. Name diagnostics follow lexical order, legacy diagnostics follow original category/index order, and unsupported kinds follow prior installation order. The boundary performs no I/O, state mutation, ownership adoption, reference edit or output planning. Source/scoped selection and CLI precedence are not represented. It neither changes production schema 1 commands nor establishes native support. See lifecycle selection.

Complete single-owner lifecycle upsert planning

The opt-in plan_upsert coordinator accepts complete desired installation sets, explicit prior selection, exact bundle/reference edits and supplied file observations. It reconciles graph retention before planning mixed owned outputs and validates the complete resulting state. Every selected surviving installation requires a complete set; existing changes and graph retirements require selection.

Empty audience-filtered sets retire outputs while retaining referenced installation/source/requested-surface history. Desired roots must match the installation exactly. Output consumers and disjoint exclusions must exactly account for requested surfaces. Changed owner associations are rebuilt from the complete sets; untouched ownership, references and tagged legacy rows survive.

Selection and new-name legacy/native blockers expose no successful subset. Known frozen legacy entrypoint/resource namespace collisions block even when observed files are absent. Graph/artifact errors return no partial plan. All affected destinations, including no-op bytes, require exact observations. Affected shared artifacts and new sharing remain unsupported; equal bytes do not establish an adapter contract. Manual aggregate bytes and observed modes retain the mixed planner’s preservation contract.

This inactive pure boundary provides no source authenticity, qualified filesystem or native authority. The separate shared entrypoint is specified in explicit shared-output reconciliation. This pure API retains first-error results; the opt-in session layer supplies complete selected-set reporting. Production CLI precedence remains pending. Opt-in inspection and preview provide observational adapters. Guarded backup, application and recovery are available through the opt-in application boundary. Production schema and commands stay unchanged. See the lifecycle upsert design.

Explicit shared-output reconciliation

The inactive compile_rule_desired_set_with_contracts producer accepts explicit target profiles covering exactly the installation’s requested surfaces. Profiles are unique, nonzero and match their target families. It reuses captured rule compilation and returns the desired set plus exact per-artifact contracts binding source manifest, normalized content and only that artifact’s consumers to the supplied revisions. Audience exclusions produce no artifact contracts. It never fills revisions from defaults. Original producer and batch behavior is unchanged. The separate bounded Codex instruction producer now returns instruction contributions, exact owner contracts and a reader report for captured unconditional rules. The explicit Codex source route now connects that producer to guarded orchestration; production command activation remains separate work.

The inactive plan_shared_upsert entrypoint uses the same complete desired sets, selection, graph authority, legacy checks and evidence propagation as plan_upsert. Exact scope, raw root, path and optional contribution selector identify an artifact. Surviving artifacts and owners retain relative order; new identities append in key order. Immutable real-owner bindings remain enforced. Conflicting desired bytes, modes or instruction bodies reject. Untouched owners cannot implicitly acquire changed content or inferred proof.

Every affected artifact receiving desired output with multiple final owners requires complete valid contracts with exact source-manifest and complete surface/revision-map agreement. Unknown unselected evidence blocks; complete newly supplied evidence may replace unknown selected history. Single-owner plans require no invented evidence. Retirement removes released owners’ evidence while retaining surviving history. Pure owner release with no desired value at the identity preserves surviving content, associations and evidence unchanged, without requiring missing contracts, even when multiple owners remain.

Private per-artifact projection reuses the mixed physical planner after real ownership and agreement validation. Exact supplied observations are required for all affected locations, including no-ops. Unknown dedicated files and unmanaged markers cannot be adopted. Instruction changes preserve manual bytes and observed modes; unchanged shared neighbors may retain unknown body proof while aggregate layout metadata refreshes. Final contribution removal keeps an empty aggregate. Only reconciled content metadata maps back onto authoritative real owners; internal projection identities never escape to state, contracts or references or error diagnostics. Physical errors name authoritative real owners and preserve actionable location/cause text. Complete final validation precedes return and failures expose no partial plan.

Inputs remain unchanged. This supplied-value path grants no filesystem containment, source authenticity, native behavior or execution authority. Production commands, schema 1, original upsert and public mixed-planner sharing rejections remain unchanged. Guarded application and executor recovery are available through an opt-in library boundary; command activation, client adapters and native qualification remain later work. See the shared upsert design.

Combined owned-content lifecycle contract

The opt-in library lifecycle creates ownership only through explicit new installation. Fresh and migrated add, update and remove preserve unrelated owned references and indexed tagged legacy/native rows without adopting legacy outputs. Unsupported or malformed retained rows remain visible. Selection that requires unsupported or ambiguous legacy work stops before effects; explicitly disjoint owned work can proceed.

Complete selection and planning precede application. Changes to affected owned whole files or contribution regions conflict without partial application. Instruction edits retain manual bytes and untouched contributions. Removal changes only unchanged individually enumerated owned files whose final reference has ended; extra resources and unowned directories survive.

List, doctor and preview remain observational: they preserve exact state bytes and create no migration, backup, journal or output changes. Applying operations reject changed state, source or destination evidence and require the matching qualified scope writer guard. A competing cooperating writer cannot apply in that scope concurrently. Backup and interruption requirements are recorded in combined lifecycle backup and recovery.

The source-backed rule adapter and supplied instruction adapter exercise this contract without activating production commands. Production writes remain schema 1 until the Codex adapter and selector gate is complete. Local acceptance coverage and its limits are recorded in the Gate 5 acceptance matrix. The combined implementation passed local full just verify, explicit Rust 1.85 checking and an independent audit of all ten acceptance criteria. Final delivery review, hosted CI and merge remain delivery requirements; this contract does not declare Gate 5 delivered.

Architecture

Combined lifecycle integration

The library connects one authoritative combined-state envelope, reference graph and owner contracts to captured compilation, complete desired sets and the mixed owned-artifact planner. It introduces neither an ownership sidecar nor a second writer or recovery protocol. The typed indexed legacy projection preserves each original row and source label; unknown original JSON fields remain in the exact schema-1 backup rather than the typed projection.

For applying requests, the caller acquires the qualified scope writer guard. The captured session recovers any pending executor operation before source work; every recovery report stops the new request. Complete selection preflight then rejects unsupported selected work before resolution. Explicit rule-source orchestration retains local/Git captures, actual commits, compiler bindings and owner contracts throughout scoped preparation and consuming application. Explicit Codex instruction sources now use the same guarded orchestration engine through with_prepared_codex. Caller-supplied contributions remain available through the captured adapter. Both paths observe affected destinations, plan the complete operation and revalidate exact state, source and destination evidence before applying through the same origin-bound qualified executor.

Migrated origin binds the original schema-1 bytes to a restricted create-only backup. Fresh origin requires absent state and backup without creating imaginary history. Ordered executor actions persist file effects followed by combined state; recovery uses those persisted actions without fetching or recompiling sources. Inspection and captured preview are separate observational boundaries and never recover, fetch, migrate or write. See the component interfaces below and origin-bound application.

The acceptance evidence maps all ten criteria to executable examples, including 162 process interruptions and two late-edit reconciliation cases. Accounted staging residue remains explicit debt #316; this evidence does not prove hardware power-loss durability or native client discovery. Production command dispatch and CURRENT_SCHEMA = 1 stay unchanged. Gate 6 must qualify the independent Codex/OpenCode plugin rule routes and selectors, all customization reader/isolation controls and compatible commands before production activation; the experimental contribution adapter is not the production route.

Satisfies: combined owned-content lifecycle contract.

Captured lifecycle selection preflight

The opt-in lifecycle::selection::select_owned_nodes library boundary accepts borrowed validated CombinedState and Selection::All or Selection::Names. It returns SelectionOutcome::Ready(SelectedNodes) or Blocked with descriptive candidates and typed diagnostics. Invalid entire state or malformed requests return errors. It performs no I/O or input mutation.

Literal selection and graph closure

Names match installed aliases, owned/legacy bundle names, and legacy plugin/MCP names or declaring bundle labels. Matching is literal: source names, source labels, scoped identities, glob patterns and CLI syntax are not interpreted. A request must contain nonempty unique names and must not be empty. Unknown names and names matching multiple rows block instead of choosing a first match. One native row counts once even when its own and bundle names both match.

Selecting an owned bundle includes its authoritative dependency and installation closure. Shared dependency paths are visited once. Node results retain prior installation/bundle order, independently of request or traversal order. Selecting an installation alone does not select its parent bundles or other members.

Complete blockers and preserved history

All selection includes every owned node and typed legacy item, bundle, plugin and MCP row. Every selected legacy row blocks, including duplicate, malformed, unsupported or apparently identical rows. Legacy indices address the unchanged original vectors; no source label is reparsed and no ownership is inferred. Historical native rows declaring a selected owned bundle name also block, including bundles reached transitively. This conservative label association is not proof of native identity or an instruction to apply historical native effects.

Unselected malformed legacy history does not block explicitly disjoint work. Only selected Rule installations pass the owned-kind check; selected Skill or Agent installations block even with no artifacts. Unsupported descendants of a selected owned bundle block too. All blockers are collected before returning; Blocked candidates never identify an independently ready subset.

Name diagnostics are in lexical requested-name order, followed by selected legacy items/bundles/plugins/MCPs in their original category/index order, then unsupported installations in prior order. Candidates include all matching owned nodes and their bundle closure even when other selection work blocks.

Boundary and later integration

Ready means this selection preflight passed. It establishes no source authenticity, filesystem containment, native support, compatible sharing, or authority to remove references or apply outputs. Opt-in orchestration enforces this outcome and connects complete desired-set/reference planning, qualified observation and execution. Its target precedence is nonempty invocation, nonempty project configuration, nonempty global configuration, then recorded update targets or caller-supplied add defaults. Empty lists do not override the fallback. Production source/scope selectors and CLI selection behavior remain unchanged. Production schema remains 1. The captured command adapter now connects this preflight to guarded application and executor recovery. Local full Gate 5 acceptance is recorded in the acceptance matrix; final delivery, M1 activation and Codex qualification remain pending.

Components

Satisfies: captured lifecycle selection preflight.

Lifecycle reference reconciliation

lifecycle::reconcile::reconcile_references is an inactive pure graph boundary. It consumes a validated CombinedState and explicit GraphChanges, and returns ReconciledGraph. It performs no I/O, source fetch, ownership adoption, migration, backup, application or schema-2 production write.

Interfaces and components

The public reconciliation module exposes GraphChanges, which carries node upserts and exact edge edits, and ReconciledGraph, which carries surviving graph records and removed IDs. The private reachability traversal indexes bundle dependencies and memberships and follows direct roots using an iterative queue. It keeps installation and bundle reachability separate even when IDs have the same spelling.

The boundary reuses the combined-state validator and whole-file binding checks. Validation-only graph values omit artifacts, consumer associations and legacy state after the complete prior state has been validated. Those temporary values do not replace the complete state or grant authority to apply effects.

Authority and retention

Only DirectInstallation, DirectBundle, BundleInstallation and BundleDependency edges establish retention. An installation survives while any direct or transitive bundle path reaches it. Removing one root of a diamond retains shared dependencies reached through another root. Artifact count, audience-filtered empty output, consumers and matching source labels are not retention references. Bundle and installation IDs remain separate namespaces.

The result contains installations, bundles, references and removed IDs. It contains no artifacts or tagged legacy snapshot and is not a final combined state. A removed installation ID does not authorize deleting its files. Later lifecycle planning must reconcile complete ownership, retain unrelated owners and legacy data, and validate the complete final state before effects.

Explicit edits

GraphChanges supplies installation/bundle upserts and exact reference additions and removals. Duplicate upserts/edits, adding an existing reference, removing an absent reference or adding/removing the same reference in one request conflict. An empty changes value is a no-op; this exact edit API does not infer intent from an incomplete replacement reference list.

Existing installation IDs retain source, kind, source name, installed alias, scope and raw root spelling. Even a spelling-only root change conflicts. Requested surfaces and captured evidence/revision may change subject to state validation; later artifact planning must establish matching complete outputs. Existing bundle IDs retain source and name, while revision evidence may change. New IDs cannot bypass installation identity uniqueness.

Every supplied node and edge is validated before reachability pruning. Private validation-only direct roots allow the existing state validator to check all candidate nodes, dangling edges and cycles without treating intentional detaches as unreachable-state errors. Those roots never escape or participate in actual retention. Explicitly upserted nodes and added references must survive in the result; conflicting update-and-final-detach requests fail rather than silently discard updates.

Iterative traversal uses indexed dependency and membership edges. Unreachable prior nodes and their outgoing edges are removed. Surviving prior nodes and references retain order; new nodes append in lexical ID order and new references in enum order. Removed ID lists follow prior record order. Final graph validation runs again without validation-only roots. Inputs remain unchanged on success and failure; there is no successful partial result after an error.

Remaining integration

The complete prior state is validated, including artifact ownership/consumer associations and tagged legacy schema. Graph reconciliation never mutates those prior values and does not reconstruct them in its graph-only result.

The separate removal planner reconciles shared-owner retirement. The pure selection preflight aggregates selected legacy and native conflicts for all/literal-name requests. The pure single-owner upsert coordinator now connects complete desired sets and selected graph changes to mixed-artifact planning. The separate explicit shared upsert now consumes recorded contracts and supplied observations. Source/scoped selectors and CLI precedence and complete command lifecycle integration remain task 2b work. Codex rule source integration now has an explicit library route. Existing mixed and instruction planners keep their affected multi-owner rejection. Guarded lifecycle backup/application/recovery and observational adapters are available through separate library boundaries; complete command integration and native activation remain separate gates. This boundary alone does not complete task 2b, Gate 5, M1 or the updater prerequisite.

Trace

Requirements are recorded in authoritative lifecycle reference reconciliation. The public acceptance suite establishes retention, edit rejection, binding immutability, deterministic output and input preservation. Validation evidence records local checks and their limits.

Lifecycle reference removal planning

The opt-in lifecycle::retire::plan_reference_removal boundary connects reference reconciliation to complete shared-owner retirement. It accepts validated combined state, exact reference removals, and caller-supplied file observations. It returns complete combined state and individual PlannedFile values without I/O or input mutation. The public ReferenceRemovalPlan contains state: CombinedState and files: Vec<PlannedFile>; the function takes borrowed prior state, a slice of exact Reference removals and a slice of ObservedFile values.

Retention and ownership

Graph reachability decides which installations retire. Artifact counts and consumer counts establish no graph authority. Removing one bundle path retains an installation reached by another path. Referenced empty installations survive; retiring empty installations produces no file action.

Only rule installations may retire through this M1 boundary. Unselected unsupported kinds and tagged legacy rows remain unchanged. Additions, updates and new sharing contracts are outside this API.

For each retained artifact, owner order is preserved and consumers are the union of surviving authoritative per-owner associations, in prior consumer order. No equality of rendered bytes is used to establish new sharing. Final combined state validation includes graph, ownership, associations and tagged legacy. Retained artifact and association rows keep their prior order. Tagged legacy rows retain their order, duplicate entries and historical source labels exactly.

File evidence and effects

Each affected physical location requires exactly one present observation, including shared-owner changes whose bytes are unchanged. Location identity uses exact scope and raw root spelling. Duplicate, missing, extraneous, malformed and invalid-mode observations conflict. Ancestor/descendant artifact paths involving an affected location conflict, including conflicts with untouched artifacts. Actions are ordered by scope, raw root and relative path and retain the complete observed destination and before-value. A conflict at any location returns an error without exposing a partial plan.

Whole-file hashes and recorded modes must match the observation. A surviving owner retains the observed file unchanged; final-owner retirement emits an absent after-value for that individual file.

For each aggregate instruction file, every recorded region is inspected, including untouched regions at that location. The aggregate editor runs once with the retained bodies. It preserves manual bytes and retained body text, updates retained marker metadata, and leaves an empty aggregate present. Aggregate mode is preserved from valid observed evidence: individual region records store no prior mode. Unknown markers, missing markers and edited owned regions conflict. Manual text may have changed before observation.

These supplied values establish no qualified filesystem provenance. Application must later use guarded, revalidated observations and the existing executor. No production CLI dispatch, schema-2 write, backup, recovery or native target activation is introduced. Existing mixed/instruction planners retain their conservative multi-owner rejection.

Components

Satisfies: reference-driven shared-owner removal planning.

Complete lifecycle installation upsert planning

The inactive lifecycle::upsert::plan_upsert boundary joins complete desired installation sets, explicit selection, authoritative reference reconciliation and the existing mixed-artifact planner. It receives CombinedState, an UpsertRequest and supplied ObservedFile values. It returns either UpsertOutcome::Ready(Box<UpsertPlan>) containing complete state and file plans, or Blocked(Vec<SelectionDiagnostic>). Invalid state, graph edits, desired sets or artifact evidence return an error with no partial plan. No I/O or input mutation occurs. Production commands and lockfiles remain on schema 1.

The working plan declared an unboxed ready payload. The implemented payload is boxed to satisfy the outcome-size lint; its state and file planning contract is unchanged.

Selection and references

UpsertRequest contains optional prior selection, complete desired sets, bundle upserts, explicit owner contracts and exact added/removed references. Installation graph upserts derive only from the complete sets. None selects no existing nodes and permits new roots. Existing installation and bundle upserts require selection. Edits to an existing direct root or bundle’s outgoing references require that root to be selected. Adding a new bundle may reference an untouched existing installation; that reference does not replace its artifacts or source history.

Every selected surviving installation needs a complete desired set. Nodes retired by graph reachability must also be selected. Selecting a bundle uses the existing complete dependency/member closure. Reconciliation preserves identity bindings and retention independently of artifact counts. Unsupported selected owned kinds block; this coordinator accepts rule installations only.

Prior-selection blockers and legacy/native blockers for supplied installed or bundle names are collected without exposing a successful subset. Repeated identical diagnostics appear once. Prior-selection diagnostics retain the selector output order: lexical requested-name diagnostics, original legacy category/index order, then unsupported installations in prior order. Additional legacy name blockers append in original category/index order. Unknown names from the new-name collision probe do not prevent adding new roots. This probe does not interpret source/scoped selectors or command-line syntax.

Complete output sets and associations

Desired files and instruction contributions must use the installation’s exact scope and raw root spelling. Each artifact has nonempty unique consumers. The union of artifact consumers and unique audience exclusions must exactly equal requested surfaces; an excluded surface cannot also receive output. These are supplied-value consistency checks, not proof that the source actually declared the exclusions or that native clients discover the outputs.

An empty set retires prior output ownership while retaining a referenced installation, its source evidence, requested surfaces and graph edges. The mixed planner’s Remove value is used only for output retirement here; graph reconciliation independently decides installation retention. A new empty set can retain history without any file action.

The coordinator rebuilds changed per-owner consumer associations from the complete sets. Untouched rows retain order and values. Replaced association identities retain relative order among surviving rows; new rows append in lexical scope, raw root, path, optional contribution ID and installation ID order. Final combined state validation checks graph, artifact ownership and exact consumer unions. Tagged legacy snapshots remain unchanged, including duplicate and malformed rows and native statuses.

Destination evidence and sharing

The existing mixed planner produces all affected file actions in one complete plan. Exact before observations are mandatory even for no-op bytes. It preserves manual aggregate text and observed aggregate modes and rejects edited ownership, unmarked dedicated files, topology conflicts and malformed observations.

Known frozen legacy entrypoints and resource namespaces also conflict with affected old or desired paths, including missing observed files and different aliases. Component boundaries distinguish old from oldish. Historical rows lack qualified scope/root bindings, so this check conservatively compares relative destinations across supplied roots. Missing candidate information is not filled in by inferring ownership; later source/destination qualification must establish the facts needed for application.

In plan_upsert, affected multi-owner artifacts and new shared artifact identities remain unsupported. Unaffected shared rows survive disjoint changes. Equal rendered bytes cannot establish compatibility. The separate owner-contract boundary now records and checks explicit source, content and adapter-revision evidence. The separate plan_shared_upsert entrypoint consumes that evidence with supplied observations as described below. Shared retirement also remains available through the separate removal API.

Explicit shared upsert

plan_shared_upsert accepts the same request and observations and returns the same outcome as plan_upsert. Both entrypoints share selection, complete-set validation, graph retention, legacy collision checks and contract propagation. The original entrypoint and the public mixed planner retain their conservative sharing rejection. The shared entrypoint is an inactive supplied-value API; source and filesystem qualification remain outside this boundary.

Candidate artifacts use exact scope, raw root, path and optional contribution selector. Surviving artifact identities and real owners retain relative order; new identities and owners append in key order. Changed installations supply complete sets. Untouched owners keep their binding, body/file content and contract evidence. Adding an owner cannot silently change retained content. Desired file bytes and modes or instruction bodies at one identity must agree. Real immutable owner bindings are checked before physical projection.

After candidate whole-state validation, every affected artifact receiving desired output with multiple final owners requires complete matching contracts for all final owners. Full source-manifest digests and complete surface/revision maps must agree. Missing unselected evidence blocks the operation. Selected owners may replace unknown history with complete newly supplied evidence; no default revision is inferred. Single-owner results do not require invented proof. Retirement removes only the released owners’ proof. A pure release with no desired value at that identity retains all surviving owners and evidence unchanged, including unknown proof, without requiring agreement. Physical observations remain mandatory. Unaffected shared neighbors may retain unknown evidence.

Private physical projection

The private sharing planner assigns one deterministic internal owner to each logical artifact identity in the union of prior and candidate state. It delegates physical planning to operations::plan::plan_owned_artifacts after validating real ownership and agreement. No alternate reference authority or filesystem writer is introduced. For affected retained output without desired bytes, exact supplied observations provide the existing body or file; the existing planner verifies ownership and recorded content.

The existing planner retains observation, no-op verification, topology, permission and marker guards. Unknown dedicated files and unmanaged markers cannot be adopted. Shared instruction changes preserve exact manual text and observed modes. Untouched contribution bodies retain their proof, while aggregate whitespace metadata may refresh. Removing the final contribution keeps an empty aggregate file.

The projection maps only normalized content and instruction layout metadata back to the real candidate rows, requiring exact identity count and content agreement. Internal owner identities never enter returned state, contracts or references or public error messages. Physical errors translate complete private owner contexts into real owner lists. Typed instruction locations retain exact scope/raw-root attribution. Retained aggregate extraction reports the exact physical file ownership grouped by selector, including neighbors verified by the reader. Literal destination and cause text are preserved. Final complete validation precedes return. Errors expose no partial plan and inputs remain unchanged. This projection supplies no execution, source-authenticity, physical-containment or native authority.

Boundary limits and later work

These supplied-value boundaries plan supported rule outputs and explicit shared reconciliation. Their graph and artifact APIs retain first-error results. The opt-in session layer provides complete safely observable selected-set reporting. The captured command adapter connects supplied source snapshots, qualified destinations and guarded application. The source-backed adapter now resolves explicit rule roots and applies invocation/configuration/recorded target precedence. Codex rule source integration now has an explicit library route. Source/scoped selectors, registry discovery and production CLI integration remain. Captured preview is available as an opt-in supplied-value boundary. Read-only captured list/doctor and qualified scope inspection are available through the inspection boundary. The opt-in guarded application boundary now provides migration-only first-write backup, fresh backup-absence guards and executor/recovery integration. Gate 6 must qualify native targets before schema-2 production activation. This pure plan grants no execution, source authenticity, physical containment or native support authority.

Components

Read-only lifecycle inspection

The opt-in lifecycle::inspection library exposes inspect_state, doctor, and inspect_scope. inspect_state accepts captured schema-1 or strict schema-2 bytes, retains those exact bytes, and exposes validated combined records alongside indexed legacy rows and diagnostics. Unknown schema-1 JSON fields remain in the captured bytes rather than the typed projection.

doctor consumes supplied destination observations. It distinguishes missing evidence from an observed missing file, checks whole-file hashes and recorded modes, and verifies the complete set of recorded Markdown regions at each location through the existing aggregate verifier. A shared aggregate validation failure is reported against each recorded contribution at that location. Manual bytes outside owned regions remain outside the content hash. Duplicate destination observations are rejected; extra observations are permitted. Whole-file mode checks apply only when state records an expected mode; missing mode evidence then reports ModeUnknown. Markdown contribution checks do not compare file modes. Legacy diagnostics remain visible in the inspection report; legacy output is neither adopted nor checked as owned content.

inspect_scope captures the state and owned destinations through the qualified filesystem boundary and revalidates both snapshots before returning. It reports validated journal presence through QualifiedScope::has_pending_operation without taking a writer lock, creating execution storage, fetching sources, writing backups, or performing recovery. A completed journal awaiting cleanup also counts as pending. Malformed journals, unsafe destinations, foreign artifact roots, and unsupported schemas return errors. These observations sample the namespace and do not exclude concurrent writers or prove native discovery or source freshness. Qualified inspection is supported on Linux and macOS.

Production command dispatch and schema 1 remain unchanged. The captured command adapters and source-backed orchestration are now implemented as opt-in library boundaries. Complete lifecycle delivery remains Gate 5 work; this inspection slice does not complete Gate 5. Captured preview is described below.

StateInspection contains exact original bytes, validated CombinedState, and an optional descriptive LegacyProjection. ScopeInspection contains optional state, ordered artifact diagnostics, and the sampled pending_recovery flag. An absent state file yields no state or artifact diagnostics while journal inspection still runs. Diagnostic indices address the original artifact vector; one artifact may report both content and mode drift. No report authorizes a repair or ownership adoption.

Components:

Read-only lifecycle preview

The opt-in lifecycle::preview::preview API consumes captured schema-1 or schema-2 state bytes, an explicit PreviewRequest, and supplied output observations. It delegates restrictive upserts to plan_upsert, explicit shared upserts to plan_shared_upsert, and exact reference removals to plan_reference_removal. Reference-removal requests identify graph edges; CLI name/source selection and source resolution remain separate work.

A ready LifecyclePreview retains exact original state bytes, the complete validated proposed combined state, and ordered FilePreview actions. Each action retains the planner’s exact location and before/after bytes and modes. FileChange distinguishes creates, content-only changes, mode-only changes, combined content/mode changes, removals, and unchanged actions. Reference and owner changes can change state while leaving output bytes unchanged; those no-op output actions remain visible when returned by the planner.

StateChange compares canonical validated records, so differences only in serialization whitespace or canonical record order do not imply semantic changes. Schema-1 input instead reports UpgradeFromSchema1: any subsequent applying transition would require the exact-backup prerequisite. Preview does not create that backup or perform a migration. Output actions exclude the final state write and backup, which remain guarded application responsibilities.

Selection blockers return PreviewOutcome::Blocked with the existing typed diagnostics and no partial actions. Invalid state, missing/edited output evidence, and graph/artifact conflicts return an error without a successful partial preview. The underlying planners retain their first-error behavior; this preview does not introduce operation-wide conflict aggregation. Applying source/session reporting has its own contract. Unrelated typed legacy/native rows survive in the proposed state, and unknown original schema-1 fields remain available in the retained bytes rather than the typed projection.

The adapter performs no filesystem access, source fetch, writer locking, journal creation, application or recovery. Supplied observations do not prove current filesystem freshness or source authenticity. The preview is descriptive, not an execution capability; application must qualify evidence under its writer guard. Production command dispatch and schema 1 remain unchanged. Captured command adapters and explicit rule-source orchestration are implemented as opt-in library boundaries. Complete lifecycle delivery and Gate 6 native qualification remain pending.

Captured lifecycle command adapters

The opt-in command adapter connects the existing selection, shared upsert and reference-removal planners to origin-bound qualified execution. It is a library interface for applying requests; production command dispatch remains on schema 1. Source fetching and native client discovery are outside this boundary.

Session capture and recovery

Callers acquire the matching QualifiedScope writer guard before opening LifecycleSession. Opening recovers the existing executor journal before capturing canonical lockfile and backup evidence. Every recovery report returns SessionOutcome::Recovery and stops the new request, including completed recovery; callers inspect the report and open a new session before source work. A ready session retains opaque metadata observations under the same guard. Absent state establishes fresh origin, schema 1 establishes migrated origin bound to its exact bytes, and schema 2 requires known origin. Fresh state rejects a backup; migrated state validates the retained original backup rather than accepting today’s backup as new historical authority.

Upsert preparation and supplied authority

prepare_upsert runs complete existing-selection preflight before invoking its builder. The builder supplies complete desired sets, owner contracts, bundle nodes and exact reference edits. Every desired installation’s manifest digest must match a retained SourceSnapshot. This proves captured association and freshness, not compiler origin for arbitrary supplied output. Typed instruction contributions remain caller-authorized values under ruling R-G5-CAPTURED-1; a source orchestration adapter must bind its rule values to actual compiler results. Alternatives for this supplied-value boundary were not documented.

The adapter enumerates old and desired output locations into qualified evidence, then supplies only affected observations to plan_shared_upsert. File permission selection updates desired content and matching owner contracts together. Generated files start private, ordinary existing modes survive, resource owner execution follows explicit ResourceExecutionPolicy (normally Disabled), and instruction actions select generated modes from exact observed before values. State, backup and executor-journal destinations cannot be content outputs.

Direct-root removal and consuming application

prepare_remove preflights the complete selection and derives only direct roots whose literal installation or bundle name was requested. Bundle dependency/member closure does not remove independent direct roots. A named member retained only through a bundle returns NoDirectReference; the caller can identify its declaring bundle. Reachability and owner retirement reuse the existing planners, preserving shared survivors, manual instruction bytes, unselected graph records and individually unowned extra resources.

A ready PreparedOperation privately retains its next state, file plan and qualified destinations while borrowing its session and exact source snapshots. Read-only accessors expose the proposed state and plan. Consuming apply cannot accept replacement state, snapshots or guard: it rechecks metadata, destinations and sources before calling the existing origin-bound writer. That writer owns output/state checkpoints and recovery; no second journal or writer is introduced.

Acceptance examples cover fresh and migrated writes, update/removal, complete blockers, stale captures, direct roots, shared ownership, permissions and reserved destinations. The validation note records qualification limits. The source-backed adapter binds compiled rule outputs to retained captures. Whole-lane interruption acceptance, hosted delivery and Gate 6 native qualification are separate evidence and delivery concerns.

Satisfies: captured lifecycle selection preflight, complete lifecycle upsert, shared-output reconciliation, and reference-driven removal.

Source-backed lifecycle orchestration

Requirements

The opt-in applying boundary opens the guarded lifecycle session and completes recovery before any source work. A recovery report stops the current request. Complete literal/all selection and target preflight precede resolution. Selected legacy or unsupported nodes block the complete operation; unrelated legacy rows remain preserved. Removal uses recorded direct references and ownership without resolving sources or interpreting current preferences as history.

Explicit add requests identify each source rule name and installed alias. Updates preserve installation IDs, aliases and the existing reference graph while recapturing the requested source binding and recording actual Git commits and exact manifest digests. Repeated add of the same binding reuses its recorded ID and adds no duplicate direct reference. A different binding cannot reuse an installed alias, and an add alias cannot collide with an owned bundle name. The historical source route rejects instruction-backed owners, including artifact-free Codex installations, before parsing or resolving sources. The explicit Codex source route accepts its supported retained representation. Recorded removal remains source-free. Caller-supplied complete graphs and manual/shared instruction contributions remain on the captured command API.

Nonempty invocation targets override nonempty project configuration, then nonempty global configuration. Update otherwise uses recorded targets; add uses caller-supplied defaults. Empty selections are absent overrides. No effective selection or duplicate requested targets rejects rather than inventing defaults. Requested targets, audience exclusions and actual artifact consumers remain separate. Compilation requires explicit compiler bindings and profiles and is limited to the existing Claude, Copilot and OpenCode families on the historical route. The separate Codex route accepts only Codex CLI revision 1. Native Codex qualification and production schema-2 activation remain Gate 6 work.

Architecture

Interfaces and preparation lifetime

lifecycle::orchestration::with_prepared accepts a qualified scope, its caller-retained writer guard, installation root, SourceRequest, SourceOptions and a callback. SourceRequest distinguishes explicit add roots, selected updates and selected removals. SourceOptions contains target precedence inputs, add defaults and a CompilerContext holding compiler bindings, contract profiles and resource execution policy.

OrchestrationOutcome distinguishes typed blockers, recovery and Ready(T), which contains the callback result rather than proving application. The callback receives opaque SourcePrepared; consuming apply revalidates all retained captures, including Git identity, before the captured operation’s own metadata, destination and snapshot checks. Discarding preparation applies nothing. The prepared operation cannot outlive the callback or accept substituted output. Resolvers, private temporary checkouts and captures remain alive until the callback returns. Existing permission selection keeps owner contracts consistent with resulting file modes. The adapter introduces no separate writer or journal.

Installation identity

New IDs are full lowercase SHA-256 digests of a versioned JSON tuple containing upskill-installation-v1, ownership scope, raw root bytes, normalized requested source label, rule kind, source name and installed alias. Tuple serialization frames component boundaries; actual resolved Git commits and manifest digests are evidence outside identity. Exact prior bindings reuse recorded IDs, including previous caller-selected IDs. A computed ID already bound differently or multiple prior installations with the same binding rejects. Raw root spelling remains identity-bearing.

The adapter selects existing matching owners for complete upsert and adds only absent direct references. Alias preflight normalizes local ancestors while leaving the selected final component for qualification. Updates retain graph history; this module neither discovers registries nor constructs bundle graphs.

Acceptance and limits

Integration examples cover local add/update/remove, branch advancement, exact revision and manifest evidence, ID and alias preservation, repeated add, target precedence, stale captures, manual output conflicts, disjoint legacy preservation, complete selection blockers, audience exclusions and resource permissions. A private identity example pins raw-root framing, collision rejection and reuse of prior IDs independently of resolved commit evidence. The validation note records local checks and review evidence. These examples alone establish neither whole-lifecycle interruption acceptance nor hosted delivery or native behavior.

Explicit Codex route and retained representation

with_prepared_codex shares session recovery, complete selection preflight, source resolution, captured lifetimes and application with with_prepared. CodexSourceOptions supplies target precedence and defaults, an explicit profile and execution policy. Profile and effective selection failures return typed Instructions capability errors before source resolution. Recovery and Remove precede these checks; Remove uses recorded ownership even after source deletion or with unavailable compiler options.

Every retained selected owner, including same-alias Add refreshes, must be a rule requested exactly for Codex CLI. Its scope and raw root path must exactly match the declared session root before any selected source is resolved. It may own no artifacts when excluded, or one Markdown contribution at project AGENTS.md or user .codex/AGENTS.md. That contribution must have the producer-derived identity, sole installation ownership, exact Codex CLI per-owner consumers and its matching revision-1 contract. Arbitrary identities, multiple contributions, shared ownership, whole-file conversion and malformed bindings reject before any selected source is resolved. This conservative rule does not migrate hand-constructed or historical adapter state.

SourcePrepared::instruction_readers() exposes complete per-installation reports sorted by installation ID, including excluded output with NotEvaluated compatibility. Historical and Remove callbacks expose no newly compiled reports. Potential incidental readers never enter ownership, references, contracts, combined state or execution receipts. A caller can inspect or clone reports before applying, or discard preparation without publication.

The declared root determines the exact destination. Qualified filesystem access does not establish native user-home selection or client discovery. All selected sources compile before the callback, and application rechecks original capture bytes and actual Git revision. Local/Git project/user examples, stable exclusion and reinclusion, complete preflight failures, manual bytes/modes, and a revision-only private-checkout mutation are covered by lifecycle_codex_orchestration.rs. Production commands and schema 1 remain unchanged; native qualification, all-kind compatibility, activation and staging accounting in #316 remain separate gates. Complete independently observable preflight reporting is implemented at the opt-in library boundary described below.

Complete selected-set preflight reporting

Requirements

Source-backed Add and Update collect independently observable binding, resolution, source-capture, compilation, capability, destination and ownership failures across the complete selected set. An invalid source does not hide conflicts in independently compiled sources. An item that fails one preparation stage supplies no speculative downstream destination diagnostics.

Root qualification, writer-lock acquisition, recovery, invalid authoritative state and unsafe destination observation remain operation-level boundaries. Recovery stops the new request. Invalid retained root or representation authority is rejected before selected sources are resolved. Collection stops when safe inspection cannot proceed; a report is complete only for observations that can be obtained safely. The existing selection blockers retain their own typed outcomes.

A blocked request cannot invoke the prepared callback, create a new operation journal, publish outputs, migrate state or execute a successful subset. Valid sharing remains permitted under the existing complete owner-contract agreement. Retained owners, manual instruction text, exact modes, unrelated references and schema-1 backup evidence remain protected by the existing planner and executor.

Architecture and report contract

CommandDiagnostic::Preflight(PreflightReport) carries structured diagnostics and available compiled instruction-reader reports inside the existing Blocked outcome. Diagnostics contain installation participants, category, optional location and explanatory text. Participant IDs are sorted and deduplicated. Diagnostics sort by participant IDs, then category in the order Binding, Resolution, Capture, Compilation, Capability, Destination, Ownership; ties sort by scope, raw root, relative path and message. Only exact diagnostic records are deduplicated. Error wording is descriptive rather than a stable machine code.

Update diagnostics use recorded installation IDs. Add diagnostics normally use the same normalized requested binding as installation identity. If normalization fails, tuple framing of the original requested source label supplies a report-only identity; it never establishes source evidence or persisted state. Duplicate selected aliases or identities are reported without choosing a winner. Their invalid desired topology is excluded consistently before observation and reporting; their recorded prior ownership remains protected.

Structural and reserved-path checks precede filesystem observation. Conflicting locations are excluded from observation; unrelated safe locations remain inspectable. The qualified session captures their union once and selects modes before comparing desired file contracts. Location-level reporting retains every recorded owner, consumer association and contract at that location, including owners whose selected source failed preparation. It reuses shared ownership and physical planning checks without constructing an applying operation. Structural failures suppress speculative content diagnostics at their locations. A mode failure likewise suppresses diagnostics that depend on a usable desired mode.

A complete captured request projects its authorized reference edits before location reporting so explicitly retired owners do not appear as untouched survivors. Duplicate bindings prevent that projection; failed-source subset reporting never infers retirements. Only a complete clean request enters opaque preparation. It retains the qualified destination snapshot through application, which still revalidates metadata, source bytes, Git revisions and destinations. Pure planners retain their first-error APIs; independent diagnostic collection belongs to the orchestration/session boundary.

Compiled reader inventories sort by installation ID on blocked and ready outcomes. Excluded content retains its NotEvaluated report; failed compilation creates no report. Potential incidental readers grant no ownership, consumer association, contract, reference or execution authority. Historical compilation and recorded removal introduce no new instruction-reader inventory.

Executable examples and limits

The following integration examples pin the reporting contract:

  • invalid_source_does_not_hide_two_destination_conflicts reports one resolution failure and two independent occupied destinations, with no callback or effects.
  • unchanged_selection_does_not_hide_changed_conflict retains a source-label parse failure alongside changed and unchanged selected-source conflicts and an exact unselected survivor.
  • complete_conflict_inventory, mixed_artifact_and_sharing_conflict_matrix and duplicate_binding_does_not_hide_disjoint_conflicts pin complete participant inventories, mixed artifacts and independent conflict groups.
  • compatible_shared_files_and_distinct_contributions_have_no_conflicts pins legal identical file sharing and distinct instruction contributions.
  • blocked_preflight_keeps_excluded_and_incidental_readers pins both compiled reader roles and unchanged retained ownership.
  • instruction_mode_failure_does_not_hide_an_independent_source_failure pins special-mode rejection alongside an independently failed source.

Run cargo test --test lifecycle_preflight --test lifecycle_orchestration --test lifecycle_codex_orchestration --test lifecycle_commands to exercise these examples and the existing capture/authority regressions. Process and special-mode fixtures require an environment that permits their OS operations.

This is library-only G6-03 reporting support. Production commands still use schema 1; production schema-2 migration, all-kind compatibility, native client qualification and activation remain separate gates. The roadmap remains Gate 6, then owned-skill lifecycle support, then automatic maintenance.

Edit the canonical source.

Artifact planning

Architecture reconciliation (2026-10-25): the delivery, discovery and activation contract controls intended production routes and reader intent. Existing Codex AGENTS.md contribution APIs described here are experimental library boundaries, not production independent-rule delivery. Independent Codex/OpenCode rules remain beside unchanged team guidance through a selected native or hook route. The native-first amendment permits explicit OpenCode paths after Q5 PASS and selects the trusted Codex hook with its declared recorded-history limitation. Reusable ownership, preflight and recovery mechanisms do not select or qualify a runtime route, and schema-2 production activation remains blocked (AD-0040 amendment).

This concern owns the requirements and implemented architecture below. Moving these records does not change their implementation or approval status. Library-only capabilities remain separate from production CLI activation; each section records its own limits. Accepted decisions remain in ADRs.

Requirements

Pure owned instruction planning

The library function instructions::plan::plan_instructions joins validated owned-artifact records with the Markdown aggregate editor. Callers supply complete installation contribution replacements or removals and exact observations for every affected file. The result contains the complete resulting artifact vector and file plans with exact optional before bytes and after bytes, including content no-ops. Absent files and existing empty files have distinct preconditions. Removing the last region retains manual bytes and keeps the aggregate file, which is empty only when no content remains. The plan has no deletion action.

Set replaces one installation’s complete contribution set. Existing installation IDs retain their source, item kind, source name and installed alias; recorded revision may change. Untouched selectors retain their bodies, owners and consumers and cannot be taken over, even with identical content. Affected regions require one owner; affected whole files or shared regions conflict. Unaffected records, including whole files and shared regions, survive exactly. At affected locations, the editor rebuilds separator metadata for retained regions after other removals.

instructions::aggregate::inspect_owned reads exact verified Markdown bodies in document order through the same private marker/span validator as reconciliation. Edited, missing, duplicate, overlapping, unknown or malformed regions conflict. Bytes outside managed regions remain unchanged. Locations use scope, raw lexical root spelling and portable relative path; file plans sort by that key. Surviving artifact rows retain prior order and new selectors append in location/ID order.

Planning performs no I/O and validates prior and final ownership plus all desired data. Observations must match affected locations exactly; missing, duplicate or extraneous observations conflict. Supplied bytes do not establish source or filesystem evidence. Callers must resolve direct/bundle references and native compatibility first. The separate pure whole-file planner is implemented in owned whole-file operation plans; captured compilation in captured item compilation derives complete per-client entrypoint and resource outputs. The complete mixed coordinator is implemented in mixed owned-artifact operation plans. Reference-graph authority and per-owner consumer associations now support removal planning (reference-driven shared-owner removal planning). Opt-in source-backed orchestration connects captured-rule desired sets to complete add/update planning, combined state persistence and guarded execution/recovery. This prerequisite changes no CLI behavior, source format or lockfile format and enables no native client support.

See the planner design and its decision records for implementation boundaries and rationale.

Owned whole-file operation plans

The pure library module files::plan exports these public values, all deriving Debug, Clone, PartialEq and Eq without serialization:

#![allow(unused)]
fn main() {
pub struct FileLocation {
    pub root: InstallationRoot,
    pub path: String,
}
pub struct FileValue {
    pub bytes: Vec<u8>,
    pub mode: Option<u32>,
}
pub struct ObservedFile {
    pub location: FileLocation,
    pub value: Option<FileValue>,
}
pub struct DesiredFile {
    pub location: FileLocation,
    pub value: FileValue,
    pub consumers: Vec<TargetSurface>,
}
pub enum FileInstallationChange {
    Set { owner: ArtifactOwner, files: Vec<DesiredFile> },
    Remove { installation_id: String },
}
pub struct PlannedFile {
    pub location: FileLocation,
    pub before: Option<FileValue>,
    pub after: Option<FileValue>,
}
pub struct FileOperationPlan {
    pub artifacts: Vec<OwnedArtifact>,
    pub files: Vec<PlannedFile>,
}
pub fn plan_files(
    prior: &[OwnedArtifact],
    changes: &[FileInstallationChange],
    observations: &[ObservedFile],
) -> anyhow::Result<FileOperationPlan>;
}

FileLocation uses the ownership model’s InstallationRoot; desired files use explicit TargetSurface consumers and changes use an explicit ArtifactOwner. Inputs are borrowed and unchanged. Outputs own their bytes and ownership records. The planner performs no I/O. An observed None means an absent directory entry, not an unreadable file or directory; supplied observations are unqualified before-state evidence and do not establish provenance.

Prior and desired records pass ownership::validate_artifacts. Observed and desired locations require host-absolute nonempty roots and portable relative paths; optional modes must be in 0..=0o7777. Invalid locations and modes are rejected before observation membership diagnostics. Location identity and sorting compare scope, the exact root OsStr spelling and portable path. There is no component normalization, canonicalization, case folding, alias resolution or containment claim.

A Set supplies the complete nonempty whole-file set for its installation; Remove removes its last files. Duplicate changes, unknown removals and duplicate desired locations conflict. Reusing an installation ID cannot change source, kind, source name or installed alias. Its revision may change consistently across all replacement records. Equal content establishes neither compatibility nor shared ownership.

Affected locations include prior records owned by selected installations and all desired locations. Each requires exactly one observation, including revision-only changes and byte-identical no-ops. Duplicate, missing and extraneous observations conflict. Affected Markdown contributions and multi-owner whole files conflict. Unrelated records, including shared files and contributions, survive exactly. A desired file cannot take over an untouched installation; explicit removal of the old owner plus selection of the new owner permits handoff only when the recorded before-state is verified.

Owned locations require present observed bytes matching the recorded SHA-256 and exact equality of the optional recorded and observed modes. None differs from Some(0). Edited or missing files conflict on update, removal and no-op. Unowned locations require absence even when present bytes and modes equal desired values. The planner does not reinterpret platform mode evidence or authorize permission application.

An affected location cannot be a strict slash-component ancestor or descendant of any prior or desired artifact within the same exact root and scope. Contributions count as files here. a and ab are distinct siblings; removing a while creating a/b still conflicts. Disjoint untouched topology pairs do not block other work.

The result contains one PlannedFile per affected location in lexical order, with exact observed before and desired after values, including no-ops. Creation has absent before; deletion has absent after and identifies only an individual recorded file, granting no directory ownership or recursive removal authority. Desired ownership records contain SHA-256 of exact desired bytes, optional mode, explicit owner and explicit consumers. Untouched rows and same-location replacements retain prior row order; removed rows disappear and new rows append in lexical order. The final record set is validated before return. Any failure returns no successful partial plan.

This prerequisite leaves existing commands, CLI flags, lockfile persistence and native-client behavior unchanged. Opt-in source-backed rule orchestration connects captured-rule desired sets to lifecycle planning, qualified source/destination observation, writer locking, permission policy, durable state and apply/recovery. Native destination qualification and Codex activation remain pending. The mixed coordinator in mixed owned-artifact operation plans composes complete sets across both forms; the individual planners do not authorize partial application of mixed installations. This API does not complete M1-A2, M1-A3 or M1.

See the whole-file planning design and the active operational lifecycle design.

Mixed owned-artifact operation plans

The pure library module operations::plan composes the existing dedicated-file and instruction planners into one complete installation operation. Its two new value types derive Debug, Clone, PartialEq and Eq, without serialization:

#![allow(unused)]
fn main() {
pub enum DesiredArtifact {
    File(DesiredFile),
    Instruction(DesiredInstructionContribution),
}

pub enum InstallationChange {
    Set {
        owner: ArtifactOwner,
        artifacts: Vec<DesiredArtifact>,
    },
    Remove {
        installation_id: String,
    },
}

pub fn plan_owned_artifacts(
    prior: &[OwnedArtifact],
    changes: &[InstallationChange],
    observations: &[ObservedFile],
) -> anyhow::Result<FileOperationPlan>;
}

The existing files::plan and instructions::plan modules supply the desired values. ownership supplies records and owners. The result reuses FileOperationPlan, PlannedFile, FileLocation and FileValue from files::plan; both physical forms therefore carry exact optional before and after values.

A Set replaces the installation’s complete desired set across both forms. Omitted old files and contributions are removed through their respective planners; a global Remove reaches every old form. Empty Sets require explicit Remove. Duplicate changes, unknown removals, duplicate desired file locations or contribution selectors, invalid owners and invalid consumers conflict. An existing installation retains its source, item kind, source name and installed alias even when the old and desired forms are disjoint. Its revision may change coherently across all desired records. Any multi-owner record at an affected location conflicts; untouched shared rows survive exactly.

Locations compare (scope, raw root OsStr, portable relative path). Raw dot components and project/user scopes remain distinct. Roots must be nonempty and absolute; paths must use portable relative components. Supplied optional modes must be within 0..=0o7777. Every affected physical location requires exactly one observation, including no-ops: the affected union includes every old row belonging to a changed installation and every desired location. Missing, duplicate or extra observations conflict, and all observations are validated before membership checks.

A location cannot participate in both forms anywhere in the prior/desired union. Even explicit old-owner removal and new-owner replacement cannot convert a contribution into a whole file or the reverse at that location: contribution ownership does not establish authority over manual aggregate bytes. Same-form handoffs retain the underlying planners’ rules. An ancestor/descendant pair in the union conflicts when either location is affected, within the same exact root and scope. This includes cross-form, removal-only and old/new transitions. Slash-component boundaries distinguish a/b from ab; unrelated historical pairs survive without a filesystem safety claim.

Dedicated actions retain exact optional before/after bytes and modes. Owned bytes and optional modes must still match prior evidence on update, removal and no-op; an unowned destination must be absent. Aggregate actions use the exact observed before value and the editor’s after bytes, preserving the observed optional mode. An absent aggregate receives after mode None. Invalid UTF-8, edited or unrecorded markers and missing owned aggregates conflict. Manual bytes survive; removing the last contribution retains the aggregate, even when empty. No permission authority follows from the supplied mode values.

Every affected action remains in the result, including byte-identical no-ops. Actions sort by exact location and duplicate physical actions conflict. Retained artifact identities replace rows at their original positions across the original interleaved forms and installations. Removed identities disappear; new rows append in exact location/selector order, with whole files distinguished from contribution selectors. Final combined ownership is validated, including revision coherence. Reordering changes, desired entries or observations does not change the result. The API performs no I/O or input mutation; any failure returns no successful partial plan. It may return the first contextual conflict rather than all diagnostics.

This library prerequisite activates no CLI selector, native adapter, source fetch, filesystem observation, lock, journal, schema-2 persistence or execution. Captured-rule desired sets are implemented in captured rule desired sets; reference authority and per-owner associations support removal planning in reference-driven shared-owner removal planning. Complete add/update integration with immutable source evidence, source/target adapters, qualified observations, permission/metadata policy, writer locking, guarded persistence and apply/recovery remains later lifecycle work. Pure permission selection is implemented as a separate inactive supplied-value capability. It does not complete M1-A2, M1-A3 or M1. The public declarations are in operations::plan.

Architecture

Owned instruction operation planning

This record describes the pure library planner implemented in instructions::plan. The pure owned instruction planning owns its requirements. The planner has no CLI dispatch, state codec or I/O.

Public boundary

plan_instructions borrows prior ownership records, installation changes and observations, and returns an owned InstructionOperationPlan through anyhow::Result. The public DTOs derive equality, cloning and debug support; they have no serialization contract. An InstructionLocation combines an InstallationRoot and portable relative path. ObservedInstructionFile holds that location and optional exact bytes; absence differs from an existing empty file. A set change carries an ArtifactOwner and its complete desired contribution set, including Markdown, selector and consuming surfaces. A remove change names an installation ID.

The result contains the complete artifact vector and PlannedInstructionFile entries with exact optional before bytes and resulting bytes. File plans include content no-ops and retain aggregates after final-region removal. Caller-supplied observations establish preconditions for a later executor, without establishing filesystem or source authority. AD-0016 records this boundary.

Validation and location grouping

The planner validates prior records with ownership::validate_artifacts, indexes installation bindings, and validates changes and desired contributions before editing. Existing bindings compare source, item kind, source name and installed alias explicitly; revision may change. Desired data passes through the existing editor and ownership validator, including paths, owner metadata and consumers. Duplicate changes, empty sets and unknown removals fail with context.

Affected locations are the union of prior locations involving changed owners and desired locations. Their key is scope, native OsString root spelling and portable relative path. Ordered maps and sets retain exact lexical identity without canonicalization, alias detection or containment checks. Observations must match that set exactly. A prior owned region cannot be reconciled from absent bytes; observed aggregates must decode as UTF-8.

Verified body inspection and editing

aggregate::inspect_owned shares the private span validator with aggregate::reconcile. Verified spans retain body start offsets and end-marker positions, so the reader takes exact body slices and returns contributions in document order. It rejects duplicate IDs, overlapping spans and invalid marker, hash or separator state through the existing editor validation. There is no second marker parser.

At each affected location, the planner reads all verified bodies, retains untouched selectors and combines them with the desired complete replacements. A desired selector cannot take over an untouched owner. It calls reconciliation once for the complete aggregate edit and joins returned selectors to the retained or desired owner and consumer metadata. Returned contribution metadata includes new separator context even for retained bodies. This allows subsequent removal to restore manual bytes exactly.

A changed multi-owner contribution conflicts because the stored consumer union does not identify each owner’s selection. Untouched shared contributions remain, including within an affected aggregate; separately owned regions are supported. An affected whole-file record conflicts. AD-0017 records the shared-region restriction. Empty aggregates remain files because prior file origin is unknown; see AD-0018.

Result assembly and downstream limits

Surviving prior selectors retain their relative artifact-row order. Removed selectors disappear; genuinely new selectors append in location and ID order. Unaffected locations retain exact records. File plans sort by location key. The complete result passes ownership validation before return; errors return no partial plan and inputs remain borrowed and unchanged.

Callers must resolve direct/bundle references and native compatibility before planning. The separate pure whole-file planner is implemented for explicitly supplied complete file sets. The separate captured item compiler derives complete per-client entrypoint and resource outputs from validated captured bytes. Qualified scope roots, writer locking, destination observation and qualified source observation are implemented as separate inactive capabilities. Final-head hosted source qualification remains required. The combined-state validator provides reference-graph authority and explicit per-owner consumer associations. Removal planning integrates those associations and returns complete combined state. Opt-in source-backed orchestration connects qualified source evidence and compiled rule outputs to complete add/update planning, persistence and guarded application. The opt-in qualified file executor and recovery are implemented separately; hosted final-head qualification remains required. The implemented mixed coordinator composes complete explicit sets across both forms; neither individual planner authorizes partial application of a mixed installation. Current commands, lockfile/source formats and native client support are unchanged.

tests/instruction_plans.rs covers owner retention, complete replacements and moves, exact preconditions, conflicts, consumer propagation, lexical scope/root identity and row ordering. tests/instruction_contributions.rs covers the shared reader and editor validation. These tests establish the pure boundary, without native discovery or filesystem execution claims.

The opt-in combined-state validator now checks reference authority and explicit per-owner consumer associations. Reference-driven removal planning now connects this state to shared-owner retirement. The single-owner upsert coordinator now connects complete add/update sets to graph and mixed-artifact planning. The separate inactive plan_shared_upsert entrypoint consumes explicit shared-output contracts. The opt-in application boundary connects complete plans and state to guarded execution and recovery. Production command activation remains pending; opt-in inspection and preview provide observational adapters.

Owned whole-file operation planning

This record describes the pure planner implemented in files::plan. owned whole-file operation plans owns its requirements. The implementation consumes supplied values and performs no I/O. AD-0021 records the bounded delivery choice.

Public boundary

plan_files borrows prior OwnedArtifact records, FileInstallationChange values and ObservedFile values. It returns an owned FileOperationPlan through anyhow::Result. The seven public DTOs derive Debug, Clone, PartialEq and Eq without serialization. FileLocation combines an InstallationRoot with a portable relative path; FileValue carries exact bytes and optional permission mode. Desired files additionally carry explicit consumers. A set change carries one explicit owner and its complete nonempty desired file set; removal names an installation ID. An observed absent value means an absent directory entry; it does not represent an unreadable file or directory.

The result contains the complete ownership vector and individual PlannedFile entries with exact optional before/after values. Byte-identical no-ops retain preconditions. Deletions identify individual files and grant no directory or recursive removal authority. Caller-supplied evidence establishes neither source provenance nor qualified filesystem authority.

Validation and indexing

The planner validates prior records with ownership::validate_artifacts, then validates observed roots, portable paths and optional modes. It indexes changes, validates desired records through the ownership boundary and rejects duplicate installation changes, empty sets, unknown removals and duplicate desired locations. Invalid observed or desired locations and modes fail before observation membership diagnostics. Desired record hashes use SHA-256 of exact desired bytes, rendered as lowercase hexadecimal through the existing digest dependency.

Location keys combine scope, exact native root OsString spelling and portable path in ordered maps and sets. No Path component normalization, lossy string conversion, canonicalization, case folding, alias resolution or containment qualification occurs. AD-0021 records location identity, complete replacements, stable bindings and deterministic ordering as parts of the pure API boundary.

Affected locations and before-state verification

Affected locations are the union of desired locations and prior locations with any changed owner. Affected contributions and multi-owner files fail before planning; a desired location cannot take over an untouched owner. Unrelated rows, including shared files and contributions, remain exact. Both old and new owners must be selected for a same-location handoff. AD-0022 records the composition restriction.

Observation keys must match the affected set exactly: duplicates, missing entries and extraneous entries fail, including for revision-only changes and no-ops. At a prior owned location, observed bytes must be present and match its stored SHA-256, and optional modes must match exactly. At an unowned location the observation must establish absence even when existing bytes equal the desired bytes. These choices are recorded in AD-0022.

The topology check compares the union of prior and desired locations in the same exact root and scope. It rejects a strict slash-component ancestor pair whenever one side is affected, including removal of a followed by creation of a/b. Contributions count as file locations. Component-prefix siblings and disjoint untouched pairs remain allowed. AD-0023 records why directory transitions await a qualified contract.

Result assembly and downstream limits

Each affected location produces one action in lexical key order. The planner walks prior rows to preserve unaffected rows and the positions of same-location replacements, drops removed rows, then appends new desired rows in lexical order. Replacement records contain the explicit owner, consumers, desired mode and exact byte hash. Final ownership validation precedes return; any error returns no successful partial plan. Inputs remain unchanged. AD-0021 records the ordering choice.

tests/file_plans.rs covers exact creation, replacement, removal, owner handoff, no-op preconditions, untouched-record retention, metadata validation, mode boundaries, binary/empty/Unicode values, raw-root and scope isolation, ancestor conflicts and deterministic output. The critical creation, update and empty-content hash assertions use independent literal SHA-256 values. Other fixture-derived expected records use test-side Sha256::digest. The tests establish supplied-value behavior, without proving actual filesystem observation or execution.

The captured item compiler already derives complete per-client resource and entrypoint outputs from validated captured sources. The mixed coordinator composes complete explicit file/contribution sets; neither individual planner authorizes partial application of a mixed installation. Captured-rule desired sets are implemented in captured-rule desired sets; opt-in lifecycle planning and source-backed orchestration connect these sets to guarded application. Qualified scope roots, writer locking, destination observation and qualified source observation are implemented as separate inactive capabilities, including conservative destination overlap rejection and descriptor-relative source and destination observation. Final-head hosted source qualification remains required. The opt-in adapters integrate qualified source evidence and combined durable state. Production activation and native Codex qualification remain pending. Pure permission selection and opt-in qualified file execution and recovery are implemented as separate library capabilities. This prerequisite leaves current commands, schema-1 lockfiles and native client behavior unchanged and does not complete M1-A2, M1-A3 or M1.

Mixed owned-artifact operation planning

This record describes the implemented pure coordinator in operations::plan. mixed owned-artifact operation plans owns its requirements. AD-0027 records the accepted composition choice. The coordinator performs no I/O and is not called by current installation commands.

Public boundary

plan_owned_artifacts borrows prior OwnedArtifact records, InstallationChange values and ObservedFile values, returning an owned FileOperationPlan through anyhow::Result. DesiredArtifact distinguishes File(DesiredFile) from Instruction(DesiredInstructionContribution). InstallationChange holds either a Set with one owner and desired artifacts, or a Remove naming an installation. Both new enums derive Debug, Clone, PartialEq and Eq without serialization. Existing file planner DTOs represent both forms’ physical actions with exact optional before/after bytes and modes.

A Set describes the installation’s complete desired state across both forms. The coordinator validates and partitions that operation before invoking the reviewed file planner and instruction planner. It then reconstructs one complete ownership vector and one sorted physical action vector. Failure at any stage returns no successful combined plan; inputs remain unchanged.

Complete installations and bindings

Complete prior ownership validation precedes indexing. Global installation bindings are checked before forms split, so changing from file-only to instruction-only ownership at a distinct location cannot bypass the source, kind, source-name or installed-alias binding. Source revision may change coherently. Duplicate changes, unknown removals and empty Sets conflict.

The original plan required existing helpers to remain unchanged. The accepted implementation ruling instead exposes files::plan::validate_binding as pub(crate) and imports it into the coordinator. Its body and existing planner behavior remain unchanged. This narrow visibility expansion avoids duplicating stable-binding policy, at the cost of crate-internal coupling to the file planner.

Prior rows partition by form while preserving their relative order. Each form receives a nonempty Set when desired, a Remove when previously owned but now omitted, or no change when both absent. A global Remove reaches every previously owned form. Both planners receive complete form partitions rather than partial mixed requests. Any multi-owner record at an affected physical location conflicts, including another selector there; untouched shared rows survive.

Locations and supplied evidence

Ordered keys combine ownership scope, raw root OsString and portable relative path. Roots must be nonempty and absolute; lexical identity does not normalize dot components or prove host equivalence, containment or realizability. The coordinator indexes form membership throughout the prior/desired union and marks old locations of changed installations plus every desired location as affected.

Every supplied observation is validated before membership checks, including root, portable path and permission-bit range. Exactly one observation must match each affected location, even for no-ops; missing, duplicate and extra evidence fail. A physical location cannot have both forms anywhere in the union, including an explicit removal and replacement. Contribution ownership does not prove authority over surrounding manual bytes. Same-form handoffs retain each planner’s rules.

Ancestor/descendant locations within the same exact root and scope conflict when either endpoint is affected. This check covers both forms, removal-only changes and old/new transitions. Slash-component boundaries distinguish a/b from ab. Unrelated historical topology is preserved without a filesystem safety claim.

Observations partition using validated form membership. Dedicated values retain bytes and optional modes intact. Instruction observations pass optional exact bytes to the aggregate planner; physical mode evidence stays with the coordinator. The existing planners enforce owned byte/mode evidence and verified aggregate markers, including invalid UTF-8 rejection and exact manual-byte preservation.

Combined result

The coordinator indexes planned ownership by exact location plus optional contribution selector. It walks the original interleaved prior rows, replacing retained identities at their former positions and dropping removed identities. Remaining new identities append in key order. Final combined ownership validation checks the union, including revision coherence.

Dedicated actions retain their exact before/after values. Aggregate actions use the supplied observed before value and the editor’s after bytes. Their after mode is the observed optional mode, or None for an absent aggregate. Removing the last contribution retains the aggregate, even when empty. Modes grant no permission or execution authority. All affected actions, including byte-identical no-ops, remain, sort by exact location and pass a duplicate-location guard.

Evidence and remaining integration

tests/operation_plans.rs covers mixed creation, complete replacements and removals, disjoint-form binding checks, coherent and conflicting revisions, physical collisions and topology, exact observations, manual bytes and aggregate modes, shared ownership, raw-root/scope identity, interleaved row reconstruction and deterministic permutations. Literal hashes and bytes pin changed contribution metadata and a subsequent no-op roundtrip. The tests establish supplied-value behavior rather than filesystem execution.

The captured compiler already derives complete per-client resource and entrypoint outputs. Opt-in source-backed orchestration connects rule outputs to lifecycle ownership, roots, consumers and qualified destination observations. The combined-state library now validates reference authority and per-owner consumer associations. The separate removal planner integrates those records for shared-owner retirement. Complete add/update planning, immutable source evidence, source/target adapters, writer locking, guarded durable state and physical apply/recovery are available through opt-in library boundaries. Production activation and native qualification remain later gates. The pure permission-selection policy is implemented; qualified file execution and recovery consume selected modes as an opt-in library capability. Production lifecycle activation and native qualification remain pending. The API may return the first contextual conflict. The opt-in source/session layer provides complete safely observable selected-set reporting without changing this pure API’s first-error behavior. No production CLI selector, native support, schema-2 persistence or installation effects are activated. M1-A2, M1-A3 and M1 remain incomplete.

Edit the canonical source.

Filesystem access

This concern owns the requirements and implemented architecture below. Moving these records does not change their implementation or approval status. Library-only capabilities remain separate from production CLI activation; each section records its own limits. Accepted decisions remain in ADRs.

Requirements

Qualified scope roots and advisory writer locking

filesystem::QualifiedScope::open(&InstallationRoot) pins an existing absolute scope directory on Linux and macOS. Parent components, missing or non-directory roots and final symlinks are rejected. Dot components and trailing separators are normalized before final symlink checks. Ancestor aliases are permitted during explicit selection; native non-UTF-8 paths are preserved where the underlying filesystem accepts them. Filesystem refusals remain contextual errors.

verify_binding() revalidates the normalized selected spelling, canonical path and directory device/inode identity. A renamed, replaced or symlink-substituted root, or retargeted ancestor alias, invalidates the old binding. The held handle does not freeze the path namespace or establish descendant containment.

try_lock_writer() uses a fresh close-on-exec directory description and a nonblocking exclusive advisory lock. It revalidates binding before and after acquisition. Same-process and cross-process writers to one physical directory contend, including aliases and different Project/User labels; distinct directories remain independent. Contention returns a busy error. Other filesystem failures remain errors. An opaque borrowing ScopeWriterGuard retains exclusion until drop or holder termination; ordinary exec does not retain it. Temporary fork inheritance is possible. No fork-safe lease, fairness, network-filesystem guarantee or exclusion against external editors is promised.

These operations create no files, directories or persistent state and preserve existing bytes and permissions. The API exposes no raw descriptor, cloning or write operation. Other operating systems explicitly report unsupported behavior. This library prerequisite activates no CLI command, schema change, lifecycle execution, descendant containment, observation or recovery. It does not complete the M1 filesystem stage or Windows qualification.

See AD-0028 and the qualified scope-lock design for rationale, acceptance examples and platform evidence.

Qualified destination observations

QualifiedScope::observe_destinations(&[FileLocation]) captures exact optional FileValue values beneath the borrowed physical scope. Regular files return binary bytes and Unix permission bits (Some(mode & 0o7777)); absent destinations return None. Observations preserve the supplied native roots and portable paths, sorted by scope, raw root spelling and relative path. Every supplied root must independently qualify and bind to the same physical directory, including ancestor-alias spellings and different Project/User labels. Empty input still validates the scope and filesystem profile.

Capture validates portable paths and rejects every potentially overlapping pair before descendant reads. Components may match when either contains non-ASCII characters or both compare equal ignoring ASCII case. If every component pair through the shorter path may match, the batch is rejected, including duplicates, prefix pairs and aliases across root spellings or labels. This conservative rule can reject distinct files on case-sensitive filesystems.

The inactive API qualifies APFS on macOS and ext-family/tmpfs filesystems on Linux. Linux requires a returned statx mount ID; macOS retains the fstatfs filesystem ID and mount bindings. Unknown profiles, missing mount evidence, cross-mount traversal and other operating systems return explicit errors. Descriptor-relative enumeration requires exact stored component spellings. No-follow traversal rejects symlinks, non-directory ancestors, directory leaves, files with multiple links and special files. Only ENOENT establishes absence; recoverable filesystem, permission, I/O, descriptor-exhaustion and unsupported failures remain contextual errors, including allocation errors reported by a fallible read. Recoverable memory exhaustion is not guaranteed: ordinary Rust infallible allocation failures may abort, and the operating system may terminate the process instead of returning Err.

An opaque, non-Clone DestinationSnapshot retains close-on-exec root, directory and file handles, selected root bindings, mount evidence and either exact file stamps or the first missing component with its unresolved suffix. Capture checks identity, type, links, size, mode, modification/change times (including fractional seconds) and byte count around each read. Before returning, and again from verify_unchanged(), it checks fresh bindings and directory identities, exact stored spellings, mounts, exact bytes, file stamps and absence evidence. These checks never refresh stale expectations. Timestamp-only changes, edits with bytes and mtime restored but changed ctime, same-size edits with restored mtime, chmod, identical-byte inode replacement, ancestor replacement, missing-to-present transitions and root-alias retargeting invalidate evidence. Access times and unrelated directory timestamp changes do not invalidate it. Retained handles close on drop and returned-error paths; process abort or termination does not guarantee Rust destructors run.

Observation creates no files, directories, locks or persistent state and acquires no writer guard. Plain observations convey no execution or permission authority. These sampled checks do not exclude external writers or hostile ancestor relocation. An applying executor must retain its writer guard and independently establish complete lifecycle inputs, authority, permission policy, immediate pre-effect checks and recovery. Existing commands and schema 1 remain unchanged; Windows qualification and complete observational lifecycle integration remain pending. The opt-in application boundary provides guarded execution and recovery for supplied plans. Hosted Linux runtime qualification and Windows unsupported-boundary checks passed on 2026-10-13 for PR head 765e70b9b7ce5386530ffea1fe9814b228724428 (merge checkout 242930ef9107aff6eff432484f474e47157983f2) in Actions run 38330080515. That evidence does not establish a qualified Windows backend or cover later local timestamp and special-bit tests. Independent regression controls now cover fractional mtime changes, metadata changes during acquisition, short reads with unchanged descriptor metadata, unknown native profiles and different retained mount bindings through the production qualification and verification paths. Existing-file overlap and repeated stale verification are also pinned. Linux’s missing returned mount-ID mask production-path case passed natively on Ubuntu in Actions run 38334882688 on 2026-10-13 for PR #273 head 2a50ca6a5163e11912cbcee7987593c68e0a4197. Ubuntu job 115092610337 passed 28 public destination cases; four native profile/mask check and production-call omissions failed their intended assertions. Each restored qualification.rs to SHA-256 1c5b288ed575ae1ccdd99a10170c13aa9054f92004a0927a64fb1587bac40bd5, followed by 22 restored private cases GREEN. Local APFS separately passed 28 public and 22 private cases plus 14 independent native omission controls. All 13 head checks succeeded, including Windows unsupported-boundary job 115092610428. This dated evidence covers that head; hosted CI for the final head after temporary harness removal remains required before merge. Injected query results and expected mount bindings establish neither real privileged mount or fault behavior nor a qualified Windows backend. See AD-0029 and the destination observation design.

Explicit file permission selection

files::permissions::select_file_mode(destination, source, resource_execution) is an inactive pure library API returning anyhow::Result<u32>. Its supplied-value inputs are DestinationMode::{Absent, Present(Option<u32>)}, PermissionSource::{Generated, Resource { captured_mode: Option<u32> }} and ResourceExecutionPolicy::{Disabled, OwnerExecutable}. These types derive Debug, Clone, Copy, PartialEq and Eq; they carry no serialized state or effect capability.

Every supplied destination and captured resource mode must be within 0o0000..=0o7777, including evidence unused by the selection branch. Errors identify destination or captured resource evidence. When multiple inputs are invalid, no particular diagnostic ordering is promised. Present(None) rejects as unknown existing mode. Existing modes containing any 0o7000 special bit reject as unsupported application policy; ordinary existing modes are returned exactly, including 0o000, 0o400, 0o640, 0o755 and 0o777, under either resource policy.

Absent generated output selects literal 0o600 under either policy. Absent resources also select 0o600 when execution is Disabled, including unknown captured mode. OwnerExecutable requires a known captured resource mode and selects 0o700 only when its owner-execute bit 0o100 is set; otherwise it selects 0o600. Captured 0o755 and 0o4777 therefore select 0o700; 0o011, 0o644, 0o1000, 0o2000 and 0o4000 select 0o600. Source group/other and special bits are never copied. Unknown captured resource mode rejects only when selecting new resource execution; an existing ordinary destination mode remains exact.

The function reads no environment or process umask and performs no I/O. Supplied values and selected bits convey no source authenticity, ownership, physical qualification or execution authority. Future writers must separately qualify owner/group, ACLs, extended metadata, containment and atomic replacement and persist actual resulting mode evidence. This API changes no compiler output, planner validation, CLI behavior or lockfile schema. The opt-in executor in qualified file execution and recovery supplies file application, journals and recovery. Combined lifecycle state, activation and hosted qualification remain pending; M1-A2, M1-A3 and M1 remain incomplete. See AD-0031 and the permission design.

Architecture

Qualified scope roots and writer locking

The library boundary is implemented; broader platform qualification remains pending.

Intent and boundary

Establish one process-safe writer exclusion primitive for the owned lifecycle, without changing current CLI behavior or writing state. This is the first part of the M1 filesystem stage. Destination observation, equivalence, containment, source enumeration, file application, recovery and Windows qualification remain separate gates. Completing this slice does not complete the filesystem stage.

Retain Rust 1.85, synchronous execution, a single binary, zero warnings and library-only presentation. Current commands remain on schema 1.

Components and interfaces

The filesystem module exports QualifiedScope and ScopeWriterGuard. Their implementation keeps selection, binding validation and the Linux/macOS directory-lock backend private. It consumes ownership::InstallationRoot; the scope label does not determine physical lock identity. Unsupported-platform branches return contextual errors. The real-filesystem integration tests exercise the public boundary and subprocess lifetime; private unit tests pin acquisition ordering and final-symlink refusal independently.

Alternatives and decision

Select a pinned existing directory handle and nonblocking OS advisory exclusive lock on that directory on Linux and macOS. It avoids creating a persistent lock file, removing a lock inode during release, stale PID cleanup and read-only preview writes. The inode, not a root spelling or Project/User label, identifies the lock.

A create-new sentinel needs a separate crash/stale-owner protocol and is rejected. A portable lock-file abstraction requires a securely established persistent lock namespace and Windows qualification; retain that as a future option. New APIs return an explicit unsupported error on other operating systems. Existing CLI behavior remains available there. Do not claim all supported-platform filesystem qualification from Linux/macOS evidence.

Admit the existing locked rustix 1.1.4 dependency directly, target-specific to Linux/macOS, with filesystem support. Record the dependency decision in AD-0028. Do not add an async runtime or hand-written unsafe syscall bindings. Standard library file locks require Rust 1.89 and cannot satisfy Rust 1.85.

Root contract

filesystem::QualifiedScope::open(&InstallationRoot) returns an opaque, non-Clone pinned root or an error. Require a nonempty absolute existing directory. Reject parent components, root symlinks, regular files, missing roots and special files. Normalize redundant current-directory and trailing separators before checking the final component, so they cannot hide a symlink. Preserve non-UTF-8 root paths without lossy conversion where the filesystem accepts them; propagate filesystem refusal contextually.

Ancestor symlinks are permitted during explicit ambient root selection, including macOS temporary-directory aliases. Open both the normalized selected path and its resolved directory with no final symlink following, and compare both metadata identities with the selected root. Retain original normalized spelling, resolved path and the resolved directory’s open identity. This binds the explicitly chosen root; it is not descendant containment or a security sandbox against hostile namespace relocation.

verify_binding() reopens the selected root and checks its directory identity, resolved path and nonsymlink final component. Renaming/replacing the root, retargeting an ancestor alias or substituting a symlink must reject the old scope. The pinned handle alone does not freeze the path namespace. Directory edits by other processes remain possible, and later action code must revalidate bindings and descendant preconditions immediately before each action.

Writer contract

try_lock_writer() acquires a fresh open description of the pinned directory, checks identity, obtains a nonblocking exclusive OS lock, then checks the root binding again before returning an opaque non-Clone ScopeWriterGuard borrowing the scope. Reopening must use the pinned descriptor (for example openat(".")), not a cloned description that shares a flock with another guard.

A competing same-process or cross-process writer to the same physical root returns a contextual busy error promptly. Different roots do not contend. Alias spellings and different scope labels cannot split exclusion. A dropped guard or terminated holder releases the lock through descriptor close. Do not expose the locked handle or allow cloning it. Use close-on-exec descriptors so ordinary subprocess execution does not prolong exclusion. This API promises no fork-safe lease, fairness, blocking wait, network-filesystem guarantee or protection against external editors. If the filesystem refuses locking, return that error; never pretend the lock succeeded.

Opening, validation, lock acquisition and release create no files, directories, backups, journals or schema changes and preserve existing bytes and modes. They do not establish state/source/destination preconditions or authorize writes.

Acceptance and evidence

IDRequired example
Q01Pin a real absolute root, verify binding, acquire/drop/reacquire without changing recursive directory contents or modes.
Q02Reject relative, empty, parent-traversing, missing, file and symlink roots, including trailing slash and dot disguises.
Q03Equivalent root spellings and different scope labels contend; independent roots can lock together.
Q04A second guard in the same process is busy while the first lives and succeeds after drop.
Q05A real child process holds the lock; parent is busy; graceful child exit and forced termination each permit reacquisition. Synchronize readiness and bound all waits.
Q06Replacement/rename/symlink substitution and ancestor-alias retargeting reject stale binding before a guard is returned.
Q07Native non-UTF-8 roots work where the filesystem accepts them; filesystem refusal remains contextual. Unsupported platforms return explicit errors without effects.
Q08A child exec from a holder does not retain the lock after the holder exits.

Tests use temporary roots and never write developer HOME. Capture failing acceptance tests before implementation, assertion-failure semantic controls for lock omission, missing binding validation and accidental description sharing, Rust 1.85 checks, full repository verification, Linux hosted contention/release and local macOS evidence. Windows tests pin the unsupported boundary.

Measure release CLI size against the same-toolchain baseline. Also measure a small release probe that actually calls root qualification and locking against a matched probe, because dead-code elimination hides an inactive library feature from CLI size. Record toolchain, target, exact bytes and interpretation; these measurements do not establish future executor size.

Historical execution results are retained in the filesystem validation record.

Qualified destination observation

Implemented with local APFS evidence and hosted Linux and Windows boundary checks. Windows qualification remains pending.

Intent and scope

Capture existing file bytes and Unix modes, or verified absence, beneath one qualified physical scope. Reject unsafe paths and potentially overlapping destinations before a future executor receives observations. Retain opaque evidence that can be revalidated while the caller retains its scope writer lock. Preview may observe without acquiring that lock and performs no application writes.

This is the next part of the M1 filesystem stage after AD-0028. It activates no CLI behavior, ownership adoption, permission authority, schema-2 persistence, source stabilization, file application or recovery. It does not establish that the caller supplied the complete lifecycle destination union. A future lifecycle orchestrator must derive that union and select exactly the affected observations for the pure planner. Windows qualification remains pending.

Alternatives and decision

Select descriptor-relative traversal with no symlink following, retained directory/file identities and a conservative batch overlap rule. Qualify APFS on macOS and ext-family filesystems and tmpfs on Linux. Reject other filesystems, cross-mount traversal and unknown qualification evidence explicitly. No new dependency is required beyond the admitted rustix 1.1.4.

Ambient path canonicalization followed by ordinary reads is rejected: checking a path does not bind subsequent accesses to that checked directory chain. Creating temporary names to discover comparison behavior is rejected because preview must not create files. Exact emulation of every filesystem’s Unicode comparison is also rejected: it adds version-dependent tables and still does not qualify an unknown filesystem.

APFS preserves filename spelling while supporting normalization and case insensitivity. ext4 can select Unicode case-insensitive lookup per directory. See the Apple APFS FAQ and Linux ext4 documentation. Use a conservative relation instead of claiming that byte inequality proves distinctness. This intentionally rejects some valid case-sensitive or Unicode batches. A later qualified comparison implementation may relax rejection without changing the safety boundary.

Public interface

QualifiedScope::observe_destinations returns a DestinationSnapshot borrowing the scope. The snapshot exposes observations() as a borrowed slice and verify_unchanged() as a fallible revalidation operation.

Export DestinationSnapshot from filesystem. Reuse files::plan::FileLocation, ObservedFile and FileValue. Existing regular files produce exact bytes and Some(mode & 0o7777); absent destinations produce None. Sort observations by the existing exact location key: scope, raw root OsStr and portable path. Permutation of locations must not change the returned data or acceptance.

The snapshot borrows its qualified scope and privately owns retained evidence. It is opaque, non-Clone and nonserializable, with redacted Debug. Returning or cloning plain observations conveys no execution authority. There is no raw descriptor, write method, lock acquisition or successful partial result.

Every location root must itself satisfy scope selection and remain bound to the same physical directory as the supplied QualifiedScope. Different scope labels and ancestor-alias spellings are permitted for distinct locations, but cannot split physical identity or the batch overlap checks. Retain and revalidate the selected spellings of additional roots. A different physical root is an error. An empty input returns an empty snapshot only after validating the scope and its filesystem profile.

Path and filesystem qualification

Validate all portable relative paths using the ownership path rules before accessing descendants. Reject empty, dot, parent, absolute, repeated-separator, backslash, colon and control-character paths. Preserve native root paths without lossy conversion. Error display may use the existing contextual presentation.

Qualify the pinned root with descriptor-based filesystem metadata. On Linux, accept the ext-family magic or tmpfs magic and require a returned statx mount ID. If the kernel cannot supply that evidence, return an explicit qualification error. On macOS, require the APFS filesystem name and retain filesystem identity and mount binding from fstatfs. Other operating systems remain unsupported. Every opened descendant must stay on the selected mount: Linux mount IDs must match, and macOS filesystem/mount identities must match. Comparing only device numbers is insufficient for same-device bind mounts. Recheck these facts during verification. Network, FUSE, overlay and other unqualified filesystems are not implicitly accepted.

Walk one portable component at a time relative to a pinned directory descriptor. Open each existing intermediate directory without following symlinks. Require the requested component’s exact bytes to appear in descriptor-relative directory enumeration, so a case or normalization alias is rejected rather than silently adopted. Compare the selected entry and opened directory identities. Retain the opened directories to prevent inode reuse from imitating the original chain. Use close-on-exec descriptors throughout.

Inspect the final entry without following symlinks before opening it; reject a known nonregular type before any file open. An existing final component must likewise have exact stored spelling, be opened without symlink following and be a regular file with link count exactly one. Reject directories, symlinks, hard links and special files. Nonblocking opening and type checks must prevent a FIFO or device fixture from hanging observation. Compare entry and open identities, qualify its mount and retain the file handle.

At the first missing component, retain the deepest existing directory and the entire unresolved suffix as absence evidence. Do not create directories or probe files. Only an actual not-found result establishes absence; permission, I/O, unsupported and malformed-path errors remain errors. A non-directory ancestor does not mean absence. Revalidate the existing chain and first missing component before returning the snapshot.

Conservative overlap rule

All locations resolve under one physical root. Reject exact duplicate locations and every pair whose portable paths may denote the same destination or an ancestor/descendant pair. Compare components in order up to the shorter path:

  • Two ASCII components may match when their ASCII lowercase strings match.
  • If either component contains a non-ASCII character, treat that component pair as potentially matching, regardless of its spelling.
  • Otherwise the components are distinct and the two paths cannot overlap.

If every compared component may match, reject the pair. This catches case-only collisions, Unicode normalization/case uncertainty, aliases across root spellings or scope labels, and potential prefix conflicts below missing parents. Apply the rule to the whole supplied set, including existing files. It is a conservative rejection rule, not a claim that every rejected pair is filesystem-equivalent. For example, a/x and b/y are distinct; a/X and a/x conflict; a/é and a/e conflict; a/é and b/e are distinct. A single Unicode path remains eligible when its existing components have exact stored spellings.

Also reject duplicate opened file identities as a physical backstop. Existing directory aliases introduced by mount traversal are excluded by mount checks. No permission to remove or replace anything follows from successful observation.

Capture and revalidation

Capture the regular file through its retained handle. Require stable identity, type, link count, size, permission bits, modification time and change time, including fractional seconds, across the read, and require the read byte count to match the sampled size. Do not use access time as a precondition: reads can update it. Capture failure returns no snapshot. File data may be binary; never decode or normalize it.

Before returning, revalidate all selected root bindings, every directory chain, mount evidence and final entry identity or absence. Recheck captured file metadata and exact bytes. verify_unchanged() repeats those checks against the retained evidence and exact observations; it never refreshes the snapshot into a new accepted state. Detect same-length byte edits even when modification time is restored, timestamp-only changes, and edits with bytes and modification time restored while change time differs. Also detect permission changes, identical-byte inode replacement, ancestor replacement, missing-to-present transitions and root-alias retargeting. Unrelated sibling edits do not invalidate a snapshot merely by changing directory times.

These are sampled observations under the same namespace assumptions as AD-0028. Pinned handles and revalidation do not freeze external writers, prevent hostile ancestor relocation between checks or form an operating-system security sandbox. A future executor must hold the writer guard, validate state/source authority and permission policy, recheck destination evidence immediately before each effect, and record interruptions. This API provides none of those effects or recovery.

Recoverable filesystem, read and descriptor-exhaustion failures return contextual errors; only not-found establishes absence. Allocation errors reported by a fallible read also return contextual errors. The API does not guarantee recoverable memory exhaustion: ordinary Rust infallible allocations, including captured-byte cloning, may abort, and operating-system process termination may prevent a return of Err. Retained handles close on drop and on returned-error paths; process abort or termination does not guarantee Rust destructors run. No production retries or silent fallback to ambient path reads are allowed.

Contract and acceptance coverage

The requirements are recorded in qualified destination observations. The public acceptance suite covers capture, absence, candidate validation, unsafe entries, alias handling, adversarial changes and opaque evidence. Focused private tests in the backend, qualification and candidate modules independently pin retained evidence and comparison rules. Filesystem fixtures use temporary roots; privileged mount behavior remains outside the exercised contract.

Historical execution results are retained in the filesystem validation record.

Explicit file permission selection

This design describes the implemented inactive pure library API.

AD-0026 separates captured resource modes from generated output permissions. The selected next prerequisite adds a pure selection interface before any qualified file effects.

Existing ordinary permission bits are preserved exactly. Unknown existing modes and existing special bits are errors. New generated output uses 0o0600. New resources also use 0o0600; an explicit resource-only execution policy may select 0o0700 when captured evidence contains the owner-execute bit. Source group/other and special bits are never copied. Supplied modes outside 0o0000..=0o7777 reject even when selection would not use them.

This is a supplied-value policy, with no I/O or process-umask input. It grants no ownership, containment, metadata-preservation or execution authority. The separate qualified executor enforces a restricted owner/group and metadata envelope before atomic replacement. The compiler, planners and current CLI remain unchanged. Lifecycle state persistence and activation remain pending.

Public interface and examples

files::permissions::select_file_mode accepts DestinationMode::{Absent, Present(Option<u32>)}, PermissionSource::{Generated, Resource { captured_mode: Option<u32> }} and ResourceExecutionPolicy::{Disabled, OwnerExecutable} and returns anyhow::Result<u32>. All three enums derive Debug, Clone, Copy, PartialEq and Eq. The API has no serialized representation or effect capability.

An existing 0o000, 0o400, 0o640, 0o755 or 0o777 remains exact for either source kind and either resource policy. Existing 0o1000, 0o2000, 0o4000 and 0o2640 reject rather than losing special bits. Unknown existing mode rejects.

For an absent resource with OwnerExecutable, captured 0o100, 0o755 and 0o4777 select 0o700; captured 0o011, 0o644, 0o1000, 0o2000 and 0o4000 select 0o600. Unknown captured mode rejects for that selection but is allowed with Disabled or an existing ordinary destination mode. Out-of-range captured evidence rejects even when an existing destination mode would otherwise win. Contextual errors distinguish destination evidence, captured resource evidence, unknown existing mode, existing special bits and unknown executable-resource mode.

The implementation selects from supplied values. The public acceptance tests exercise literal modes and diagnostics. Independent omission controls pin preservation, unknown-mode handling, explicit policy, owner execution, special-bit handling and both evidence ranges. No mutable input exists: all inputs are Copy values and the function consults no filesystem, environment or clock.

See specification 4.10 and AD-0031. The opt-in executor applies selected bits only after qualification; hosted executor qualification and lifecycle integration remain required. M1-A2, M1-A3 and M1 remain incomplete.

Edit the canonical source.

Execution and recovery

This concern owns the requirements and implemented architecture below. Moving these records does not change their implementation or approval status. Library-only capabilities remain separate from production CLI activation; each section records its own limits. Accepted decisions remain in ADRs.

Requirements

Ordered recovery checkpoint model

operations::recovery::plan_recovery(&[RecoveryAction]) is an inactive pure library API. Supplied File and State actions contain Queued/Pending/Done checkpoints and Before/After/Both/Neither relations to exact expected values. Exactly one State must be final. Empty inputs reject; state-only operations are valid. The complete checkpoint order must match Done* Pending? Queued* before observations are classified. Structural errors identify the required State condition or invalid position.

Done requires After or Both; Pending accepts Before, After or Both; Queued requires Before or Both. Every observation is checked, including the suffix. The first conflict returns Reconcile with its index and both prefixes. recorded_prefix preserves the count of leading Done checkpoints even when contradicted. verified_post_prefix counts consecutive leading Done post-state observations, extended through one Pending After or Both, and stops at the first mismatch. Queued never extends it.

When all actions are Done, Complete includes the final State. Otherwise Ready returns one step at the first unfinished index and both prefix counts. Pending Before and Queued Before propose Apply; Pending After or Both proposes Acknowledge; Queued Both proposes CheckpointOnly. Apply carries no physical write authority, and the function changes no checkpoint. Public model types derive Debug, Clone, Copy, PartialEq and Eq.

Supplied classifications are not evidence capabilities. The API performs no I/O, serialization, journal reading, qualification or effects. The opt-in executor in qualified file execution and recovery supplies qualified file application, receipts and recovery using this model. No lifecycle caller or schema-2 writes are activated; M1-A2, M1-A3 and M1 remain incomplete. See AD-0032 and the recovery model for a public API example.

Qualified file execution and recovery

filesystem::ExecutionRequest supplies a complete ordered sequence of PlannedFile values and one distinct final State file that remains present. Its effect-free validator rejects invalid or cross-scope paths, duplicate or ancestor destinations, reserved journal paths, unknown/special permission bits, changed existing ordinary modes, and new-file modes other than selected 0o600 or 0o700. The caller remains responsible for ownership and complete planning.

QualifiedScope::apply_operation requires its matching live writer guard and verifies all supplied source snapshots and exact destination preconditions before the first output effect. recover_operation requires the same authority and uses validated persisted payloads without fetching sources. Both are opt-in library methods; current CLI commands and schema-1 lockfiles do not use them. Unsupported operating systems reject explicitly. Supported Linux/macOS paths retain the existing qualification requirements.

QualifiedScope::apply_operation_with_backup prepends the exact original state.before bytes as a private mode-0o600 backup action. It requires the backup to be absent or byte- and mode-identical, validates the full request before effects, and atomically publishes new backups without replacement. Matching existing backups are synced with their parent and retain identity. Backup operations use execution-journal schema 2: exactly one create-only first action is bound to the final State’s original byte digest and length. Ordinary journal schema 1 remains supported. Recovery verifies the backup before subsequent effects; edits require reconciliation, and a matching Pending publication is acknowledged without rewriting. Report prefixes include the backup at index zero. The caller validates the lifecycle transition; this primitive does not parse original JSON or enable schema-2 production writes.

Effects replace files through exclusive sibling staging, file sync, atomic rename and parent-directory sync, or unlink individual planned files and sync their parents. Rechecks precede each effect. Unchanged files retain identity; directories, extra files and empty shared aggregates are retained. Existing ordinary modes are preserved. Replacement verifies unchanged UID/GID. ACLs, special bits, Darwin file flags and unsupported extended attributes reject; Darwin provenance is allowed only with an equal value on prior and staged files.

The private <scope-root>/.upskill/state/ store binds root and directory identities. Its directory is 0o700 and private files are 0o600. The root and store parent must not allow group/other writes. Both execution-journal schemas record the complete ordered actions, before/after digests, lengths and modes, bound desired payloads, final State, generations and Queued/Pending/Done receipts. Invalid paths, unsupported records, payload changes and mismatched storage bindings reject without output effects. Neither journal schema activates a new lifecycle lockfile schema.

Desired payloads are synced before initial journal publication. Each generation uses staged-file sync, atomic publication and directory sync. Persist Pending before each effect, sync and reobserve its post-state, then persist Done. A Pending post-state is resynced, including its parent directory, before recovery acknowledges it; it is not rewritten. A Queued post-state or unexpected value requires reconciliation. No-op actions use receipt-only checkpoints, except matching backup prerequisites which are synced before Done. Final State uses the same protocol. Reports distinguish the recorded Done prefix from the currently verified post-state prefix.

Completed cleanup is durably marked before removing validated payloads. The completed journal is removed last; conflicting or unknown files are preserved. Read-only observation creates no execution artifacts. Recovery with no published operation performs no output work. Interrupted initial setup may retain private orphan files rather than deleting content without established authority.

Advisory locking coordinates cooperating writers, not all external edits; rechecks cannot exclude every namespace race. Operations are not atomic across files. Process interruption coverage does not establish hardware power-loss durability. Gate-4 hosted evidence is retained in its validation record. Combined lifecycle integration and M1 activation remain separate work. See AD-0033 and the qualified execution design.

Combined lifecycle backup and recovery

When the opt-in lifecycle migrates observed schema-1 state, its restricted, create-only backup must match the observed original bytes before any output effect. A mismatched existing backup rejects without changing state or outputs. Fresh initialization requires absent state and backup, creates private schema-2 state and journals backup absence without inventing historical bytes.

An interruption at an output or final-state checkpoint must recover the captured operation without duplicating unknown effects, losing retained records or overwriting unrelated edits. Recovery reuses the qualified executor’s persisted intent and checkpoint protocol under a new matching writer guard. Late external edits requiring reconciliation remain preserved with pending recovery; successful recovery leaves no pending operation. Inspection never performs recovery.

This contract is exercised through the opt-in lifecycle session and source orchestration boundaries. It does not activate schema-2 production writes or establish native-client behavior or hardware power-loss durability. The process acceptance design describes the evidence and accounted staging residue.

Architecture

File recovery checkpoint model

This design describes the implemented inactive pure library API.

operations::recovery::plan_recovery consumes a borrowed slice of supplied RecoveryAction values and returns anyhow::Result<RecoveryDecision>. Each action has an ActionKind (File or State), Checkpoint (Queued, Pending, Done) and ObservedRelation (Before, After, Both, Neither). All public model types derive Debug, Clone, Copy, PartialEq and Eq.

Supplied classifications

File represents one complete planned file edit, including an instruction aggregate. State represents the final ownership-state checkpoint without choosing its schema or path. Exactly one State action must be final; state-only operations are valid. Checkpoints must follow Done* Pending? Queued*. The complete sequence is structurally validated before any observation decision; invalid structure returns a contextual error with the position or required State condition. Diagnostic ordering is unspecified when multiple structural conditions are invalid.

Observations compare supplied current values to exact expected before and after values. Before and After match only their respective values; Both matches both and represents an unchanged action; Neither matches neither. These classifications are not qualified filesystem evidence. Native operations are not representable.

Every Done action must match After or Both. Pending may match Before, After or Both. Queued must match Before or Both: a coincidental unstarted After observation does not establish application. All observations are checked, including the suffix after the next proposed step. The first conflict in operation order returns Reconcile { index, recorded_prefix, verified_post_prefix }.

Prefixes and next decisions

recorded_prefix counts leading Done checkpoints even if current observations contradict them. verified_post_prefix counts only the consecutive leading Done post-state observations, extended by one Pending action at After or Both. It stops at the first mismatch; Queued never extends it, including Both.

After full validation, Complete { completed } reports the number of actions when all are Done, including State. Otherwise Ready returns both prefixes and one RecoveryStep at the first unfinished index:

  • Pending Before or Queued Before proposes Apply { index }.
  • Pending After or Both proposes Acknowledge { index }, avoiding repetition.
  • Queued Both proposes CheckpointOnly { index }, with no physical write.

For a state-only operation supplied as Pending and After, the decision is Ready with recorded prefix 0, verified post-state prefix 1 and Acknowledge at index 0. The pending final state write is recognized without proposing it again. See the public model and planner and the literal acceptance cases.

Apply is a proposed logical step, not permission to write. The function changes no checkpoint and skips no action. The opt-in qualified executor obtains fresh qualified facts and advances durable checkpoints before another application decision.

Evidence and remaining contracts

tests/recovery_plans.rs pins all twelve checkpoint/relation combinations independently for both roles, whole-suffix validation, first-conflict selection, distinct prefixes and final State handling. Independent compiled semantic controls establish that these assertions detect omitted guards and altered decisions.

This pure API performs no I/O, serialization, journal reading, byte comparison, locking or effects. The opt-in qualified executor supplies physical observations, private desired payloads, durable receipts and file application in the restricted metadata envelope recorded by AD-0033. The permission-selection policy remains separate. Hosted executor qualification, lifecycle state integration and activation are pending; M1-A2, M1-A3 and M1 remain incomplete. See AD-0032.

Qualified file execution and recovery

This design describes the opt-in library executor in src/filesystem/execution/. Current commands do not call it, and it does not enable lifecycle-schema-2 writes or establish installation ownership. Gate-4 delivery evidence is retained in its validation record; combined lifecycle integration and M1 activation remain separate work.

Request and authority boundary

ExecutionRequest contains ordered files: Vec<PlannedFile> and a final state: PlannedFile. ExecutionRequest::validate checks paths, scope equality, permission values and destination topology without filesystem effects. All paths share an absolute scope root, must be valid relative paths, and must not collide or overlap by ancestry. .upskill/state/ and its ancestors are reserved. State must be distinct and present after execution. State-only requests are valid; unchanged and removal actions retain their ordered positions.

QualifiedScope::apply_operation borrows its matching ScopeWriterGuard, the caller-supplied source snapshots, and the complete request. It verifies those sources, qualifies every destination and its metadata, compares every exact before value, then revalidates source, destination and guard bindings before creating durable intent. recover_operation uses the same guard and the saved payloads; it performs no source fetch. A caller must provide the complete ownership-aware plan and all source evidence relevant to it. Neither API selects source refs, installations, consumers or ownership policy.

ExecutionReport contains recorded_prefix, verified_post_prefix and ExecutionOutcome::{Complete, Reconcile { index }}. Conflicting observations return Reconcile without modifying the conflicting file. Invalid requests, corrupt journals and I/O failures return contextual errors; errors during execution include recorded and verified prefixes; failed qualification reports a conservative verified prefix of zero because current observations are unavailable. Complete includes the final State action and successful qualified cleanup.

Exact original-state backup prerequisite

QualifiedScope::apply_operation_with_backup takes the complete request, guard and source snapshots plus a backup FileLocation. It requires an observed state.before, prepends its exact original bytes as action zero, and validates the complete resulting topology. It does not parse or reserialize those bytes; the caller remains responsible for the lifecycle transition. The backup must be absent or already match the original bytes and private mode 0o600. A mismatch rejects before journal creation or output effects. All other destination and source preconditions are also checked before the backup is created.

New backups use the qualified sibling staging protocol and renameat_with(NOREPLACE), which atomically refuses an existing destination. Unsupported filesystem behavior fails closed. A matching existing backup is synced with its parent before its Done receipt and retains physical identity. No backup is overwritten or removed by this method.

Backup operations use execution-journal schema 2, independently of lifecycle lockfile schemas. It requires exactly one create-only first action, mode 0o600, an absent or identical before value, and byte digest/length equal to the final State’s original evidence. Following actions use ordinary execution. Original execution-journal schema 1 remains accepted and cannot contain a create-only action. Removing, moving or downgrading the create-only marker rejects. Older binaries reject the new journal version; recovery requires a supporting binary.

Recovery verifies the backup alongside the entire action sequence before each step. An interrupted publication with a matching Pending post-state is synced and acknowledged without rewriting it. Editing even a completed backup blocks later outputs and final State. Reports include the backup at index zero in their recorded and verified prefixes. Cleanup retains the backup. This generic primitive does not validate schema-1 JSON, prepare lifecycle ownership, or activate production commands.

The persisted prerequisite is recorded in AD-0037.

Components and file effects

request.rs validates the supplied plan. backend.rs implements qualified descriptor-relative observations, parent traversal, sibling staging, replacement, unlink and sync. journal.rs owns storage, strict decoding, payload binding, generations and cleanup. run.rs classifies fresh observations and executes only steps permitted by operations::recovery::plan_recovery.

Before each effect, the backend rechecks the selected destination and parent binding. A replacement uses an exclusive 0o600 sibling staging file, verifies its identity, bytes and selected mode, syncs it, renames it relative to the held parent, and syncs that directory. Newly created output parents use 0o700 and have their bindings synced. Removal unlinks one planned unchanged file. Unchanged files retain physical identity. Output directories, extra resources, manual aggregate bytes supplied by planning and empty aggregate files are not recursively removed.

Existing ordinary permission bits must remain exact; unknown modes and special bits reject. New outputs require 0o600 or explicitly selected 0o700. Existing destination UID/GID must match the qualified root, and replacement verifies that the staged file retains the prior UID/GID. Linux extended attributes, Darwin ACLs and file flags, and other unsupported metadata reject. Darwin permits only com.apple.provenance; replacement proceeds only if its value matches on prior and staged files. The implementation does not copy or repair ACLs or metadata.

macos_acl.rs uses minimal Darwin ACL inspection through the system ABI without a new dependency. The ABI and absence interpretation follow the SDK sys/acl.h and Apple Libc posix1e/acl_file.c and gen/filesec.c: NULL with ENOENT on a live file descriptor denotes an absent ACL property. Other failures reject, as do ACLs containing an entry.

Private storage and strict journal

The store is <scope-root>/.upskill/state/. The root and .upskill parent must not be writable by group or others. The state directory is 0o700, is on the qualified mount, and has ownership and physical bindings checked. Private files are 0o600, single-link regular files with qualified metadata and bindings. owner.json binds the root and directory identities. A generated .gitignore contains * and !.gitignore. Existing state without matching owner and ignore records rejects; arbitrary existing content is not adopted.

operation.json uses execution-journal schema 1 for ordinary operations and schema 2 for backup operations, independently of the CLI lockfile schema. Strict records reject unknown fields and contain a random operation ID, generation, root identity, ordered action paths, a final State marker, before/after SHA-256 digests, lengths and ordinary modes, payload names and physical identities, Queued/Pending/Done receipts, and a cleanup marker. Payloads hold desired file/state bytes and, for backup operations, exact original State bytes as the desired backup. Source captures and old output copies for rollback are not retained. Original State can contain sensitive user data; the explicit backup preserves those bytes in private storage. Payload names, identities, sizes and hashes, complete path topology and checkpoint order are revalidated before recovery may act. Persisted paths never become unchecked execution authority.

Checkpoint ordering and forward recovery

Desired payloads are exclusively created and synced before the initial journal is published. Each journal generation is written and synced to a private exclusive file, checked against the prior publication, atomically renamed, and followed by a state-directory sync.

An effectful action follows this order:

  1. Publish and sync Pending.
  2. Reobserve and verify the selected destination and parent binding.
  3. Replace or unlink the file, sync its parent, and observe the desired state.
  4. Reobserve the complete action sequence and publish Done through the recovery acknowledgement path.

Each recovery iteration qualifies observations and metadata for the whole sequence before asking the pure recovery planner for a step. This includes Done and unchanged Queued actions, existing parents, and destination UID/GID, before any effect, receipt advancement or cleanup. Pending Before permits application; Pending After or Both permits acknowledgement. Acknowledgement resyncs the observed file, when present, and its parent directory before Done, covering an interruption or error after the namespace effect but before its sync. Queued Both uses a receipt-only checkpoint for ordinary actions; an unchanged backup prerequisite is synced with its parent before Done. Queued After and unexpected values require reconciliation. Recorded completion and currently verified completion are kept separate. Final State follows the identical protocol.

After every action is Done, cleanup first publishes cleanup_started in a new durable generation. This permits later recovery to recognize completion after some payloads have already been deleted. Remaining payloads are removed only when their names, identities and bytes match the saved evidence. The journal is removed and its directory synced last. Unknown files and the private store’s owner/ignore records remain. Cleanup conflicts preserve the completed receipt. An interruption before initial publication can leave private setup or payloads; recovery with no published operation performs no output work and does not collect arbitrary orphan files.

Guarantees and qualification evidence

This backend relies on qualified local Linux/macOS filesystem operations and explicitly rejects unsupported hosts/forms. Scope locks coordinate cooperating writers, including recovery; process exit releases the lock. Rechecks and pinned handles detect observed external changes, but cannot prevent every namespace race or make a multi-file operation atomic.

The focused tests beside each component cover request validation, exact preconditions, ordinary modes, metadata rejection, journal corruption, payload identity, manual edits and prefix reporting. process_tests.rs terminates real child processes at intent/effect/sync/receipt, initial journal publication, generation publication and completed-cleanup boundaries. It checks exact final bytes and modes, retained file identity after observed effects, writer-lock release, and preservation of unexpected edits and extra files. A compiled Pending-omission control was observed to fail these assertions. These are process interruption checks, not hardware power-loss tests.

The local full test suite and Rust 1.85 all-target check passed for the candidate implementation. Independent review identified parent-preflight, error prefix and public-export findings; each has a regression fix. Final just verify, all-target Clippy and the Rust 1.85 all-target check passed after those fixes. Exact-head hosted results are retained in the gate-4 evidence record. Configuration alone is not execution evidence. Durable lifecycle state, schema-2 activation, legacy adoption and native Codex qualification remain separate work.

See AD-0033 and qualified file execution and recovery.

Validation results and tested source identities are retained in the gate-4 evidence record.

Guarded lifecycle application

The opt-in lifecycle::application boundary binds a complete owned-artifact plan to the combined state written by the same qualified execution request. It requires exact schema-1 state evidence when migrating an observed schema-1 lockfile and requires the resulting state’s artifact records to equal the planner output. The request is applied under the matching QualifiedScope writer guard. The existing executor creates the private exact-byte backup before output actions for that schema-1 migration and uses its durable journal for state-last recovery. Fresh initialization uses the origin-bound boundary below and journals backup absence instead of creating historical bytes.

lifecycle::application::recover delegates to the same qualified executor recovery API under its matching writer guard. It does not perform inspection or recovery without an explicit caller invocation.

See the validation record for backup and recovery examples and remaining Gate 5 work.

Origin-bound lifecycle application

lifecycle::application::apply_bound_schema2_write accepts captured optional state and backup values, a complete file plan, proposed combined state and retained source snapshots. It requires canonical lockfile/backup paths, validated state and complete artifact agreement. A matching scope writer guard and executor checks remain necessary; the records alone grant no authority.

For an absent lockfile, proposed origin must be fresh, and the backup must be absent. The executor receives a backup-absence no-op and a new private mode-0600 state action. For observed schema 1, the proposed migrated origin must bind its exact bytes and retain its typed snapshot; existing create-only backup execution runs before output effects. For observed schema 2, origin and legacy history cannot change. A migrated backup must match the original digest/length and mode 0600, while fresh state continues to require backup absence. Unknown provenance requires reconciliation rather than a guessed migration.

This entrypoint reuses existing source/destination rechecks and checkpointed recovery. The captured command adapter now recovers before state capture, retains session evidence and consumes its prepared operation through this bound writer. Recovery always stops the new request, including successful recovery; retry opens a new session. The source-backed orchestration adapter now opens this session before resolution and retains full captures through scoped application, rechecking source trees and actual Git revisions immediately before the captured operation applies. It reuses this writer and recovery protocol. Production activation and complete Gate 5 delivery remain pending. See AD-0038.

Lifecycle process acceptance

The test-only process driver records emitted executor events from successful source-backed fresh and migrated add, update and remove operations. It restores the before-state and starts a new child at each discovered event, including backup and final-state actions. The parent rejects a concurrent writer, terminates the child, changes source bytes and opens a new lifecycle session to recover persisted intent. Late output edits must instead produce reconciliation while preserving those edits, old combined state and pending recovery. Successful recovery must leave no pending operation and permit a subsequent ready session without replay.

The comparison checks regular-file bytes and modes outside executor storage under .upskill, including outputs, combined state and migration backup. An observed sibling .upskill-stage-<nonce> is accounted separately only when it matches any expected final regular file’s bytes, mode and parent directory. The match does not bind the stage to an interrupted action token. Backend staging writes and verifies the action’s after value before its effect hook. Directory modes, symlinks and filesystem identity are outside this image comparison.

Such stage residue remains tracked in debt #316. These tests establish neither native-client handling of staging names nor hardware power-loss durability. The ignored subprocess probe introduces no production hook or flag. The separate actual CLI test checks production schema 1 and backup absence with isolated HOME. The acceptance evidence maps all ten accepted criteria to tests and records validation and delivery limits.

Satisfies: combined lifecycle backup and recovery and the combined lifecycle contract.

Edit the canonical source.

Customization delivery, discovery and activation

Status: intended architecture and acceptance contract, reconciled on 2026-10-19 and extended with shared-catalog evaluation requirements on 2026-10-20. The 2026-10-24 native-first rules decision and implementation-order amendment are reconciled with retained slice-1 evidence on 2026-10-25 for user adoption review under AD-0040. This contract does not describe newly implemented adapters or qualify a client. The roadmap owns release order; AD-0040 resolves conflicts with earlier experimental instruction plans. A proposal dated 2026-10-23 suggests managed per-client packages for skills and subagents, and places subagents in the MVP; its amendments are listed in AD-0040’s proposed amendments and are not adopted here.

Architecture and authority

Delivery places files or registers a package. Discovery makes content available to a runtime. Activation loads instructions, invokes a skill or agent, starts a server, registers a tool or executes a hook. These are separate outcomes. Ownership authorizes upskill changes; it does not authorize an incidental reader.

Always-on rules use native loading where available and a qualified trusted hook where no native route exists. Codex and OpenCode delivery must be independent of team AGENTS.md; explicit OpenCode paths are a candidate, not a mandatory plugin route. See the selected routes and retained limits. Install, update and removal must leave team instruction bytes and modes unchanged. File placement or packaging alone is insufficient: the selected route must deliver guidance at the required lifecycle points with scope, precedence, exclusion and activation evidence. CD-04 records the bounded Codex history-duplication allowance; it does not waive planner deduplication. A skill inside a package is not automatically an always-on rule. The exact event/API and package storage remain adapter qualification decisions. Missing capabilities block that route; they never authorize an AGENTS.md rewrite fallback.

The merged Codex contribution compiler and with_prepared_codex are experimental library mechanisms. Their explicit reader reports and guarded editing tests remain useful foundation evidence. They are not the approved production rule architecture, and identical Markdown for potential readers does not establish intentional sharing. No existing experimental installation is silently adopted or converted to a plugin installation. Recorded removal and recovery retain their root/ownership safety contracts.

Claude and Copilot rules use native rule loading where qualification establishes the requested behavior. Skills/resources use qualified native discovery where possible, even when packaged in a plugin. Agents retain native client formats; new owned-agent management is not added to M1. Bundles compose installations and references; native plugins remain separate packages. Existing plugin/MCP functionality must remain compatible without advancing general hooks, tools, permissions or expanded MCP management into this implementation.

Client representation baseline

AD-0041 selects separate generated representations per intended client, with explicitly qualified sharing as an exception. One authored source supplies copied outputs; each logical item/client has one selected representation and effective native exposure. Resources belong to that selection. A generated authored default is not an installed shared catalog, and a default need not exist.

Retain the one-copy unscoped Claude/Copilot rule exception and native-first routes. Other sharing requires compatible declared readers, preserved capabilities and qualified exclusion, duplicate and lifecycle controls. A client-named directory or equal bytes cannot establish isolation.

Selection and lifecycle reuse source capture, provenance, owner/reference contracts, preflight, locking, backups and recovery. Preserve manual content, local edits, surviving references and native state. Removing a selected client replacement makes it unavailable; switching to a default requires an explicit, qualified selection. This future lifecycle policy does not change schema-1 behavior or implement active source overrides.

This adoption does not settle exact paths, package granularity/defaults, new owned-agent MVP scope, OpenCode skill sources, Codex distribution/trust or any other pending scope amendment. The remaining step-7 decisions, preservation contracts, Q10/Q11 and CD-01 through CD-11 gates remain required. Skills/resources stay first, then rules and automatic maintenance; the four-client release scope is unchanged.

Shared content root evaluation

Evaluate .agents/{rules,skills,agents} with client-specific adapters under #349. This is a candidate content layout, not an adopted production default. Compare intentional .claude sharing and selective Copilot/OpenCode Claude-compatibility exclusions as alternative profiles before selecting a root. A skill-directory exclusion does not establish exclusion of rules, agents, commands or explicit imports. The repository research note docs/technotes/shared-content-root-evaluation.md records current source evidence and route candidates. CD-11 below defines the coexistence gate.

Treat .agents/skills as intentionally shared discovery space for a declared shared-catalog installation. Prefer one shared native entrypoint per logical skill and client; a client lacking qualified direct discovery needs one adapter route, not an additional private-looking compatibility copy visible to other clients. Client-specific variants stay outside all shared catalogs. A variant and shared base may not both remain exposed to the same client. Native source namespaces, aliases and package exports do not redefine logical identity to hide duplicates.

.agents/rules and .agents/agents are source-catalog candidates, not universal native formats or proven inert storage. Each adapter retains qualified native scoping and restrictions. Codex/OpenCode rules still require independent delivery beside unchanged team AGENTS.md; the native-first amendment permits explicit OpenCode instruction paths after Q5 PASS. Shared skills must be directly consumption-ready; raw client directives or silently weakened permission semantics cannot be placed there. An audience exclusion chooses another qualified representation or fails preflight rather than silently publishing to shared discovery.

Shared authoring does not require shared native discovery. A common procedure can remain in the canonical body with generation-time client directives for small orchestration differences, or in copied resources explicitly read by client-specific wrappers for larger differences. Whole-body overrides replace the canonical body and do not compose with it. Their specified semantics are not implemented by the historical installer or captured compiler; see format §2.3 and AD-0025. Resource copies do not create additional skill entrypoints. Qualify the native read, resource path and permission behavior separately; references do not register agents or grant tools/MCP access. An embedded composition/include feature and override execution are separate work, outside this Gate 6 change.

This intent does not adopt existing schema-1 outputs or manual content, add owners from compatibility readers, or authorize moving installed files. Select one effective native route per logical item/client before effects and preserve manual/third-party entries and surviving direct/bundle references. Require per-client retained PASS before implementing a candidate skill adapter and integrated qualification before adopting its route. The user’s skills-before-rules implementation order supersedes the former all-four-feasibility-first and rules-before-new-owned-skills implementation dependency only. Gate 6 still requires selected M1 rules and preservation of existing kinds; the new skill layout and combined four-client MVP release remain blocked until qualified (AD-0040 amendment, CD-11). An unadopted layout has no production effects. All applicable platform/scope and existing regression obligations still apply to schema-2 activation. Generated files and earlier independent-rule probes do not satisfy shared-layout qualification.

Reader and activation policy

Every adapter/profile must declare the client/runtime/surface, exact version, OS, project or user scope, effective roots and configuration. Its record includes all entrypoints, resources, package payloads, native registrations, imports, recursive discovery, compatibility directories, configured globs, ancestor and nested roots, shared paths and relevant external-home overrides. Inventory all four clients, plus known supported IDE/compatibility surfaces. Unknown behavior is a blocker for an isolation claim, not an empty reader set.

For each possible reader record intended, unintended or unresolved, with its discovery condition. Declared shared-catalog consumption under the policy above is intentional; other cross-client consumption defaults to unintended unless explicitly selected and accepted for that installation/profile. Audience/body compatibility, equal bytes, a directory name or a warning alone never supplies that consent. A requested audience exclusion must prevent activation, not just omit one generated copy. Incidental readers gain neither owners nor references.

A shared artifact is permissible only for explicitly intended consumers with qualified equivalent behavior, compatible resources and recorded owner contracts. Otherwise select an independently qualified route or fail preflight. Do not disable existing user/team discovery or rewrite their settings to make a negative control pass. Existing configurations with imports/globs that prevent isolation are explicitly unsupported until an approved representation works.

Native catalogs and prompts must distinguish availability from activation. Each unintended-reader negative needs a successful positive control in that same client and an invocation relevant to the customization. For skills and agents, inspect native catalog/registration and explicit invocation as well as behavior; no marker in an unrelated prompt does not prove absence from discovery. Use registration/event counters or equivalent native receipts to distinguish repeated activation: a single final marker cannot prove exactly-once loading. Test same-name and alias collisions, project/user overlap, compatibility copies and direct-plus-plugin installation. One logical installation activates at most once per intended event/invocation. Intentional repeated user invocations are separate events. CD-04 separately records the selected Codex recorded-history limitation without claiming provider-effective duplication or waiving planner deduplication. Client-specific precedence or deduplication is evidence to verify, not a portable assumption (resume-only, AD-0040).

Preservation contracts

Every matrix row below inherits these installation, update and removal rules:

  • Files (F): capture exact entrypoint/resource bytes and modes, enumerate owned resources individually, reject unowned collisions even when bytes match, and preserve manual files, unrelated owners and references. Update/remove rejects edited owned artifacts; no recursive deletion of a mixed directory. Rule resources must not become instructions through a broad Markdown glob.
  • Rules (R): F plus no team AGENTS.md, override-file or imported-manual-text changes for independent plugin rules. Native Claude/Copilot rule files remain separately owned; existing bridge/settings entries need their own ownership evidence. Scope mappings must preserve matching and nonmatching behavior.
  • Native registrations (N): F for package/config files plus observe existing registration, scope, enablement, trust and inverse actions. Add only owned entries; update/remove preserve other keys, comments, order where meaningful, credentials and manual registrations. Unknown/pre-existing registrations are conflicts or externally satisfied objects, not automatically owned. File success is not native effect success. Recovery observes native outcomes before retrying, and blocks if it cannot safely determine the result.
  • Composition (B): preserve dependency identity, source/ref, aliases and direct/bundle reference edges. Removing one bundle releases only its edges; surviving direct/other-bundle references retain content, resources and native registrations. No new retention is inferred from incidental readers. Existing explicit integration-removal policy remains; narrowed selection cannot silently uninstall a plugin or MCP server.

Reuse complete selected-set preflight, captured source/destination evidence, qualified roots and writer locking, exact schema-1 backup, owner contracts and journaled recovery. Extend native-effect planning where needed; the file journal alone is not proof of native registration recovery. Keep schema-1 production routing until every applicable activation requirement passes. Never expose a schema-2 writer alongside legacy recursive removal or save only the legacy projection over an outer owned state.

Per-client delivery matrix

The paths here distinguish current output from intended routes. They are not new path guarantees. Project/user roots, package locations and compatibility settings must be pinned for each released version before qualification.

Status vocabulary: I means code exists; X means experimental library; O means a bounded native observation exists; Q means the exact recorded native case passed; P means intended work remains. I and generated-file tests do not imply O/Q. Historical Q applies only to its recorded method/version, never automatically to this corrected delivery route.

Rules

The following are intended routes, not a change to schema-1 production output. Each evidence link qualifies only its recorded case. The user selected these always-on routes on 2026-10-24; CD-01 through CD-11 still gate implementation (AD-0040).

ClientIntended always-on deliveryReaders and scopePreservation and current status
Claude CodeNative .claude/rules/<name>.md, no frontmatterWith Copilot selected, both are declared per item and there is one physical file; other CLI/IDE/cloud readers remain unresolvedR; historical generation I. Default native case Q at 2.1.292; independent native positive PASS at 2.1.308 in Q4; complete adapter P (native control, Q4, Q1)
Copilot CLISame Claude file when Claude is selected; otherwise .github/instructions/<name>.instructions.md without applyToNever also write the Copilot modular copy for the shared rule. IDE/cloud/review readers need their own declarationR; Q1 at 1.0.103 reads unscoped Claude rules. Its positive uses the canonical Copilot file, not modular no-applyTo output; modular-route qualification P. Plugin rules component FAIL at both scopes (Q1, Q3)
Codex CLITrusted package SessionStart hook reads a generated static rule index; no runtime upskill invocationPreserve native project/user guard and trust. Other package/readers remain unintended unless declaredR + N; selected policy supported with declared limitation. O at 0.164.0: startup 1, resumed cumulative history 2, appended 1 per rule, no compaction; provider input and full adapter P (Q9, resume-only)
OpenCode v2 CLIExplicit instructions file paths, no glob; separately qualified context-hook fallbackAll configured imports/globs and compatibility readers inventoried before effectsR + N; legacy glob output I. Q5 positive FAIL at 2.1.4, downstream NOT RUN; explicit paths and current-pin fallback P. Q5 deferred, not evidence of absence (Q5 retries, issue #391)

One physical delivery per rule/project and per-item intended-reader ownership are required planner behavior, not a new implemented lockfile field. Existing schema-1 output does not yet implement that ownership record. Claude-only rules visible to Copilot require a compatible intended-reader declaration or are unsupported for that project; warnings do not supply consent. Claude IDE, Copilot cloud agent, code review and VS Code are unresolved readers, not intended consumers inferred from CLI selection (Q1, AD-0040).

Scoped rules are a secondary native capability policy: Claude paths:, and Copilot applyTo only where Claude is not selected. Matching/nonmatching native controls remain required; Q1 did not access a matching file. Shared scoped Claude+Copilot rules and other unenforced scopes are reported unsupported, never widened to always-on. Q1b matching-file access and Q3b Copilot startup-hook injection are deferred, unscheduled. Q4 is unscoped-only and does not prove fresh resume attachment or provider-effective uniqueness (Q1, Q3, Q4, handoff).

Skills and supporting resources

ClientIntended delivery, discovery and activationPotential other readers and sharing intentPreservationCurrent status
Claude CodeNative skill entrypoint and adjacent resources, standalone or plugin-packaged; qualified explicit/automatic invocation.claude/skills is a Copilot/OpenCode compatibility source; shared-catalog consumers are intentional, but a Claude adapter must not add a second visible copy; other imports/readers retain explicit intentF + N for packaging; resources never auto-executed on installI .claude/skills; O Q6 at 2.1.308, separate project/user scopes and fresh-session combined edit visibility; owned/coexistence P (Q6)
Copilot CLINative skill discovery/invocation with resources; package location chosen by scope/profile.github/skills can serve Copilot IDE/cloud surfaces; .agents/skills is intentionally shared for the evaluated catalog; .claude/skills and direct/plugin copies can duplicate one logical item; qualify one selected routeF; preserve other catalogs and manual skillsI .github/skills; user-root mapping, owned lifecycle and native matrix P
Codex CLIQualified native skill discovery from profile-declared roots or plugin catalog, with resources.agents/skills is intentionally shared for the evaluated catalog; plugin or extra-root exports must not expose the same logical item again; other readers retain explicit intentF + N if plugin; preserve resource paths/modes and independent manual configO Q8 at 0.164.0: namespaced cache skill/resource reads and remove/re-add refresh; integrated adapter P (Q8)
OpenCode v2 CLIQualified native skill source or plugin-provided catalog, resources loaded only when needed.agents/skills is intentionally shared for the evaluated catalog; .claude/skills, configured sources and plugins can add duplicates; other-reader intent remains explicitF + N if plugin; no claim that current shared location is privateI legacy .agents/skills; client-specific candidate .opencode/skills and exact v2/root behavior require qualification

Every resource inherits its parent installation’s consumer classification. Test relative links after relocation/aliasing, executable modes, ignored files, resource-name collisions and resources reachable through another client’s instruction glob. Native packaging must not cause direct and packaged copies of the same skill/resources to activate twice. A valid entrypoint cannot qualify missing or differently interpreted supporting files.

Agents

Require real delegated execution with model, tool, permission, instruction and MCP inheritance controls. A registered prompt or parent response is insufficient. Claude plugin agents ignore hooks, mcpServers and permissionMode; do not replace these with broader plugin/session settings. Reject unsupported mappings. Codex instructions do not establish a child sandbox override. OpenCode v2’s ordered permission/agent-merging contract needs a qualified adapter; existing legacy generated output is not proof. Copilot project .claude/agents is a known compatibility reader with no established selective directory exclusion. Keep client variants outside shared catalogs and preserve manual registrations. These are capability/regression requirements, not new agent management in M1.

ClientIntended delivery, discovery and activationPotential other readers and sharing intentPreservationCurrent status
Claude CodeNative .claude/agents or qualified plugin agent registration; explicit/automatic delegationOther clients’ compatibility loaders, imports and package agent catalogs remain an inventory obligation; no cross-client intent by defaultF + N if registered; preserve manual agents and tool restrictionsI Markdown generation; owned/native compatibility P, lifecycle expansion unsequenced
Copilot CLINative .github/agents/*.agent.md, qualified CLI agent/subagent behaviorCopilot IDE/cloud and any compatibility loader are separate consumers; none implied by CLI selectionF; preserve model/tool mappings and delegation restrictionsI generation; CLI/IDE behavior requires separate evidence
Codex CLINative agent configuration/registration for the pinned release, or explicit unsupported resultShared config roots and any plugin compatibility agent catalogs require inspection; unintended consumers blockedF + N; do not convert agent tool/permission restrictions into proseO Q7 at 0.164.0: explicit package TOML registration, real child and inherited read-only denial; plugin-only role absent; full adapter P (Q7)
OpenCode v2 CLINative agent files/config and qualified invocation/delegation for v2Current .opencode/agents files may coexist with config/plugin-defined agents; other-client imports and duplicate IDs require controlsF + N where configured; preserve existing agents and restrictionsI legacy generation; v2 semantic/native qualification P

Bundles

ClientIntended delivery, discovery and activationPotential other readers and sharing intentPreservationCurrent status
Claude CodeUpskill resolves bundle graph; each child activates through its declared Claude routeEvery child’s reader inventory applies; bundle YAML is not native runtime registrationB plus child F/R/N; no extra activation for bundle membershipI composition; owned mixed-state/native combination P
Copilot CLIUpskill graph and per-child Copilot delivery; no native bundle loader assumedChild compatibility/IDE exposure remains explicit, not made intentional by common bundle membershipB plus child contractsI composition; corrected-route evidence P
Codex CLIUpskill graph with Codex plugin/native child routesChild .agents/package/shared-path readers; do not reinterpret bundle as a Codex pluginB plus child contractsGeneric graph foundations I; Codex integrated delivery P
OpenCode v2 CLIUpskill graph with OpenCode plugin/native child routesShared skills/config imports and child package compatibility; explicit child-level sharing onlyB plus child contractsI legacy composition; v2 corrected-route evidence P

Plugins

ClientIntended delivery, discovery and activationPotential other readers and sharing intentPreservationCurrent status
Claude CodeNative package/marketplace installation, enablement and trust; qualify each exported skill/agent/hook/MCP separatelyOther clients may accept compatible manifests or scan exported content; cross-client package activation unintended without selectionN + B; preserve manual marketplace/plugin entriesI install/remove shellouts; thin upskill integration and exact native lifecycle P
Copilot CLINative plugin package/marketplace lifecycle with qualified exportsCompatibility packages, shared catalogs and IDE runtimes are separate potential readers; do not infer interoperabilityN + B; preserve existing plugins and trustI shellouts; thin integration qualification P
Codex CLIPinned native plugin installation/enablement plus independently qualified rule integrationPackage compatibility and exported .agents/other discovery paths can expose payload elsewhere; no shared activation by defaultN + B; no personal-config replacement or auth copyingP upskill integration; installed client plugin support alone does not implement it
OpenCode v2 CLIQualified native plugin loader/API and lifecycle; package registration distinct from rule injectionShared config imports, auto-loaded plugin directories and exported skill/agent catalogs can duplicate activationN + B; preserve plugin array order/entries and manual setupI legacy shellouts; v2 invocation, registration and upskill integration P

Plugin update transport, upskill CLI binary updates and installed-content reconciliation remain three separate lifecycles. Namespace or manifest names alone cannot prove activation isolation. A package that exports MCP/tools has the corresponding additional activation and permission checks.

MCP servers

ClientIntended delivery, discovery and activationPotential other readers and sharing intentPreservationCurrent status
Claude CodeExisting native CLI/config registration; distinguish configured, enabled, trusted, authenticated and connectedShared/imported config or plugin exports may register the same server in another runtime; explicit server-sharing intent requiredN + B; retain manual server keys, env/secret references and integration-removal policyI CLI/config fallback; owned native-effect recovery/qualification P
Copilot CLIExisting CLI/config registration and qualified connection/tool exposureCopilot IDE configuration is a separate surface; plugin exports or shared config can duplicate server/tool registrationN + B; preserve manual entries and native approvalsI CLI/config integration; expanded management P
Codex CLIPlanned native MCP adapter/config registration for pinned CLIConfig roots, plugin MCP exports and imported definitions can expose shared endpoint/tools; another client is never inferred from endpoint equalityN + B; no credential migration or grants inferred from tool proseP upskill adapter; no existing Codex MCP support implied
OpenCode v2 CLIExisting config route adapted and qualified for v2; verify actual connection and tool activationConfig imports and plugins can duplicate registrations; other-client config sharing must be explicitN + B; preserve native restrictions and unrelated JSON entriesI legacy config writes; v2/native qualification and broader management P

An intentionally shared remote endpoint does not imply shared credentials, consent, ownership or connection lifecycle. A denied/offline server must not be reported as connected; a config parser passing is not runtime evidence.

Planned hooks, tools and permissions

These rows are requirements inventory, not new implementation work. Internal rule-delivery/maintenance hooks needed by the MVP are qualified as plugin internals; general hook management, tool mapping and permission management retain the roadmap’s later scope. Existing restrictions remain regression obligations.

ClientHooks: intended activation and exposureTools: intended mapping and exposurePermissions: intended enforcement and exposurePreservation/status
Claude CodeNative events via qualified plugin; inspect compatibility manifests and nested hook exports; other-runtime execution unintendedExplicit native tool IDs and MCP exports; duplicate names/exports require controlsNative approval/sandbox/settings authority; other-runtime config import is not a grantN; preserve hooks, grants and deny rules; expanded management P
Copilot CLINative events via qualified plugin; CLI/IDE and imported hook definitions treated separatelyCLI-native identifiers and MCP/plugin exports; no CLI-to-IDE equivalence assumedCLI controls retained; no prose substitution or wideningN; preserve manual config/registration; expanded management P
Codex CLITrusted static-index internal rule hook; Q9 observes bounded continuity at 0.164.0, complete lifecycle/re-trust still P (Q9)Native tool identifiers and explicit capability refusal; package/shared-config exports inventoriedNative execution/approval policy and root authority; plugin cannot override sandbox restrictionsN; preserve user policy; general management P
OpenCode v2 CLINative plugin events/API; guard reload/duplicate registration and imported configNative tool/plugin/MCP registration; same-name tools and duplicate exports controlledV2 native permissions and deny rules; runtime-specific translation only when equivalentN; preserve ordered policy and unrelated config; expanded management P

Slice-1 evidence and adoption (2026-10-25)

This is the separate step-7 reconciliation for task #412. The user owns adoption at its PR review. Native-first always-on rules and skills-before-rules implementation order were already decided on 2026-10-24; the additional direction choices below are not implied by a PASS. Merged packets and indexed technotes complete a control administratively, including FAIL results, without completing Gate 6 or adopting a production adapter (handoff, AD-0040).

Retained control dispositions

All rows below are macOS arm64 observations. Pins are per row, not an implied current four-client support range. Retained qualified: false stays false; Linux, IDE/hosted, arbitrary user-root and integrated ownership/recovery evidence are not supplied by these packets (CD-09 and CD-10).

ControlPinRetained result and limit
Q1Copilot 1.0.103PASS: reads an unscoped .claude/rules file. The paths: variant returned fallback without matching-file access; no scope/exclusion conclusion. Other three pre-run client pin receipts are missing in this early packet (note, summary)
Q2Copilot 1.0.103PASS for Claude-format agent discovery, real delegation and resource read; tool catalog, permissionMode and equivalent restriction enforcement remain unqualified (note, summary)
Q3Copilot 1.0.103FAIL: plugin rule positives at both scopes. Bounded user-child delegation observed, exact response contract fails; project-overlay activation unverified. Rules component retired as a candidate at this pin; not a proof that all plugin components are absent (note, summary)
Q4Claude 2.1.308PASS: unscoped startup/post-compaction observations and absence controls. Resume callback/response do not prove a fresh attachment; scope and effective-input uniqueness remain unqualified (note, summary)
Q5OpenCode 2.1.4FAIL: native positives; component controls NOT RUN. Explicit-path retries retain backend-unavailable and provider-limit failures. User deferred further retries; no absence or route-failure inference (initial, latest retry, summary)
Q6Claude 2.1.308PASS: local marketplace, namespaced skill and combined content/version edit visibility in fresh sessions at separate project/user scopes. Earlier auth failures and settings/cache residue retained; overlap and running-session edits untested (note, summary)
Q7Codex 0.164.0PASS: explicit package-owned TOML registration, real child/resource read, inherited read-only native denial and calibrated plugin-only catalog absence. Initial restoration failure and final exact correction both retained; full agent adapter unqualified (note, summary)
Q8Codex 0.164.0PASS: namespaced skill/resource activation and native remove/re-add refresh. Resource+version edits stay cached before refresh; skill-body-only updates and unrelated-project isolation untested (note, summary)
Q9Codex 0.164.0Original zero-duplicate result FAIL: counts 1/2/1 across startup and combined reopen/compaction windows. Resume-only attribution PASS: startup 1, resumed cumulative history 2, appended records 1 per rule, zero compaction. Earlier HTTP 401 positive failure remains retained. Provider input and duplicate lifetime are unqualified (combined, failed retry, attribution, summary)

The Codex selected support policy is supported with declared limitation. The resume-only result attributes the extra recorded copy before compaction; it does not establish whether the provider receives both copies or when that duplicate ceases to affect input. The older combined run’s second compaction records one body per rule in that separate window. Do not merge those observations into a new provider-lifetime claim, rewrite the historical FAIL, remove native history or implement a transcript-dedup workaround (Q9, resume-only).

Update visibility and preservation review

These observations bound the implementation contract; they do not grant permission to leave unrelated configuration changed. Exact restoration of measured files and declared native effects are separate from whole-profile equality. Native cache directories are never directly edited or reclaimed by upskill (Q6, Q8, CD-07).

Client and pinUpdate visibilityMeasured restoration and native residue
Claude 2.1.308Fresh sessions see combined content/version edits without reinstall; running-session/content-only behavior untestedNative removal leaves empty enabledPlugins/extraKnownMarketplaces, then measured settings restored exactly. Native cache remains, including later-observed 0.0.1; registry files match. Session/log/cache state is outside whole-profile equality (Q6)
Copilot 1.0.103Managed-source catalog/load paths observed; do not infer a complete passing content-update loop from failed rule positivesEmpty owned installedPlugins residue after native uninstall; measured configuration restored. Session/log/cache/trust effects excluded (Q3)
Codex 0.164.0Q8 resource+version source edit stays invisible until native remove/re-add; other refresh verbs not qualifiedQ8 restores its own config baseline; Q7 separately corrects a notice counter to restore its earlier pre-UI hash. Both initial failure and correction retained. Native logs/cache bookkeeping excluded (Q7, Q8)
OpenCode 2.1.4Unobserved after failed positives; Q5 deferredMeasured config/manual/rule bytes and modes unchanged in retry. No successful install/update/remove lifecycle or whole-profile claim (Q5 retry)

Decision required: the user approves each client’s owned-key/byte preservation allowances before implementation. Recovery must use the journal plus observed native registration state; no successful file operation proves deactivation (CD-07, handoff).

Amendment inventory

This inventory reconciles every record named by the proposal. Shipped output is kept distinct from intended route changes; no path table below is a declaration that code has changed (proposal inventory).

RecordDisposition in this adoption review
AD-0040 and this topicRecord the decided native-first rules, shared readers, Codex limitation and implementation order. Package/MVP alternatives remain explicit pending decisions
RoadmapSkills-before-rules implementation order recorded; updater remains after both lifecycles. Four-client release scope and qualification remain. Bundles/subagent release changes are not adopted
ADR-0003Keep shipped paths/ancillary behavior, add intended native rule/package ownership transition and step-8 gate. Managed package root and declared-reader persistence still need implementation design
Specification section 3.4Keep current bridge/glob writes documented. Removal and future doctor reporting wait for step 8; existing files are never automatically deleted
AD-0016, AD-0017, AD-0018Explicitly bound aggregate contributions to the experimental library; planner/owner algorithms remain reusable
AD-0039Existing experimental-only clarification already applies; no algorithm amendment or new production destination
Topic 0001 section 7 and format-spec section 7Update the canonical concern reference’s current-versus-intended clarification; the compatibility page already forwards there. Preserve shipped output tables until corresponding code changes
ADR-0012 and VS Code ancillary outputPreserve current selection/defaults and write. CLI reader consent does not qualify VS Code. Its profile/write disposition is deferred, not silently removed
2026-10-14 route mapping and adapter evidenceReconcile the mechanism labels and add a dated native-evidence supplement; preserve historical research and later-client scope
ADR-0014No change: accepted client-specific offline mappings remain; no new runtime resolver or committed test model identifier
Storage candidates registrationRetained local comparison context only. User-reported OpenCode cache performance is unqualified; no source bytes copied from the original dirty worktree
#379Resume-only attribution merged in #411. Re-scope to K1 debt on user adoption; do not close it as a zero-duplicate PASS or silently change current labels before review
Proposal/implementation WIP and technote backlinksKeep active while the following direction choices and later implementation remain unresolved. Backlinks still name a live proposal; archive/repoint only when that proposal is fully adopted, preserving packet bytes

Decisions and entry controls still required

AD-0041 settles the organizing baseline and removal selection policy only. The user separately accepts or rejects each remaining direction change below; until then, its existing scope remains in force (AD-0040 proposed amendments, roadmap):

  • Decision required: make self-contained per-client skill/subagent packages the default instead of the shared-root-first experiment. Q6/Q8 establish bounded feasibility, not a default selection or integrated ownership design.
  • Decision required: add new owned-subagent management to the MVP. Q2/Q7 do not adopt release scope.
  • Move authorable bundles out of the MVP, preserving existing bundle composition.
  • Decision required: select explicit OpenCode package skill sources instead of the native-directory candidate. Q5 has not run that control, so this is a design decision only.

Decision required: select the Codex hook-bearing package distribution channel. The local marketplace fixtures establish no public-directory distribution or automatic trust. Ordinary native hook review is mandatory (design ledger, Q9).

The Codex index-only trust control remains unscheduled: start with a trusted hook/index positive, change only the static index while retaining exact hook bytes, compare native trust state, complete any ordinary user re-review, then verify activation. Capture version, exact changed bytes and native trust/outcome receipts. Failed positive blocks the result; no trust bypass or credential copy. Its result determines update UX, not a presumed exemption from hook review (handoff, Q9).

Q10 #409 must pass same-package user/project overlap with one catalog entry and one observable activation; Q11 #410 must pass calibrated unintended-reader controls for each applicable package profile. Direct-plus-package coexistence remains CD-04 work even without another numbered control. Preflight must inventory .claude/rules, .github/instructions, compatibility roots and imports/globs; onboarding, preservation and update visibility need per-client controls before owned-skill implementation (handoff, CD-03 through CD-08).

Q1b and Q3b remain deferred and unscheduled. Step 8 stays blocked while Q5 lacks explicit-path PASS: no bridge/glob removal, automatic deletion or production route activation follows from this documentation PR. Generic package-hook loading on all four clients remains rejected context; Copilot hook injection is untested. No native-versus-hook performance comparison has run. Criterion 7 still requires identical bytes/tasks and cached/uncached token counts, one client at a time (user decision, Q5 retry).

Acceptance and evidence gates

IDRequired executable example and evidence
CD-01For each applicable matrix cell, record delivery, discovery, activation, complete known reader conditions, intentional consumers, F/R/N/B preservation, status and exact evidence/version/OS/scope. Missing/unresolved entries block that claim.
CD-02With manual team AGENTS.md present, deliver two independent Codex/OpenCode rules through the selected native or hook route; update/remove one and observe the other. Preserve team bytes/modes and scope/trust. Test fresh/resumed sessions and compaction where applicable, then disable/remove the selected registration or file as a negative. Native files are not forced into a plugin. Q9 continuity observations do not qualify fresh registration or complete lifecycle; CD-04 records the Codex limitation (AD-0040, Q9, resume-only).
CD-03For each intended client run an unpredictable positive control; for every unintended reader run a successful native session without that activation marker. Exercise compatibility dirs, imports, broad globs, nested roots and configured overrides separately. Each negative requires a successful same-client positive; skills/agents need native catalog and explicit-invocation controls. Failed positives/auth/timeouts are blocked, never negative passes.
CD-04Install standalone plus package, duplicate aliases, same-name project/user content and two bundles. Observe one logical exposure/activation and correct surviving references; retain native catalog/registration/event counts. Planner duplicate delivery remains rejected. The selected Codex 0.164.0 rule policy allows the declared recorded-history duplicate after resume; provider-effective input and duplicate lifetime are unqualified. No other client, content kind or lifecycle inherits that exception (resume-only, Q10).
CD-05Resource link/mode and agent tool/model/delegation restrictions survive install/update/remove; an excluded client cannot activate them through a compatible catalog, imported resource or alternate agent registration.
CD-06Mixed rule/skill/resource/agent/bundle/plugin/MCP state remains lossless through schema-1 backup/migration and every current installed-state command, including remove-mcp. Manual edits, unselected owners, unknown legacy rows and unresolved native effects reject safely or remain inspectable.
CD-07Whole selected-set preflight, lock contention, source/destination drift and process interruption preserve recoverability. Native effects have observed pre/post state and safe replay/compensation evidence; file-only receipts cannot substitute.
CD-08Package registration, trust, enablement, authentication, discovery and actual use have separate outcomes; test disabled/denied/offline cases. Hook/tool/permission negative controls prove no unintended execution, exposure or grant without expanding the implementation scope.
CD-09All four CLI clients pass applicable pinned macOS arm64 and Linux x86_64 project/user cells. Existing binary platforms and supported IDE behavior retain regression obligations; CLI tests cannot qualify an IDE. No automatic single-client/project-only fallback.
CD-10Generated-file, offline harness, experimental-library, native-observed and native-qualified evidence stay separately labeled. Link sanitized raw/summary receipts and their hashes; reproducible commands alone do not substantiate historical passes. Publish unsupported cells and reload requirements.
CD-11Implement skills/resources before always-on rule work under the user’s explicit sequencing amendment. Require per-client retained PASS, step-7 user adoption and approved preservation contracts; start Claude with Q6, then Codex Q8, OpenCode only when Q5 skills/resources pass. Q10/Q11 are additional entry controls. This changes implementation order, not the all-four-client combined release gate or production activation. Keep one effective entrypoint per item/client, variants outside shared catalogs, integrated scope/permission/ownership/lifecycle evidence and existing-kind regressions. No unadopted route may activate (AD-0040, Q6, Q8, handoff).

Qualification starts with a complete matrix and upstream contract refresh for pinned releases. The existing canonical-file discovery harness qualifies only its exact fixtures; it does not test installed independent rule delivery, all customization kinds, unintended-reader isolation or duplicate activation. Existing observations retain their provenance but cannot pass CD-02 through CD-09 or CD-11 by implication. Production activation remains blocked until applicable G6 and CD examples have evidence and exact-head review/hosted checks pass.

Documentation sources and limits

The following official documentation was refreshed on 2026-10-19 to identify reader risks, not to declare installed-version conformance:

Compatibility paths not established by these sources remain explicit questions for the adapter inventory, never assertions that other clients cannot read them.

Edit the canonical source.

Previous reference entrypoints

These compatibility chapters retain the earlier reference navigation. Read the concern sequence for the consolidated contracts and architecture. ADRs retain their original decisions and status.

Qualified scope roots and writer locking

This record is consolidated into the concern reference. Use the links below.

Read the concern.

Lock qualified scope directories before owned lifecycle effects

AD-0028

The library boundary is implemented with local macOS evidence; hosted Linux and Windows unsupported-boundary evidence remain pending.

Context

Owned lifecycle writers need process exclusion on Rust 1.85 without preview writes, stale sentinel cleanup or independently replaceable lock-file inodes. Directory identity and scope-label identity are different: two aliases may select one physical scope. Current CLI lifecycles remain inactive consumers of this API.

Decision

Use opaque pinned directory identities and nonblocking exclusive directory flock on Linux and macOS. Add the already locked rustix 1.1.4 as a direct Linux/macOS dependency with filesystem support, retaining its safe synchronous bindings. Open a fresh close-on-exec description for every guard and revalidate the selected root before returning it. Unsupported platforms fail explicitly.

Alternatives

Standard library file locking stabilized in Rust 1.89. A create-new sentinel requires a crash and stale-owner protocol. A portable lock-file crate still requires a stable contained lock namespace. Both add machinery that directory locking avoids for these two qualified operating systems.

Consequences

Locking creates no state directory or lock file. Aliases and Project/User labels cannot split writer exclusion. Descriptor closure releases the lock after normal exit or termination. Directory replacement invalidates the root binding; the lock does not stop external namespace edits. Destination containment, complete precondition validation, executor integration and Windows qualification remain separate activation gates. No network filesystem or fork-safe lease is promised.

The implemented backend uses the exact direct dependency rustix = 1.1.4 with filesystem support, gated to Linux/macOS. The existing lock resolution is unchanged apart from the direct edge. Local Rust 1.85 all-target compilation and warning-denying clippy passed. Same-toolchain release CLI size stayed at 3,215,120 bytes; matched control/live probes measured 302,848/303,168 bytes, a 320-byte file delta that includes root/lock/error machinery and linker padding. The durable design records compiler, target, process controls and filesystem limits.

Complete admission evidence must include Rust 1.85 compilation, real-process contention, normal and forced release, close-on-exec, local macOS and hosted Linux tests, and matched release CLI plus live-API probe size measurements. Record evidence in the durable scope-lock design before marking this decision implemented.

Related: qualified scope-lock design.

Qualified destination observation

This record is consolidated into the concern reference. Use the links below.

Read the concern.

AD-0029: Capture qualified destination evidence

Implemented with local APFS and hosted Linux evidence, plus hosted Windows unsupported-boundary checks. Windows qualification remains pending.

Context

AD-0028 pins one scope and coordinates cooperating writers. The pure mixed planner still trusts supplied observations and exact lexical locations. Before execution can use them, filesystem observation must bind reads to directory identities and reject aliases and unsafe paths without making preview write.

Filename comparison differs across local filesystems and directory settings. Two absent spellings cannot be proved distinct by inspecting only their absence. Reproducing every versioned Unicode comparison table is outside the binary and dependency budget.

Options

Ambient path canonicalization followed by ordinary reads does not bind later accesses to the inspected directory chain. Creating temporary comparison probes would violate preview’s no-write contract. Exact emulation of every filesystem’s Unicode comparison would require version-dependent tables without qualifying unknown filesystems. Descriptor-relative traversal with retained identities and conservative overlap rejection preserves the selected boundary within the existing dependency budget.

Decision

Implement the inactive snapshot boundary in the qualified destination design. Use descriptor-relative no-follow traversal, retained handles and exact revalidation. Accept APFS and Linux ext-family/tmpfs profiles only when mount evidence is available; reject cross-mount traversal and other profiles. Reuse rustix 1.1.4 with no new dependencies or async runtime.

Conservatively reject candidate path pairs whose components may alias: ASCII case variants and any differing non-ASCII component are treated as potentially matching. Also reject potential ancestor/descendant pairs, stored-spelling aliases and hard-linked final files. This deliberately rejects some otherwise valid batches. The API describes rejection, not exact filesystem equivalence.

Return exact optional file values with an opaque, non-Clone snapshot borrowing the qualified scope. Verification detects relevant identity/content/binding changes and never refreshes stale evidence. The caller remains responsible for complete lifecycle inputs, writer-lock lifetime and future effect checks.

Consequences

Preview creates no probe files, directories or persistent state. Recoverable filesystem, read and descriptor-exhaustion failures return contextual errors, including allocation errors reported by a fallible read. This is not a guarantee of recoverable memory exhaustion: ordinary Rust infallible allocations, including captured-byte cloning, may abort, and the operating system may terminate the process instead of returning Err. Linux requires statx mount-ID support; unknown filesystems and Windows are explicit pending qualification boundaries. Retained handles and sampled checks do not exclude external editors or provide a security sandbox against hostile namespace relocation.

Source stabilization, permission policy, schema-2 persistence, apply/recovery and CLI activation remain separate gates. Completion requires acceptance RED, independent semantic controls, real APFS and hosted Linux evidence, unsupported Windows checks, Rust 1.85, full repository verification and matched live-API size measurements. Local APFS acceptance, private evidence tests and independent semantic controls now cover the implemented boundary. Linux profile, absent mount-ID mask and same-device/different-mount-ID validators are tested separately with synthetic metadata. On 2026-10-13, Actions run 38330080515 passed Linux runtime qualification with returned statx mount IDs and Windows compilation/unsupported-boundary checks for PR head 765e70b9b7ce5386530ffea1fe9814b228724428 (merge checkout 242930ef9107aff6eff432484f474e47157983f2). This is historical evidence for that head; later local timestamp and special-bit assertion controls are separate. A qualified Windows backend remains pending. Privileged mounts, device nodes and hostile namespace relocation remain unexercised. The issue #270 regression implementation now independently pins fractional mtime, acquisition-read stamp/size and call wiring, native profile rejection, retained mount comparison, existing-file overlap and repeated stale verification. Local APFS passes 28 public and 22 private destination cases; 14 independent production omissions produce assertion failures, with exact source restoration and focused green checks. Restored full repository verification passes without warnings. On 2026-10-13, Actions run 38334882688 passed for PR #273 head 2a50ca6a5163e11912cbcee7987593c68e0a4197. Ubuntu job 115092610337 passed 28 public destination cases and the native missing returned mount-ID mask case. Four profile/mask check and production-call omissions failed their intended assertions; each restored qualification.rs to SHA-256 1c5b288ed575ae1ccdd99a10170c13aa9054f92004a0927a64fb1587bac40bd5, followed by 22 restored private cases GREEN. All 13 head checks succeeded, including Windows unsupported-boundary job 115092610428. This dated evidence covers that head; hosted CI for the final head after temporary harness removal remains required before merge. It establishes neither a qualified Windows backend nor real privileged mount or fault behavior. See the durable design for evidence scope, counts and limits.

Local full verification, warning-denying all-target Clippy and Rust 1.85 all-target checks passed. Matching Rust 1.97.1 macOS release probes measure 33,888 additional bytes when snapshot capture and verification are live; the inactive CLI remains 3,215,120 bytes at both baseline 8298386 and candidate. This measures local binary size; it does not measure runtime cost.

Satisfies: specification §4.8. Related: AD-0028.

Qualified source tree observation

This record is consolidated into the concern reference. Use the links below.

Read the concern.

AD-0030: Capture qualified source evidence

Inactive library implementation complete; hosted Linux and Windows scope/compile checks passed at a393263 on 2026-10-13. Final-head hosted checks remain required.

Context

Strict source manifests supply exact freely mutable values to pure compilation. Their existing ambient-path capture establishes neither descriptor-relative containment nor later source revalidation. Qualified destination and scope-lock boundaries are implemented independently; lifecycle effects remain inactive.

Options

Ambient path rereads are simple but do not bind descendant access to inspected parents. Creating platform snapshots or probe files adds writes and dependencies. Select descriptor-relative full-tree capture with retained handles and complete membership revalidation, using the existing qualified scope and mount policy.

Decision

Implement the qualified source contract as a separate opaque non-Clone snapshot borrowing a QualifiedScope. Expose only an immutable existing SourceManifest and non-refreshing verify_unchanged. Preserve complete source inclusion and hard-link paths; reject links, special entries and mount changes. Record empty-directory topology privately. Share native qualification without changing destination policy or strict manifest digests. Keep the current CLI, schema 1, dependencies and legacy capture behavior unchanged.

Consequences

Retained descriptors and repeated exact checks cost memory, descriptors and I/O. Recoverable errors return contextual failures without a partial snapshot; ordinary allocation or OS termination is not a recoverable exhaustion guarantee. A sampled check cannot promise an atomic snapshot or exclusion against hostile writers. Source and destination roots may use separate qualified mounts. Permission application, Git revision/authenticity, lifecycle state, effects, recovery and Windows qualification remain separate gates.

Local implementation checks pass. CI run 38346202172 and CodeQL run 38346200154 succeeded at a393263 on 2026-10-13. That Windows run checked scope rejection and source API compilation, not the source method inside the previously uncalled helper. The new private unsupported-host test executes that method. Final-head hosted checks, including native execution of this assertion on Windows, remain merge gates. The design evidence records local coverage, independent-control limits and matched release-size measurements. Initial public binding failures preserve source-root/verification context and their original scope cause; retained nested paths are independently pinned. No as-built binding-context exception remains. No lifecycle or CLI activation follows.

Satisfies: specification §4.9. Related: AD-0028, AD-0029.

The binding-context fix measured at a3932633e9cc9a269f8ba8d8b6dd2ec26d0b828e refreshes the candidate CLI and live public-API probe under the same Rust 1.97.1 locked dependencies, release profile and runtime inputs. Exact hash-verified prior baseline/control binaries are reused: CLI baseline/candidate remain 3,215,120 bytes (delta zero); control is 303,120 bytes and live capture/revalidation is 353,840 bytes (delta 50,720). Both runtime-input fixtures pass. No byte-identity, performance or final-head hosted success claim follows from these size measurements.

The later test-seam and fixture closeout does not repeat these size builds or claim these measurements for a future head. Independent final revalidation binding omission coverage now complements the earlier capture binding controls.

Explicit file permission selection

This record is consolidated into the concern reference. Use the links below.

Read the concern.

Select explicit file permissions before file effects

AD-0031

This decision is implemented as an inactive pure library API.

Context

Captured resource modes and generated entrypoints have different provenance. AD-0026 intentionally left permission application to a future qualified writer. Pure operation plans preserve exact optional modes but do not grant authority to apply them.

Options

Copying source modes conflates evidence with authority. Reading umask makes a supplied-value policy depend on ambient process state. Default execution grants more authority than a resource-only explicit choice. A supplied-value interface with private defaults preserves ordinary existing modes and bounds new resource execution to an explicit choice backed by owner-execute evidence.

Decision

Expose files::permissions::select_file_mode with DestinationMode, PermissionSource and ResourceExecutionPolicy as a separate pure permission-selection interface. Preserve supported ordinary existing modes exactly; reject unknown existing modes and existing special bits. Select 0o0600 for newly generated files and resources. An explicit resource execution choice selects 0o0700 only from a known captured owner-execute bit. Never copy source special, group or other bits into a new file. Reject all out-of-range supplied modes, including captured evidence unused by existing-mode preservation. Unknown existing mode always rejects; unknown captured mode rejects when selecting execution for a new resource. Captured special bits alone never grant owner execution.

Public acceptance tests pin literal mode selections and contextual diagnostics. Independent compiled omission controls pin validation and selection branches.

The selection is deterministic and independent of process umask. It does not modify compiler output or existing planners and has no filesystem effects. Future application must persist actual resulting mode evidence and separately qualify owner/group, ACLs, extended metadata and replacement operations.

Consequences

Private output may need an explicit sharing policy later. Existing special modes fail conservatively until a qualified metadata policy exists.

Related: AD-0026, permission policy.

See specification 4.10. Qualified file effects and journal/recovery are implemented as opt-in library capabilities. Combined lifecycle state, activation and hosted qualification remain pending; M1-A2, M1-A3 and M1 remain incomplete.

Satisfies: specification §4.10. Related decisions: AD-0026.

File recovery checkpoint model

This record is consolidated into the concern reference. Use the links below.

Read the concern.

Model recovery checkpoints before file effects

AD-0032

This decision is implemented as an inactive pure library API.

Context

Owned lifecycle recovery must distinguish recorded completion from current observations, recognize an interrupted final state write and preserve manual edits. Neither supplied values nor the existing pure permission policy grant file-effect authority.

Decision

Expose operations::recovery::plan_recovery(&[RecoveryAction]) as a separate pure ordered checkpoint model before durable journals or file effects. Require one final State action and a completed prefix, followed by at most one Pending action and queued work. Validate all supplied observation relations before returning a next step. A pending post-state is acknowledged, queued post-state requires reconciliation, and an unchanged queued action performs no physical write. Report recorded and verified post-state prefixes separately.

Alternatives and cost

Embedding these rules into effectful code couples logical defects to file mutation. Inferring completion from desired values alone adopts unstarted changes and can repeat the final state write. The selected model adds an interface and requires future qualified producers and durable checkpoint protocols. It does not select journal serialization, credential storage, native metadata preservation or lifecycle activation.

Related: recovery model, permission policy.

Implemented boundary

RecoveryDecision returns Ready with one Apply, Acknowledge or CheckpointOnly step, Complete including the final State, or Reconcile at the first observation conflict. Structural errors return contextual errors before observation decisions. The recorded prefix counts leading Done checkpoints even when contradicted; the verified post-state prefix stops at a mismatch and may extend through one Pending post-state action. Queued Both never extends it. Complete observation validation prevents a later conflict from being hidden by an earlier next step.

Apply conveys no write authority. Producers must qualify physical identities, exact values, containment, source/state bindings and source availability. The opt-in executor in AD-0033 supplies desired-payload reconstruction, durable receipts, checkpoint persistence and physical effects within a restricted metadata envelope. No lifecycle caller or schema-2 writes are activated; hosted executor qualification, M1-A2, M1-A3 and M1 remain incomplete. Literal acceptance cases and independent compiled semantic controls pin both action roles and ordering boundaries.

Satisfies: specification §4.11.

Qualified file execution and recovery

This record is consolidated into the concern reference. Use the links below.

Read the concern.

Qualified file execution with durable forward recovery

AD-0033

Context

The pure operation and recovery planners do not authorize filesystem effects. Owned lifecycle work needs a qualified executor that preserves manual edits, recognizes an interrupted final state write, and retains evidence when recovery cannot safely proceed. Lifecycle state and native adapter activation remain separate gates.

Options

The approved design selects forward recovery from exact before/after evidence. A multi-file atomicity promise is unavailable with independent file replacements. Retaining old user files for rollback would introduce extra sensitive copies; the ordinary executor retains desired outputs and final state only. The separate opt-in backup prerequisite is recorded in AD-0037. Other alternatives were not documented.

Decision

Expose opt-in QualifiedScope::apply_operation and recover_operation methods, requiring the matching live scope writer guard. Accept a complete ordered file plan and one distinct, present final State action. The caller supplies ownership authority, final state bytes and all relevant qualified source snapshots; the executor does not select installations or define lifecycle lockfile schema 2.

Verify supplied sources and all destination preconditions before publishing intent. Store a strict execution journal and bound desired payloads in the private <scope-root>/.upskill/state/ directory. Persist each generation by syncing the staged file, atomically renaming it, and syncing its directory. An effectful action follows Pending publication, destination recheck, atomic replacement or individual unlink, destination-directory sync, post-state observation, and Done publication. Final State uses the same protocol.

Use the existing pure recovery decisions over freshly qualified observations. A Pending post-state is synced again, including its parent directory, before acknowledgement; a Queued post-state requires reconciliation. Unexpected bytes or ordinary modes remain untouched. Report recorded and verified prefixes separately. Persist a completed-cleanup marker before deleting validated payloads, then remove the completed receipt last. Preserve unknown files and retain conflicting receipts for reconciliation.

Consequences and limits

The backend uses descriptor-relative operations on qualified Linux/macOS local filesystems and returns explicit unsupported errors elsewhere. It adds no dependencies. Existing ordinary modes are preserved; new files use selected private modes. ACLs, special bits and unsupported extended metadata reject. Darwin permits only com.apple.provenance, and replacement requires the same value on the prior and staged files. Replacement also verifies unchanged UID and GID; it does not attempt ownership or ACL repair.

Advisory locks coordinate cooperating upskill writers. Pinned handles and rechecks reject observed namespace changes, but cannot exclude every race with external editors. There is no multi-file atomicity guarantee. Sync ordering and real process termination tests do not establish hardware power-loss durability. Gate-4 hosted delivery evidence is retained in the validation record. Combined lifecycle integration and M1 activation remain separate work. Current CLI commands and schema-1 lockfiles do not consume this API.

Satisfies: specification §4.12. Related: AD-0028, AD-0031, AD-0032, qualified execution design.

Require create-only backup journals

AD-0037

Context

Guarded lifecycle application needs to preserve exact observed original State bytes before changing any output. Ordinary file replacement must not overwrite an existing backup, including one that appears after the last observation. Recovery must retain the same publication restriction after process termination. This is a bounded executor prerequisite; lifecycle preparation and production command activation remain separate work.

Options

The accepted slice uses a separate opt-in executor method and persists the create-only prerequisite in a versioned execution journal. Alternatives were not documented.

Decision

Expose QualifiedScope::apply_operation_with_backup without changing ExecutionRequest. Require a matching writer guard and observed state.before bytes. Prepend those exact bytes as action zero, with mode 0o600, and validate the complete request, sources, destinations and metadata before creating intent or backup files. The caller supplies lifecycle authority; the executor does not parse or reserialize original State bytes.

Use execution-journal schema 2 for backup operations. Require exactly one create-only action at index zero, distinct from the final State, with an absent or identical before value. Bind its desired digest and length to the final State’s original evidence. Reject missing, moved or downgraded policy markers, invalid private modes and mismatched evidence before recovery effects. Ordinary execution-journal schema 1 remains supported and cannot carry a create-only action. This journal version is independent of lifecycle lockfile schemas.

Publish a new backup with qualified sibling staging and atomic renameat_with(NOREPLACE) using existing rustix support. Reject unsupported host or filesystem behavior. Preserve an existing matching backup’s identity; sync its file and parent directory before its Done receipt. Reuse the existing Pending/Done, payload, final State and cleanup protocol for the remaining operation.

Recovery verifies all actions, including the backup, before progressing. Sync and acknowledge an interrupted matching Pending publication without replacement. An edited completed backup requires reconciliation before later effects. Include the backup at index zero in report prefixes and retain it after successful journal cleanup. Remove only enumerated journal files, never directories.

Consequences and limits

Older binaries reject execution-journal schema 2; recovery requires a supporting binary. There are no new dependencies or CLI commands. Qualified observation, advisory locking and external namespace-race limits continue to apply. Process termination checks do not establish hardware power-loss durability. This primitive does not infer ownership, fetch sources, prepare lifecycle transitions or activate lifecycle-schema-2 production writes. Gate 5 and native qualification remain separate work.

The implementation is in run.rs, journal.rs and backend.rs. Public acceptance examples are in execution_backup.rs; private publication, policy and sync checks are in backup_tests.rs.

Satisfies: specification §4.12. Related: AD-0033, qualified execution design.