# CocoaSkills RFC 0003 — Runtime Roots for Skill Commands

Status: accepted
Date: 2026-05-19
Author: Ivan Oparin
Target: v0.5.0

## 1. Goals

- Allow skill-provided commands to depend on sibling runtime files, libraries,
  modules, and helper scripts.
- Keep runtime-only files out of the agent prompt context.
- Preserve the v0.4.x single-file command behavior for existing
  `csk-skill.json` schema v1 skills.
- Make system dependency validation stricter and explicitly non-executing.
- Keep project integration secure: `csk` checks system dependencies but never
  installs them.

## 2. Non-Goals

- Installing system dependencies such as `glab`, `sentry-cli`, Python,
  Node.js, or package-manager plugins. Project bootstrap tooling owns that.
- Running arbitrary `check`, `install`, `post_install`, or health-check
  commands from skill manifests.
- Transitive skill dependency resolution. Dependencies between skills are
  deferred to a future RFC.
- Public registry, trust providers, signing, or audit. Deferred to later
  milestones.
- Changing `Skillfile.json` schema. This RFC only changes the skill-side
  `csk-skill.json` schema.
- Removing legacy `agents/runtime.json` fallback in v0.5.0.

## 3. Motivation

v0.4.x supports command shims through `csk-skill.json`, but a script command is
installed as a single copied file:

```
<skill>/scripts/gmr
  -> ~/.cocoaskills/runtime/<skill>/<commit>/bin/gmr
  -> <project>/.agents/bin/gmr
```

This breaks real skills whose commands are entrypoints into a local runtime
tree:

- `skill-tracker/scripts/yt` and `ytx` call adjacent Python modules and
  helper scripts.
- `skill-review/scripts/gmr` calls `gmr_main.py` and shared helpers.
- `skill-monitor/scripts/monitor-api` and `sentry-cli-auth` call adjacent
  bootstrap/runtime files.

Copying only the entrypoint loses those side files. Copying all `scripts/` into
the project prompt context works mechanically but exposes operational code to
the agent context and violates the v0.1 design boundary: executable runtime
belongs in the runtime store, not in `.agents/skills`.

v0.5.0 introduces explicit runtime roots. A skill can declare which directories
are runtime-only. `csk` copies those directories into the global runtime store,
points project shims at the copied entrypoints, and excludes those directories
from installed skill context.

## 4. `csk-skill.json` Schema v2

`csk-skill.json` gains schema version `2`. Version `1` remains supported
unchanged.

Example:

```json
{
  "schema_version": 2,
  "runtime_roots": ["scripts"],
  "commands": {
    "mr": {
      "type": "script",
      "unix_path": "scripts/mr",
      "win_path": "scripts/gmr.cmd"
    },
    "review-cli": {
      "type": "system",
      "command": "review-cli",
      "hint": "Install the review CLI through project bootstrap tooling"
    }
  }
}
```

Top-level fields:

| Field | Required | Type | Notes |
|---|---:|---|---|
| `schema_version` | yes | integer | `1` or `2` in v0.5.0 |
| `runtime_roots` | no | list of strings | Runtime-only directories copied to runtime store |
| `commands` | no | object | Command declarations, same object shape as schema v1 plus v2 constraints |

Unknown top-level fields are rejected in schema v2. This keeps the manifest
surface small and prevents accidental reliance on ignored security-relevant
fields.

## 5. `runtime_roots` Field Rules

`runtime_roots` is optional. Default: `[]`.

Every entry must satisfy all rules:

- String value.
- Relative POSIX-style path inside the skill snapshot.
- No leading `/`.
- No `..` path component.
- No empty path component.
- Must exist in the snapshot.
- Must be a directory.
- Must be unique after normalization.
- Must be disjoint from every other root.
- Trailing slashes are stripped before comparison.
- Comparison is case-sensitive. Paths are POSIX-style relative paths, so
  `Scripts` and `scripts` are distinct roots.

Disjoint means neither root can contain the other. These are invalid:

```json
{"runtime_roots": ["scripts", "scripts/lib"]}
{"runtime_roots": ["tools/runtime", "tools/runtime/python"]}
```

Reason: overlapping runtime roots make copy, context exclusion, hash
interpretation, and future permissions rules ambiguous.

Validation error examples:

| Input | Error |
|---|---|
| `"/scripts"` | `runtime_roots[0] must be a relative path` |
| `"../scripts"` | `runtime_roots[0] must not contain '..'` |
| `"scripts/missing"` | `runtime root does not exist: scripts/missing` |
| `"scripts/tool"` where `tool` is a file | `runtime root must be a directory: scripts/tool` |
| `["scripts", "scripts/lib"]` | `runtime roots must be disjoint: scripts contains scripts/lib` |

## 6. Script Command Path Constraints

Schema v1 behavior remains unchanged:

- A `script` command copies one declared file into
  `~/.cocoaskills/runtime/<skill>/<commit>/bin/<command>`.
- This is the compatibility mode for existing skills.

Schema v2 behavior depends on `runtime_roots`.

If `runtime_roots` is empty:

- v0.4.x single-file copy behavior applies.
- This is valid, but new multi-file skills should prefer runtime roots.

If `runtime_roots` is non-empty:

- Every script command path must be inside one declared runtime root.
- `unix_path` must satisfy the rule when present.
- `win_path` must satisfy the rule when present.
- The command path must exist as a file in the snapshot.
- The command path must not point to a directory.

Invalid example:

```json
{
  "schema_version": 2,
  "runtime_roots": ["scripts"],
  "commands": {
    "tool": {
      "type": "script",
      "unix_path": "bin/tool"
    }
  }
}
```

Validation error:

```
command path "bin/tool" is not inside any runtime_roots
```

## 7. Command Field Validation

Schema v2 validates command objects strictly.

### `type: "script"`

Allowed fields:

- `type`
- `unix_path`
- `win_path`

Rules:

- At least one of `unix_path` or `win_path` must be present.
- Every declared path must be a non-empty string.
- Paths must be relative, must not start with `/`, and must not contain `..`.
- Unknown fields are rejected.

### `type: "system"`

Allowed fields:

- `type`
- `command`
- `hint`

Rules:

- `command` is required and must be a non-empty string.
- `hint` is optional and must be a string when present.
- Unknown fields are rejected.
- These field names are explicitly rejected when present:
  - `install`
  - `check`
  - `post_install`
  - `script`
  - `command_args`

`csk` validates system commands only with `shutil.which(command)`. It must not
execute shell snippets, version checks, package-manager commands, or arbitrary
manifest-provided checks.

If a system command is missing, `csk install` fails before installing that skill
into the project.

## 8. Runtime Store Layout

Schema v1 layout remains unchanged:

```
~/.cocoaskills/runtime/<skill>/<commit>/bin/<command>
```

Schema v2 with runtime roots uses root-preserving layout:

```
~/.cocoaskills/runtime/<skill>/<commit>/
  scripts/
    gmr
    gmr_main.py
    lib/
      gitlab.py
```

For this manifest:

```json
{
  "schema_version": 2,
  "runtime_roots": ["scripts"],
  "commands": {
    "mr": {"type": "script", "unix_path": "scripts/mr"}
  }
}
```

The project shim points at:

```
~/.cocoaskills/runtime/<skill>/<commit>/scripts/gmr
```

not at a copied `bin/gmr` file.

Runtime install should use an atomic staging directory:

1. Copy declared runtime roots into a temporary runtime directory for the skill
   commit.
2. Preserve file metadata using `shutil.copytree(..., copy_function=shutil.copy2)`.
3. Force executable bits on declared command entrypoints after copy.
4. Replace the previous runtime directory for the same skill commit only after
   all roots copy and command validation succeed.

This avoids partially installed runtime trees when copy or validation fails.

When the target runtime directory already exists for the same skill commit, the
copy step is skipped. Commit hash is content-frozen by git; re-copying the same
runtime roots would be redundant work.

## 9. Prompt Context and Whitelist Interaction

Runtime roots are not prompt context. They are executable runtime artifacts.

When a schema v2 skill declares `runtime_roots`, each declared root is excluded
from the installed skill context under:

```
<project>/.agents/skills/<skill>/
```

This exclusion happens even when older whitelist logic would otherwise copy
that directory. For example, if a skill declares:

```json
{"runtime_roots": ["scripts"]}
```

then `scripts/` is not copied into `.agents/skills/<skill>/`, even though
legacy compatibility rules may copy `scripts/` for skills without declared
commands.

The installed skill context may still include ordinary prompt-facing roots such
as:

- `SKILL.md`
- `agents/`
- `references/`
- `assets/`
- `templates/`
- `examples/`
- `.skill_triggers/`

Runtime roots live only under `~/.cocoaskills/runtime/<skill>/<commit>/` and
are exposed through project-local shims in `<project>/.agents/bin`.

## 10. Installation Transaction Semantics

System dependency checks and runtime root validation are pre-install gates.

For each project:

1. Resolve skill source, ref, commit, and snapshot.
2. Parse and validate `csk-skill.json`.
3. Validate runtime roots and command paths.
4. Validate system commands with `shutil.which`.
5. Only after all checks pass, install runtime roots and project context.

If any required system command is missing:

- The affected skill is not installed into `.agents/skills`.
- Its runtime roots are not installed into runtime store.
- Its command shims are not written.
- Existing installed version remains untouched until a successful install
  replaces it.
- The project result is a failure and process exit code follows existing
  partial-failure rules.

This preserves the security boundary: `csk` can say "the project environment
does not satisfy this skill", but it never mutates system tools to satisfy it.

After a blocked install, the project marker still points at the previous
successful commit. `csk status` will show the affected skill as
`update-available`. Re-running `csk install` produces the same failure until
the missing system dependency becomes available on `PATH`.

## 11. Marker Schema

`.csk-install.json` remains at `schema_version: 1`.

New optional marker fields:

```json
{
  "schema_version": 1,
  "name": "skill-review",
  "commit": "abc123...",
  "commands": ["gmr", "glab"],
  "runtime_roots": ["scripts"],
  "skill_schema_version": 2
}
```

`runtime_roots` is absent or `[]` for schema v1 skills and schema v2 skills
without runtime roots.

`skill_schema_version` records the parsed `csk-skill.json` schema version. It
helps `csk status` and manual debugging without bumping the install marker
schema.

## 12. Runtime GC

Runtime GC keeps its current ownership model:

```
~/.cocoaskills/runtime/<skill>/<commit>/
```

The contents of that commit directory may be either:

- `bin/<command>` for schema v1 single-file commands.
- Declared runtime root directories for schema v2.

GC remains commit-directory based. It does not need to understand individual
runtime roots. A runtime commit directory is retained when any configured or
current project marker references the same skill and commit; otherwise it is
eligible for removal.

## 13. Unsupported Schema Error

v0.5.0 supports `csk-skill.json` schema versions `1` and `2`.

When a newer schema is encountered, the error should be actionable:

```
Unsupported csk-skill.json schema_version 3; this skill requires a newer csk.
Upgrade with: pipx upgrade cocoaskills, brew upgrade cocoaskills, or mise upgrade pipx:cocoaskills.
```

For schema v2 skills consumed by v0.4.x, old clients will still show the
generic "requires a newer csk" message unless a v0.4.x compatibility patch is
released. A v0.4.x patch may adopt the same upgrade hint before v2 skill
migrations begin, but it is not required for v0.5.0.

## 14. Backwards Compatibility

- `csk-skill.json` schema v1 remains supported unchanged.
- `agents/runtime.json` fallback remains supported unchanged when
  `csk-skill.json` is absent.
- Skills with no declared commands remain context-only skills.
- Existing `Skillfile.json` files do not change.
- Existing `.csk-install.json` markers without `runtime_roots` remain valid.
- Existing runtime store directories using `bin/<command>` remain valid until
  GC removes them naturally.

## 15. Test Surface

Required tests:

- `test_csk_skill_schema_v2_parses_runtime_roots`
- `test_runtime_root_must_be_relative_directory`
- `test_runtime_roots_must_be_disjoint`
- `test_script_command_must_be_inside_runtime_root`
- `test_schema_v2_copies_runtime_root_to_runtime_store`
- `test_schema_v2_runtime_root_side_file_is_available_to_command`
- `test_schema_v2_excludes_runtime_roots_from_installed_context`
- `test_schema_v1_single_file_runtime_behavior_unchanged`
- `test_system_command_rejects_install_check_and_post_install_fields`
- `test_missing_system_command_blocks_skill_install_without_overwriting_existing_install`
- `test_runtime_root_preserves_executable_bits_on_peer_files`

Recommended tests:

- Windows `win_path` command inside runtime root creates `.cmd` shim.
- Dry-run validates runtime roots without writing runtime store or project
  files.
- Runtime GC keeps/removes schema v2 runtime directories correctly.
- Unknown top-level schema v2 fields fail validation.
- Unknown script command fields fail validation.
- Unsupported future schema reports upgrade hint.

## 16. README and Documentation Updates

- `README.md`: add a short `csk-skill.json` schema v2 example with
  `runtime_roots`.
- `docs/mvp-design.md`: add a v0.5 note that runtime roots supersede
  single-file runtime copy for multi-file command runtimes.
- `CHANGELOG.md`: document schema v2, runtime root copy, context exclusion,
  system command hardening, and backwards compatibility.
- CLI help does not need new public flags. This is manifest behavior, not a new
  command.

## 17. Skill Migration Sequence

After v0.5.0 is released:

1. Migrate `skill-tracker` as the pilot:
   - `schema_version: 2`
   - `runtime_roots: ["scripts"]`
   - commands: `yt`, `ytx`
2. Migrate `skill-review`:
   - `schema_version: 2`
   - `runtime_roots: ["scripts"]`
   - script command: `gmr`
   - system command: `glab`
3. Migrate `skill-monitor`:
   - `schema_version: 2`
   - `runtime_roots: ["scripts"]`
   - script commands: `sentry-api`, `sentry-cli-auth`
   - system command: `sentry-cli`
4. Tag each skill repository with a new release tag.
5. Update `demo-app` `Skillfile.json` to those tags.
6. Only after observation, remove legacy `agents/runtime.json` from the skill
   repositories. `dependencies.json` has since been removed from the active
   skill format; system dependencies belong in `csk-skill.json`.

Demo-app integration details such as `.mise.toml`, `make agents`, stamp
files, and `csk shell-init` are tracked outside this RFC. This RFC defines only
the `csk` behavior required to make those integrations clean.

## 18. Release Sequencing

1. Land RFC 0003 as `docs/v0.5-design.md`.
2. Implement schema v2 support and tests in `csk`.
3. Update README, CHANGELOG, and MVP design notes.
4. Tag and release `v0.5.0` through the normal PyPI/GitHub/Homebrew pipeline.
5. Run distribution smoke.
6. Migrate internal skills to schema v2.
7. Update `demo-app` to consume the new skill tags.

No deprecation cycle is needed. Schema v2 is additive, and schema v1 remains
supported.

## 19. Out of Scope for v0.5.0

Deferred to future RFCs:

- Skill-to-skill dependency graph and transitive install.
- Per-skill health checks.
- Post-install hooks.
- Declarative system package installation.
- Version constraints for system commands.
- Runtime sandboxing.
- Signature verification and trust policy.
- `csk audit`.

## 20. Acceptance Checklist

- [ ] `csk-skill.json` schema v2 agreed
- [ ] `runtime_roots` validation rules agreed
- [ ] Script command path constraints agreed
- [ ] System command strict field validation agreed
- [ ] Runtime store layout agreed
- [ ] Runtime roots excluded from installed prompt context
- [ ] Missing system dependency blocks skill installation
- [ ] Schema v1 compatibility preserved
- [ ] Legacy `agents/runtime.json` fallback preserved
- [ ] Required tests implemented and green
- [ ] README, CHANGELOG, and MVP design notes updated
- [ ] v0.5.0 released through normal pipeline
