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
| Role | Lives in | Commands |
|---|---|---|
| Consumer | A project that uses AI assistance | add, remove, update, list, doctor, search |
| Author | A source-registry repo | new, 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, first add, first scaffold.
- Commands — full reference for every verb.
- Recipes — CI usage, private repos, pinning.
- Specification — the contract.
- Portable format — the on-disk SSOT format.
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).
| Command | Role | Purpose |
|---|---|---|
add | Consumer | Install content from any source. |
remove | Consumer | Remove installed content. |
remove-mcp | Consumer | Unregister an MCP server configured by a bundle. |
update | Consumer | Pull latest, regenerate changed items. |
list | Consumer | Show installed content from the lock file. |
doctor | Consumer | Verify installation consistency. |
search | Consumer | Look up skills via the public registry. |
index | Consumer | Build or manage the local registry index cache. |
new | Author | Scaffold a new rule, skill, or agent. |
lint | Author | Validate SSOT files against the format spec. |
fmt | Author | Canonicalise 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:
| Flag | Effect |
|---|---|
--no-color | Disable colored output. Honored alongside NO_COLOR, UPSKILL_NO_COLOR, TERM=dumb. |
-q, --quiet | Suppress informational stdout. Errors and exit codes are unchanged. |
-h, --help | Show help. |
-V, --version | Show 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 updatefixes. - Lockfile entries with no recoverable source (the local path went away, or
the named item was removed in the source) —
upskill removeto clear. - Missing plugins — recorded as
installedin the lockfile but no longer found in the client. Likely uninstalled out-of-band;upskill updatereinstalls them. Causes exit 1. - Skipped plugins (informational) — recorded as
skippedbecause the client CLI was not on PATH at install time. Install the CLI then runupskill updateto 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 updatereconfigures it). A missingclaudeCLI 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
| Flag | Description |
|---|---|
--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]
| Flag | Description |
|---|---|
--registry <name> | Rebuild only a specific registry. |
--clear | Remove 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 ID | Severity | Source |
|---|---|---|
frontmatter | error | format-spec §3 |
name-matches-dir | error | format-spec §2.1 |
body-h1 | warning | format-spec §5.1 |
fence-lang | warning | format-spec §5.2 |
body-format | warning | format-spec §3.8 |
directive | error | format-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:
| Scope | Location | Committed? | Purpose |
|---|---|---|---|
| Project | <cwd>/.upskill-lock.json | Yes | Deterministic regeneration in CI. |
| Global | $HOME/.upskill-lock.json | No | Cross-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 kind | Claude Code | GitHub Copilot | opencode |
|---|---|---|---|
| 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
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | General error |
| 2 | Usage error (bad args) |
| 130 | Interrupted (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>, ordocker 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:
-
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 theBashtool, and state in its body that it must get the user’s approval before running install commands. -
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-installeragent to install it, then retry. -
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/skillssource 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:anditems:lists are visually adjacent to their targets. - Matches
upskill new. Runningupskill new <kind> <name>from insideskills/producesskills/<name>/<KIND>.mdwith 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 usescontent/orskills-src/instead ofskills/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
| Kind | Purpose | Entrypoint file |
|---|---|---|
| Rule | Always-on behavioural guidance. | RULE.md |
| Skill | On-demand procedural content. | SKILL.md |
| Agent | Persona 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
| Scope | Lives at | Use |
|---|---|---|
| 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.mdfiles and*.bundle.yamlmanifests. 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.
| Command | Role | Purpose |
|---|---|---|
add | Consumer | Install items or bundles from any source. |
remove | Consumer | Remove installed content. |
update | Consumer | Pull latest, regenerate changed items. |
list | Consumer | Show installed content from the lock file. |
doctor | Consumer | Verify installation consistency. |
search | Consumer | Look up skills via the public registry. |
index | Consumer | Build or manage the local registry index cache. |
new | Author | Scaffold a new rule, skill, or agent. |
lint | Author | Validate SSOT files against the format spec. |
fmt | Author | Canonicalise YAML frontmatter (key order, quoting). |
2.1 add
upskill add <source> [items...] [--global|--project]
<source> accepts (parity with npx skills add):
owner/repo— GitHub shorthandowner/repo@ref— pinned ref (branch, tag, or commit SHA)owner/repo:path/to/item— subfolderowner/repo:path/to/name.bundle.yaml— bundle file (resolves transitively, see §2.6)owner/repo@ref:path— combinedhttps://github.com/owner/repo[...]— full HTTPS URLhttps://<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,
updateregenerates outputs from the same SSOT. - Track upstream evolution.
updategit-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--availableview (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 onlocal:sources (updatefixes), lockfile entries with no recoverable source (removefixes), installed plugins missing from the client (updatereinstalls), 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 ofrule,skill,agent).lint [paths...] [--strict]— validate SSOT files.--strictis 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.agentsis resolved against the same source repository and installed. Unresolved names error out before any write. - Transitive
requires:. When the bundle declaresrequires:, 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 (sourcereuses theupskill addDSL), and the closure MAY span sources. Each bundle and item is recorded in the lockfile under its own source. Circularrequires: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.jsonalongside its resolved items. The same item may appear in multiple bundleitems[]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 kind | Claude Code | GitHub Copilot | opencode |
|---|---|---|---|
| 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.
3.2 Copy, not symlink
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.mdat repo root — created once with@AGENTS.mdcontent if absent. Never overwritten..vscode/settings.json—chat.instructionsFilesLocationsis set; other keys preserved.opencode.json: checked on rule-bearing installs. The entry".agents/rules/**/RULE.md"is added toinstructions[]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:
| Scope | Location | Committed? |
|---|---|---|
| Project | <cwd>/.upskill-lock.json | Yes |
| Global | $HOME/.upskill-lock.json | No |
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
| Code | Meaning |
|---|---|
0 | Success. |
1 | General error (network, parse, I/O). |
2 | Usage error (invalid args or flags). |
130 | Interrupted (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_COLORand TTY detection (no color in pipes/CI). - Global
-q/--quietsuppresses informational stdout. Errors on stderr and exit codes are unaffected. - Single static binary, no runtime dependencies.
6.3 Environment variables
| Variable | Purpose |
|---|---|
NO_COLOR | Disable colored output. |
HTTPS_PROXY | HTTP proxy for network requests. |
XDG_CACHE_HOME | Override 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
- Format spec:
docs/specification/format-spec.md - User guide: Getting started, Commands, Recipes
- Agent Skills open standard: https://agentskills.io
- skills.sh ecosystem: https://skills.sh
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, orAGENT.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.
- A skill
- 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 omitsname:. - 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.mdfor a rule,SKILL.mdfor a skill,AGENT.mdfor 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 runupskill 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 recommendsskills/; 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 thenamefield 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 dedicatedbundles/directory. Implementations discover bundles by scanning for the.bundle.yamlsuffix and MUST NOT depend on a specific bundle-root path. A YAML file matching the suffix but lacking a top-level integerschema: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 underskills/; see Conventions. - A bundle MAY have an optional sibling Markdown file
<name>.bundle.mdcarrying 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 ofclaude,copilotoropencode. The four audience/directive family identifiers in §3.1 and §6.1 do not extend this override vocabulary.codexis 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:
- If
<KIND>.<C>.mdexists, use its body verbatim. Directives in the canonical body are NOT processed for clientC. - Otherwise, use the canonical body with
<!-- @client: -->directives (§6) processed for clientC.
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
ignorelist (§3.1) to scope which supporting files are copied. The list is.gitignore-style and subtractive only — there is noinclude/allowlist form. A file matching anyignorepattern is not copied into the client-expected location.ignoreis 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)
| Field | Type | Required | Description |
|---|---|---|---|
schema | integer | YES | Protocol version of this specification. Current: 1. |
name | string | varies | Stable identifier. Required for bundles; optional for items (effective name defaults to the directory name). See §2.1. |
description | string | YES | What this item does and when to use it. Max 1024 characters. Skill-specific guidance: see §3.3. |
license | string | no | SPDX identifier or custom (proprietary, LicenseRef-*). |
audience | string[] | no | Target 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. |
metadata | map | no | Free-form governance block. See §3.6. |
requires | map | no | Directed item dependencies (rules/skills/agents). See §3.7. Stripped from output. |
ignore | string[] | 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 withschemagreater 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-commandname in clients that support it and as the identifier in bundle manifests. Whennameis omitted on an item, the directory name is the effective name; a rule or agentnameMAY 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;audienceis the only mechanism that filters whether the item is generated at all. The three are complementary: useaudienceto 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 withinmetadata.requires: directed prerequisite items. Eachrequires.<kind>entry is either a bare name (same source, resolved by(kind, name)) or a{ name, source }map for a cross-source dependency, wheresourcereuses theupskill addsource DSL. See §3.7 for resolution, cycle, and conflict rules.ignore:.gitignore-style subtractive copy-scope (§2.4). Bothrequiresandignoreare SSOT-only authoring fields and are stripped from every client output (the same treatment asschema,metadata, andlicense).
3.2 Rule-specific fields
Rules MAY include path-scoping:
scope:
paths:
- "src/api/**/*.ts"
- "src/handlers/**/*.ts"
| Field | Type | Required | Description |
|---|---|---|---|
scope | map | no | Scoping constraints. |
scope.paths | string[] | no | Glob 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’sapplyTo: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
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
mode | string | no | subagent | primary, subagent, or all. Primary consumer is opencode; see notes below. |
model | string | no | sonnet | Short alias. Spec-guaranteed aliases listed below. |
tools | string[] | no | client-supported §4 | Capability-level tool names. See §4. Default semantics defined below. |
preload-skills | string[] | 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:
| Alias | Family |
|---|---|
sonnet | Anthropic Claude Sonnet (latest) |
opus | Anthropic Claude Opus (latest) |
haiku | Anthropic 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>.mdwithtools:as capitalized names,model:as literal,skills:frompreload-skills.modeis implicit by file location and not emitted. - GitHub Copilot:
.github/agents/<name>.agent.mdwithtools:filtered per §4 mapping;modeomitted;preload-skillsrendered as a prose## Skillssection in the body (no frontmatter field). - opencode:
.opencode/agents/<name>.mdwithmode:,permission:map (per §7.3), and passthrough fields;preload-skillsrendered as a prose## Skillssection 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):
| Key | Type | Convention |
|---|---|---|
version | string | Semver, quoted (e.g., "1.0.0"). MUST be quoted to avoid YAML float coercion. |
author | string | Team or individual identifier. |
updated | string | ISO 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
metadataduring processing. - MUST NOT require any specific key within
metadatabeyond what the implementation itself needs. - SHOULD validate
versionas 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).
| Field | Type | Required | Description |
|---|---|---|---|
schema | integer | YES | Same as §3.1. |
name | string | YES | Same as §3.1. Matches filename stem. |
description | string | YES | Same as §3.1. |
license | string | no | Same as §3.1. |
items | map | YES | What this bundle provides. |
items.rules | string[] | no | Rule names. |
items.skills | string[] | no | Skill names. |
items.agents | string[] | no | Agent names. |
requires | array | no | Bundle dependencies. See below. |
plugins | map | no | Client-native plugins. See below. |
mcps | map | no | MCP server descriptors. See below. |
metadata | map | no | Same 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 thenamefield. - MUST report unresolved item names as errors.
- MUST resolve
requirestransitively: installing a bundle implies installing every bundle in its transitiverequiresclosure, 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
requireschains 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:
| Client | Field | Type | Required | Description |
|---|---|---|---|---|
claude | source | string | YES | Marketplace source (passed to claude plugin marketplace add): owner/repo, URL, or path. |
claude | marketplace | string | YES | Marketplace name for the install ref <plugin>@<marketplace>. Distinct from source (see below). |
claude | plugin | string | YES | Plugin identifier (passed to claude plugin install). |
claude | install_url | string | no | URL shown in warn-skip message when CLI not found. |
copilot | source | string | YES | Marketplace source (passed to copilot plugin marketplace add). |
copilot | marketplace | string | YES | Marketplace name for the install ref <plugin>@<marketplace>. Distinct from source. |
copilot | plugin | string | YES | Plugin identifier (passed to copilot plugin install). |
copilot | install_url | string | no | URL shown in warn-skip message when CLI not found. |
vscode | extension | string | YES | Extension ID (passed to code --install-extension). |
vscode | install_url | string | no | URL shown in warn-skip message when CLI not found. |
opencode | module | string | YES | Module name (passed to opencode plugin). |
opencode | install_url | string | no | URL 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:
| Mode | Discriminating fields | Available for | Behavior |
|---|---|---|---|
| Auto-install | source+marketplace+plugin / extension / module | all clients | Shell out to client CLI |
| Instructions-only | instructions_url | all clients | Print info notice; no automated action |
| Config-write | plugin_uri | opencode only | Write 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.
| Field | Type | Required | Description |
|---|---|---|---|
instructions_url | string | YES | URL to manual install documentation. |
summary | string | no | Short 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
| Field | Type | Required | Description |
|---|---|---|---|
plugin_uri | string | YES | Plugin URI appended to opencode.json plugin[] array. |
install_url | string | no | URL 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:
| Client | CLI commands |
|---|---|
| claude | claude plugin marketplace add <source>, then claude plugin install <plugin>@<marketplace> --scope <scope> |
| copilot | copilot plugin marketplace add <source>, then copilot plugin install <plugin>@<marketplace> |
| vscode | code --install-extension <extension> |
| opencode | opencode plugin <module> |
Claude’s --scope derives from upskill’s project/global context:
| upskill scope | claude --scope |
|---|---|
| project | project |
| global | user |
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:
status | Meaning |
|---|---|
"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.jsonper 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:
| Transport | Field | Type | Required | Description |
|---|---|---|---|---|
remote | type | string | YES | Protocol: http or sse. |
remote | url | string | YES | Endpoint URL the client connects to. |
remote | headers | map | no | HTTP headers. Values pass through verbatim (use ${VAR}). |
local | command | string | YES | Launcher command (e.g. npx, uvx, docker, bare binary). |
local | args | string[] | no | Arguments passed to the command. |
local | env | map | no | Environment variables. Values pass through verbatim. |
| (either) | requires-env | string[] | no | Env 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).
| Target | Primary (CLI) | Fallback config file | Root key |
|---|---|---|---|
| Claude Code | claude mcp add <name> --scope <scope> [-e K=${V}] -- <cmd> [args] (local) / claude mcp add --transport <http|sse> <name> <url> --scope <scope> [-H …] (remote) | .mcp.json | mcpServers |
| GitHub Copilot | copilot mcp add … (no --scope) | ~/.copilot/mcp-config.json (user scope) | mcpServers |
| VS Code | code --add-mcp '<json>' | .vscode/mcp.json | servers |
| opencode | — (no mcp add verb; config-only) | opencode.json | mcp |
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 theirhttp/ssevalue intype, plusurlandheaders. The root key isservers, notmcpServers. - opencode —
typeis"local"or"remote". For local serverscommandis a single array combining the command and its args (["npx", "-y", "my-tool-mcp"]) and the env map is namedenvironment(notenv). Remote servers carryurlandheaders. - GitHub Copilot scope caveat — Copilot has no documented project-scope MCP
config file (
.github/mcp.jsonis 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 thecopilot mcp addCLI or warn-skipped.
Claude’s --scope derives from upskill’s project/global context:
| upskill scope | claude --scope |
|---|---|
| project | project |
| global | user |
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:
status | Meaning |
|---|---|
"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
requiresclosure: installing an item implies installing every item reachable throughrequires. - MUST resolve each
requires.<kind>entry by(kind, name). - MUST reject circular item
requireschains 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):
| Kind | Key order |
|---|---|
| Rule | schema, name, description, audience, license, scope, metadata, requires, ignore, claude, copilot, opencode, extras |
| Skill | schema, name, description, audience, license, tools, preload-skills, metadata, requires, ignore, claude, copilot, opencode, extras |
| Agent | schema, name, description, audience, license, mode, model, tools, preload-skills, metadata, requires, ignore, claude, copilot, opencode, extras |
| Bundle | schema, 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.
| Capability | Meaning | Claude Code | GitHub Copilot | opencode |
|---|---|---|---|---|
read | Read file contents | Read | — | read |
write | Create or overwrite files | Write | — | write |
edit | Modify existing files | Edit | — | edit |
bash | Execute shell commands | Bash | shell | bash |
grep | Search file contents | Grep | — | grep |
glob | Match file paths | Glob | — | glob |
web-fetch | Retrieve URL content | WebFetch | fetch | — |
web-search | Search the web | WebSearch | web_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’snamefield (verbatim, no transformation).descriptionis 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_issueto file a tracking issue” (MCP tool in portable form) - NO: “Use the
Readtool 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), whenXmatches 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,copilotoropencode). 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.codexhas no override-file form.
| Construct | Why prohibited (in always-rendered content) | Client |
|---|---|---|
$ARGUMENTS, $0, $1 | Substitution variable | Claude Code |
${workspaceFolder}, ${file} | Substitution variable | GitHub Copilot |
!`command` | Shell pre-execution | Claude Code |
@path/to/file | File import | Claude Code |
#tool:name | Tool reference | GitHub Copilot |
#file:path | File reference | GitHub Copilot |
ultrathink | Extended thinking trigger | Claude 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 kind | Output path | Frontmatter mapping |
|---|---|---|
| Rule | .claude/rules/<name>.md | paths: array from scope.paths (if non-empty). |
| Skill | .claude/skills/<name>/SKILL.md | name:, description: pass through. Agent Skills extended fields pass through. |
| Agent | .claude/agents/<name>.md | name:, description:, model:, tools: (capitalized). |
7.2 GitHub Copilot
| Item kind | Output path | Frontmatter mapping |
|---|---|---|
| Rule | .github/instructions/<name>.instructions.md | applyTo: (comma-joined from scope.paths, default "**"), name:, description:. copilot.excludeAgent if present. |
| Skill | .github/skills/<name>/SKILL.md | name:, description: pass through. Follows Agent Skills open standard. |
| Agent | .github/agents/<name>.agent.md | name:, 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 kind | Output path | Frontmatter mapping |
|---|---|---|
| Rule | .agents/rules/<name>/RULE.md | name:, description:. opencode-specific fields from passthrough block. scope.paths dropped (no per-rule scoping per §3.2). |
| Skill | .agents/skills/<name>/SKILL.md | name:, description:. Agent Skills extended fields and opencode-specific fields from passthrough block. opencode walks this directory. |
| Agent | .opencode/agents/<name>.md | name:, 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-cli2with 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
schemagreater 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
schemaequal 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:
- It correctly parses all frontmatter fields defined in §3, accepting unknown
fields within
metadataand client passthrough blocks without error. - It validates
nameagainst §2.1 constraints and rejects non-conforming names with a clear error. - It rejects files with unsupported
schemaversions per §8. - It correctly processes
@client:directives per §6.3, including balanced-block validation. - It resolves bundle
itemsreferences against available item sources and reports unresolved names as errors. - It preserves supporting files from item directories in generated output per §2.4.
- 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
metadatafields 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:
- Scope patterns: whether
scope.pathsshould support negation (!test/**), boolean combinations, or named scopes. - 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.
- Agent mode semantics: whether
primary/subagent/allis sufficient given divergent agent models across clients, or whether mode should be client-passthroughed entirely. - Bundle constraints: whether
requiresshould support semver ranges, platform predicates (requires-client: [claude >= 2.1]), or conditional items. - Per-client partial overrides: whether override files should support section-level overrides (replacing only a specific heading’s content) rather than full-body replacement.
- Skill activation control: whether
disable-model-invocationanduser-invocablefrom the Agent Skills standard should be surfaced as neutral fields or remain client passthroughs. - Multi-repo item sources (RESOLVED): the item
requiresfield with a{ name, source }locator (§3.7) defines the cross-source resolution contract —sourcereuses theupskill addsource 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. - 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 field | Claude Code output | GitHub Copilot output | opencode output |
|---|---|---|---|
name | name: | name: | name: |
description | description: | description: | description: |
scope.paths | paths: (array) | applyTo: (comma-joined string) | (not supported; always-on) |
mode | (implicit in file location) | (agent definition) | mode: |
model | model: (alias) | model: (full ID) | model: (provider/ID) |
tools (agent) | tools: capitalized names | tools: mapped only (§4); unmapped dropped | permission: map (allow per capability; write rolls into edit) |
preload-skills | skills: 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)
Appendix D: Recommended source-registry layout
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:
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.
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.
| Responsibility | Modules |
|---|---|
| Content model and parsing | src/model/, src/parse/ |
| Source classification and retrieval | src/source.rs, src/fetch.rs |
| Bundle composition | src/bundle.rs |
| Per-client rendering | src/generate/ |
| Installation and installed-state operations | src/pipeline/, src/lockfile.rs, src/ancillary.rs |
| Pure legacy-state interpretation | src/legacy/ |
| Native integrations | src/plugin.rs, src/mcp.rs |
| Author commands | src/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 family | Allowed explicit consumer identities |
|---|---|
| Claude | claude.cli |
| Copilot | copilot.cli, copilot.vscode-local |
| OpenCode | opencode.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.
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:
- ASCII
upskill:source-manifest:v1followed by one NUL byte. - File count as an unsigned 64-bit big-endian integer.
- 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 (
0absent or1present). 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.
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
- Contract model and agreement
- Combined-state validation and codec
- Upsert propagation
- Reference retirement
- Acceptance examples
- Evidence policy
- Validation limits
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.
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
- Retirement boundary: graph-driven state and per-owner retirement.
- File planning: location evidence and aggregate/file actions.
- Acceptance examples: public contract coverage.
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_conflictsreports one resolution failure and two independent occupied destinations, with no callback or effects.unchanged_selection_does_not_hide_changed_conflictretains 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_matrixandduplicate_binding_does_not_hide_disjoint_conflictspin complete participant inventories, mixed artifacts and independent conflict groups.compatible_shared_files_and_distinct_contributions_have_no_conflictspins legal identical file sharing and distinct instruction contributions.blocked_preflight_keeps_excluded_and_incidental_readerspins both compiled reader roles and unchanged retained ownership.instruction_mode_failure_does_not_hide_an_independent_source_failurepins 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.
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.
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
| ID | Required example |
|---|---|
| Q01 | Pin a real absolute root, verify binding, acquire/drop/reacquire without changing recursive directory contents or modes. |
| Q02 | Reject relative, empty, parent-traversing, missing, file and symlink roots, including trailing slash and dot disguises. |
| Q03 | Equivalent root spellings and different scope labels contend; independent roots can lock together. |
| Q04 | A second guard in the same process is busy while the first lives and succeeds after drop. |
| Q05 | A real child process holds the lock; parent is busy; graceful child exit and forced termination each permit reacquisition. Synchronize readiness and bound all waits. |
| Q06 | Replacement/rename/symlink substitution and ancestor-alias retargeting reject stale binding before a guard is returned. |
| Q07 | Native non-UTF-8 roots work where the filesystem accepts them; filesystem refusal remains contextual. Unsupported platforms return explicit errors without effects. |
| Q08 | A 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.
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:
- Publish and sync Pending.
- Reobserve and verify the selected destination and parent binding.
- Replace or unlink the file, sync its parent, and observe the desired state.
- 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.
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).
| Client | Intended always-on delivery | Readers and scope | Preservation and current status |
|---|---|---|---|
| Claude Code | Native .claude/rules/<name>.md, no frontmatter | With Copilot selected, both are declared per item and there is one physical file; other CLI/IDE/cloud readers remain unresolved | R; 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 CLI | Same Claude file when Claude is selected; otherwise .github/instructions/<name>.instructions.md without applyTo | Never also write the Copilot modular copy for the shared rule. IDE/cloud/review readers need their own declaration | R; 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 CLI | Trusted package SessionStart hook reads a generated static rule index; no runtime upskill invocation | Preserve native project/user guard and trust. Other package/readers remain unintended unless declared | R + 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 CLI | Explicit instructions file paths, no glob; separately qualified context-hook fallback | All configured imports/globs and compatibility readers inventoried before effects | R + 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
| Client | Intended delivery, discovery and activation | Potential other readers and sharing intent | Preservation | Current status |
|---|---|---|---|---|
| Claude Code | Native 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 intent | F + N for packaging; resources never auto-executed on install | I .claude/skills; O Q6 at 2.1.308, separate project/user scopes and fresh-session combined edit visibility; owned/coexistence P (Q6) |
| Copilot CLI | Native 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 route | F; preserve other catalogs and manual skills | I .github/skills; user-root mapping, owned lifecycle and native matrix P |
| Codex CLI | Qualified 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 intent | F + N if plugin; preserve resource paths/modes and independent manual config | O Q8 at 0.164.0: namespaced cache skill/resource reads and remove/re-add refresh; integrated adapter P (Q8) |
| OpenCode v2 CLI | Qualified 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 explicit | F + N if plugin; no claim that current shared location is private | I 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.
| Client | Intended delivery, discovery and activation | Potential other readers and sharing intent | Preservation | Current status |
|---|---|---|---|---|
| Claude Code | Native .claude/agents or qualified plugin agent registration; explicit/automatic delegation | Other clients’ compatibility loaders, imports and package agent catalogs remain an inventory obligation; no cross-client intent by default | F + N if registered; preserve manual agents and tool restrictions | I Markdown generation; owned/native compatibility P, lifecycle expansion unsequenced |
| Copilot CLI | Native .github/agents/*.agent.md, qualified CLI agent/subagent behavior | Copilot IDE/cloud and any compatibility loader are separate consumers; none implied by CLI selection | F; preserve model/tool mappings and delegation restrictions | I generation; CLI/IDE behavior requires separate evidence |
| Codex CLI | Native agent configuration/registration for the pinned release, or explicit unsupported result | Shared config roots and any plugin compatibility agent catalogs require inspection; unintended consumers blocked | F + N; do not convert agent tool/permission restrictions into prose | O 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 CLI | Native agent files/config and qualified invocation/delegation for v2 | Current .opencode/agents files may coexist with config/plugin-defined agents; other-client imports and duplicate IDs require controls | F + N where configured; preserve existing agents and restrictions | I legacy generation; v2 semantic/native qualification P |
Bundles
| Client | Intended delivery, discovery and activation | Potential other readers and sharing intent | Preservation | Current status |
|---|---|---|---|---|
| Claude Code | Upskill resolves bundle graph; each child activates through its declared Claude route | Every child’s reader inventory applies; bundle YAML is not native runtime registration | B plus child F/R/N; no extra activation for bundle membership | I composition; owned mixed-state/native combination P |
| Copilot CLI | Upskill graph and per-child Copilot delivery; no native bundle loader assumed | Child compatibility/IDE exposure remains explicit, not made intentional by common bundle membership | B plus child contracts | I composition; corrected-route evidence P |
| Codex CLI | Upskill graph with Codex plugin/native child routes | Child .agents/package/shared-path readers; do not reinterpret bundle as a Codex plugin | B plus child contracts | Generic graph foundations I; Codex integrated delivery P |
| OpenCode v2 CLI | Upskill graph with OpenCode plugin/native child routes | Shared skills/config imports and child package compatibility; explicit child-level sharing only | B plus child contracts | I legacy composition; v2 corrected-route evidence P |
Plugins
| Client | Intended delivery, discovery and activation | Potential other readers and sharing intent | Preservation | Current status |
|---|---|---|---|---|
| Claude Code | Native package/marketplace installation, enablement and trust; qualify each exported skill/agent/hook/MCP separately | Other clients may accept compatible manifests or scan exported content; cross-client package activation unintended without selection | N + B; preserve manual marketplace/plugin entries | I install/remove shellouts; thin upskill integration and exact native lifecycle P |
| Copilot CLI | Native plugin package/marketplace lifecycle with qualified exports | Compatibility packages, shared catalogs and IDE runtimes are separate potential readers; do not infer interoperability | N + B; preserve existing plugins and trust | I shellouts; thin integration qualification P |
| Codex CLI | Pinned native plugin installation/enablement plus independently qualified rule integration | Package compatibility and exported .agents/other discovery paths can expose payload elsewhere; no shared activation by default | N + B; no personal-config replacement or auth copying | P upskill integration; installed client plugin support alone does not implement it |
| OpenCode v2 CLI | Qualified native plugin loader/API and lifecycle; package registration distinct from rule injection | Shared config imports, auto-loaded plugin directories and exported skill/agent catalogs can duplicate activation | N + B; preserve plugin array order/entries and manual setup | I 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
| Client | Intended delivery, discovery and activation | Potential other readers and sharing intent | Preservation | Current status |
|---|---|---|---|---|
| Claude Code | Existing native CLI/config registration; distinguish configured, enabled, trusted, authenticated and connected | Shared/imported config or plugin exports may register the same server in another runtime; explicit server-sharing intent required | N + B; retain manual server keys, env/secret references and integration-removal policy | I CLI/config fallback; owned native-effect recovery/qualification P |
| Copilot CLI | Existing CLI/config registration and qualified connection/tool exposure | Copilot IDE configuration is a separate surface; plugin exports or shared config can duplicate server/tool registration | N + B; preserve manual entries and native approvals | I CLI/config integration; expanded management P |
| Codex CLI | Planned native MCP adapter/config registration for pinned CLI | Config roots, plugin MCP exports and imported definitions can expose shared endpoint/tools; another client is never inferred from endpoint equality | N + B; no credential migration or grants inferred from tool prose | P upskill adapter; no existing Codex MCP support implied |
| OpenCode v2 CLI | Existing config route adapted and qualified for v2; verify actual connection and tool activation | Config imports and plugins can duplicate registrations; other-client config sharing must be explicit | N + B; preserve native restrictions and unrelated JSON entries | I 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.
| Client | Hooks: intended activation and exposure | Tools: intended mapping and exposure | Permissions: intended enforcement and exposure | Preservation/status |
|---|---|---|---|---|
| Claude Code | Native events via qualified plugin; inspect compatibility manifests and nested hook exports; other-runtime execution unintended | Explicit native tool IDs and MCP exports; duplicate names/exports require controls | Native approval/sandbox/settings authority; other-runtime config import is not a grant | N; preserve hooks, grants and deny rules; expanded management P |
| Copilot CLI | Native events via qualified plugin; CLI/IDE and imported hook definitions treated separately | CLI-native identifiers and MCP/plugin exports; no CLI-to-IDE equivalence assumed | CLI controls retained; no prose substitution or widening | N; preserve manual config/registration; expanded management P |
| Codex CLI | Trusted 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 inventoried | Native execution/approval policy and root authority; plugin cannot override sandbox restrictions | N; preserve user policy; general management P |
| OpenCode v2 CLI | Native plugin events/API; guard reload/duplicate registration and imported config | Native tool/plugin/MCP registration; same-name tools and duplicate exports controlled | V2 native permissions and deny rules; runtime-specific translation only when equivalent | N; 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).
| Control | Pin | Retained result and limit |
|---|---|---|
| Q1 | Copilot 1.0.103 | PASS: 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) |
| Q2 | Copilot 1.0.103 | PASS for Claude-format agent discovery, real delegation and resource read; tool catalog, permissionMode and equivalent restriction enforcement remain unqualified (note, summary) |
| Q3 | Copilot 1.0.103 | FAIL: 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) |
| Q4 | Claude 2.1.308 | PASS: 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) |
| Q5 | OpenCode 2.1.4 | FAIL: 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) |
| Q6 | Claude 2.1.308 | PASS: 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) |
| Q7 | Codex 0.164.0 | PASS: 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) |
| Q8 | Codex 0.164.0 | PASS: 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) |
| Q9 | Codex 0.164.0 | Original 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 pin | Update visibility | Measured restoration and native residue |
|---|---|---|
| Claude 2.1.308 | Fresh sessions see combined content/version edits without reinstall; running-session/content-only behavior untested | Native 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.103 | Managed-source catalog/load paths observed; do not infer a complete passing content-update loop from failed rule positives | Empty owned installedPlugins residue after native uninstall; measured configuration restored. Session/log/cache/trust effects excluded (Q3) |
| Codex 0.164.0 | Q8 resource+version source edit stays invisible until native remove/re-add; other refresh verbs not qualified | Q8 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.4 | Unobserved after failed positives; Q5 deferred | Measured 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).
| Record | Disposition in this adoption review |
|---|---|
| AD-0040 and this topic | Record the decided native-first rules, shared readers, Codex limitation and implementation order. Package/MVP alternatives remain explicit pending decisions |
| Roadmap | Skills-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-0003 | Keep 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.4 | Keep 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-0018 | Explicitly bound aggregate contributions to the experimental library; planner/owner algorithms remain reusable |
| AD-0039 | Existing experimental-only clarification already applies; no algorithm amendment or new production destination |
| Topic 0001 section 7 and format-spec section 7 | Update 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 output | Preserve 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 evidence | Reconcile the mechanism labels and add a dated native-evidence supplement; preserve historical research and later-client scope |
| ADR-0014 | No change: accepted client-specific offline mappings remain; no new runtime resolver or committed test model identifier |
| Storage candidates registration | Retained local comparison context only. User-reported OpenCode cache performance is unqualified; no source bytes copied from the original dirty worktree |
| #379 | Resume-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 backlinks | Keep 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
| ID | Required executable example and evidence |
|---|---|
| CD-01 | For 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-02 | With 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-03 | For 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-04 | Install 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-05 | Resource 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-06 | Mixed 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-07 | Whole 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-08 | Package 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-09 | All 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-10 | Generated-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-11 | Implement 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:
- OpenCode v2 skills lists native and
Claude/shared compatibility sources.
V2 instructions describes
nested
AGENTS.mddiscovery and configured instruction loading. - Copilot skill sources and CLI instructions/imports expose compatibility and explicit-reference paths across client conventions.
- Claude skills and plugin manifest describe standalone/package discovery and exported customization types.
- OpenAI skill documentation and plugin documentation distinguish skill payloads from package installation. Exact Codex CLI discovery and rule injection capabilities still require the pinned CLI contract and native probe; generic plugin documentation is not proof of a supported rule event.
Compatibility paths not established by these sources remain explicit questions for the adapter inventory, never assertions that other clients cannot read them.
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.
- Scope locking
- Scope locking decision
- Destination observation
- Destination observation decision
- Source observation
- Source observation decision
- File permissions
- File permission decision
- Recovery checkpoints
- Recovery checkpoint decision
- File execution
- File execution decision
- Backup journal decision
Qualified scope roots and writer locking
This record is consolidated into the concern reference. Use the links below.
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.
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.
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.
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.
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.
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.