Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 33 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,46 @@

## Unreleased

## 0.1.0 — not yet published (tag `v0.1.0`; see docs/publishing.md)

Kit writes: loadouts apply to a project or to your home with a plan, a confirmation and a
byte-for-byte undo.

- User scope: `--user` (the same as `--repo ~`) on plan/apply/use/off/status/eject writes
`~/.claude/settings.json`, `~/.claude/CLAUDE.md`, `~/.codex/config.toml`,
`~/.codex/hooks.json`, `~/.codex/AGENTS.md` and user skill folders, honouring
`CLAUDE_CONFIG_DIR` and `CODEX_HOME` within home. Intent and ledger live in `~/.kit/`.
Claude user MCP servers are a gap with the exact `claude mcp add --scope user` command
(owner decision: Kit does not write `~/.claude.json`); Cursor user rules are a gap.
Codex single-skill disables become owned `[[skills.config]]` tables (TOML arrays of
tables are now owned one table at a time, like JSON `/-` entries).
- Skill directories: a warehouse skill with a directory and a `SKILL.md` is copied into
`.claude/skills/`, `.agents/skills/` (and `.cursor/skills/`), one owned whole file per
asset; updates follow the warehouse and `off` removes them. Symlinks, non-UTF-8 assets
and files Kit did not write are conflicts. Fix: an owned whole file (a skill file or a
Cursor rule file) whose content changes is now updated instead of reported unmanaged.
- Setup steps: `kitctl use` asks to run the loadout's `setup` steps after writing
(`--yes` runs them), skipping a step whose `when` file is missing or whose
`${plugin:NAME}` is not installed (resolved from Claude Code's installed_plugins.json).
Outputs stay the tool's. A failed step makes `use` exit 1.
- `kitctl init [PATH]` creates a starter warehouse (default `KIT_WAREHOUSE` or `~/kits`):
`loadouts/starter.yaml` (an instruction block and a `tool:git` check), empty
`primitives/<kind>/` folders, a README, and `git init` unless `--no-git`. It refuses a
non-empty directory.
- Writes for everyone (owner decision, 2026-10-21): `use`, `off`, `apply` and `eject` write
wherever they are pointed after showing the plan and asking `[y/N]` on a terminal
(`--yes` skips it; with no terminal and no `--yes`, nothing is written). The undo is
`kitctl off`. A directory that is not a git repository, uncommitted tracked changes and a
default branch are warnings, not refusals. `$KITCTL_SANDBOX`, when set, still confines
every write. `--live` and `live: true` from the opt-in below are accepted and ignored.
- Live apply, owner-authorized: outside `$KITCTL_SANDBOX`, `use`, `off`, `apply`
and `eject` write only with `--live`, a committed `kit.yaml` that says
`live: true`, no uncommitted tracked changes and a named branch other than
main, master and `origin/HEAD`. Recovery of an interrupted apply may run on
uncommitted files only when its journal names every one. Each unmet condition
is named; git runs without inherited `GIT_*` variables. `kit.yaml` accepts the
`live` key and keeps CRLF line endings. A no-op apply writes nothing and needs
no permission. See docs/apply.md#live-apply.
no permission. Superseded by the entry above; see docs/apply.md#writing.
- Cursor: project skills go to `.cursor/skills/<name>/` and doctor sees them there; every Cursor hook carries a `trust-required` gap (trusted workspaces load project hooks); a loadout that writes only the `.mdc` rule still gets the unverified trust gap. Source: cursor/cookbook@6733ef8.

- Cursor is a third harness (`--harness cursor`, `harnesses: [cursor]` in kit.yaml).
Expand Down
6 changes: 3 additions & 3 deletions PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,13 @@ requirements; it is not an independent grant of permission to change live config
|---|---|---|
| 0 Preflight | Tool/auth checks; missing requirements recorded | Complete |
| 1 Names | Registry/API checks plus binary collision research | Complete; requested name retained |
| 2 Bootstrap/release | Repos, useful 0.0.1, sandbox tests, Linux/Windows CI; npm and PyPI uploads verified | Complete; npm, TestPyPI, and PyPI 0.0.1 verified; all four CI jobs green |
| 2 Bootstrap/release | Repos, useful 0.0.1, sandbox tests, Linux/Windows CI; npm and PyPI uploads verified | Complete; npm, TestPyPI, and PyPI 0.0.1 verified; all four CI jobs green. 0.1.0 (writes, init, skills, setup, user scope) prepared 2026-10-21; publication is the owner's (docs/publishing.md). |
| 3 Design/model | Review docs/design.md; schemas and validation; no generator until reviewed | M1 (2026-10-19): schema v2 model, `validate`, read-only `resolve` with capability gaps, brand-studio golden output for both harnesses. Model approved and merged 2026-10-19. |
| 4 Inventory | Explicitly authorized real roots, skill/config/MCP/plugin discovery, usage adapters, matrix, drift report | M2 (2026-10-19): inventory reads Claude/Codex plugins, marketplaces, MCP (with redacted args), hooks, installed plugin shas and managed blocks over explicit roots; `doctor` reports tools, MCP launchability, unmanaged slot conflicts, stale and ineffective disables, pin drift, pending trust and hook heartbeats. Usage adapters and the drift matrix are still pending. |
| 5 Triage | Proposed classifications/diffs; confirm choices; archive retirement; commit warehouse | Pending inventory review |
| 6 Core/apply | Sandbox policy tests, preview global diff, confirm apply, verify trust and effectiveness | M3 (2026-10-19): ledger with loadout/profile attribution, journaled project apply, `plan/apply/use/off/status/eject`, generators for brand-studio's targets; sandbox acceptance green. Live apply (2026-10-21): outside `$KITCTL_SANDBOX`, writes need `--live`, a committed `live: true` in kit.yaml, a clean tracked tree and a non-default branch. User-scope generators and setup execution pending. |
| 6 Core/apply | Sandbox policy tests, preview global diff, confirm apply, verify trust and effectiveness | M3 (2026-10-19): ledger with loadout/profile attribution, journaled project apply, `plan/apply/use/off/status/eject`, generators for brand-studio's targets; sandbox acceptance green. Writing (2026-10-21, owner decision): writes go anywhere after the plan and a confirmation (`--yes` skips it); git state warns and never refuses; `$KITCTL_SANDBOX`, when set, confines writes. Skill directories, setup steps (after a confirmation) and user scope (`--user`, 2026-10-21) are generated; Claude user MCP stays a remedy by owner decision. |
| 7 Capture | Guidance, write guards, incremental reconcile, loop prevention, measured <300ms warm path | Pending |
| 8 Pilot | Select low-risk repo; preview/apply, harness smoke tests, migration runbook | M6 (2026-10-19): owner named the pilot; real dry run only (20 planned additions, 15 gaps, nothing written); runbook in docs/pilot-brand-studio.md with two findings to resolve before any apply. Live apply authorized 2026-10-21 as a per-repo opt-in; not yet run on the pilot. |
| 8 Pilot | Select low-risk repo; preview/apply, harness smoke tests, migration runbook | M6 (2026-10-19): owner named the pilot; real dry run only (20 planned additions, 15 gaps, nothing written); runbook in docs/pilot-brand-studio.md with two findings to resolve before any apply. Writes authorized 2026-10-21; not yet run on the pilot. |
| Forge (new) | scout/vendor, then forge new/propose/verify and the kit-forge skill | M4 (2026-10-19): `scout` (quarantine, facts with file:line evidence, never executes) and `vendor` (pinned path into inbox/ with provenance, hash, notices, license policy). M5 (2026-10-19): `forge new/propose/verify` and the `kit-forge` skill (examples/); end-to-end forge over three fixture repos; brand-builder's suite runs through `forge verify`. Offering kit-forge and brand-studio to agent-kits awaits the owner. |
| 9 Removal | Purge restores baseline; eject preserves effective config with zero Kit dependencies | Sandbox proofs (M3): `off` restores the baseline byte for byte; eject leaves native files with no Kit markers, ledger or state. |

Expand Down
59 changes: 36 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,34 +1,37 @@
# Kit (`kitctl`)

Kit inventories the skills, MCP configuration, hooks, and plugins used by coding
agents (Claude Code, Codex and, documented but not yet verified, Cursor). The planned compiler manages named **loadouts** from a separate private
warehouse and writes each harness's native configuration with an ownership ledger.
The planned default warehouse location is `~/kits`; setup will ask which parent
directories contain your repositories.

**0.0.1 is an early, read-only release.** It discovers skill folders and known
configuration files in explicitly selected roots, computes SHA-256 hashes, and
prints text or JSON. It does not yet parse connectors, count usage, compile
loadouts, reconcile changes, or modify agent configuration.
Kit manages the skills, MCP servers, hooks, plugins and instruction blocks your coding
agents (Claude Code, Codex and, documented but not yet verified, Cursor) load. You keep
named **loadouts** in a warehouse (`~/kits` by default); kitctl writes each harness's own
config for them in a project or in your home, with an ownership ledger so `kitctl off`
puts back exactly what was there.

**0.1.0** can:
- start a warehouse (`kitctl init`), check it (`validate`) and show what a loadout means
per harness, with every capability gap and its remedy (`resolve`);
- check a loadout against what is actually installed (`doctor`, read-only);
- apply loadouts to a project or to your home (`--user`): `use`, `off`, `apply`,
`status`, `eject`, each showing its plan and asking before it writes;
- copy warehouse skill folders and run a loadout's setup steps (after asking).

Kit never writes trust: approving hooks, MCP servers and projects stays with you.

## Run

Python 3.11+:

```sh
# From a checkout (works before the PyPI release):
uv run kitctl --version
uv run kitctl inventory --root /path/to/repo --json
uv tool install kitctl # or: uvx kitctl --version
kitctl init && kitctl validate
kitctl use starter --repo path/to/repo

# After publication to PyPI:
uvx kitctl --version
uv tool install kitctl
# From a checkout:
uv run kitctl --version
```

Node 20+ with [uv installed](https://docs.astral.sh/uv/getting-started/installation/):

```sh
# After publication to npm:
npx kitctl --version
npx kitctl inventory --root /path/to/repo --json
```
Expand All @@ -37,6 +40,14 @@ The npm package is a thin launcher around the bundled Python wheel from the same
release. It uses `uvx` with that local wheel; it does not require a PyPI release,
run install scripts, or ship a `kit` binary. uv manages Python if needed.

Start a warehouse of your own (`~/kits`, or `KIT_WAREHOUSE`), with a `starter` loadout
that only adds an instruction block:

```sh
uv run kitctl init
uv run kitctl validate
```

Loadouts (read-only; nothing is written):

```sh
Expand All @@ -54,14 +65,16 @@ uv run kitctl doctor --loadout brand-studio --root ~ --root path/to/repo
```

`use`, `off`, `plan`, `apply`, `status` and `eject` manage a project's native
config through an ownership ledger. They write inside `$KITCTL_SANDBOX`, or in a
real repository only with `--live` and that repository's committed opt-in (see
[apply](docs/apply.md#live-apply)):
config through an ownership ledger. They show the plan and ask before writing
(`--yes` skips the question), and `kitctl off` restores the original bytes (see
[apply](docs/apply.md#writing)):

```sh
export KITCTL_SANDBOX=/tmp/kit-sandbox
uv run kitctl use brand-studio --repo /tmp/kit-sandbox/site --dry-run
uv run kitctl use brand-studio --repo path/to/repo --live --yes # after the opt-in
uv run kitctl use brand-studio --repo path/to/repo --dry-run # the plan only
uv run kitctl use brand-studio --repo path/to/repo # the plan, then [y/N]
uv run kitctl off brand-studio --repo path/to/repo # the undo
uv run kitctl use core --user # ~/.claude, ~/.codex, ~/.agents
KITCTL_SANDBOX=/tmp/kit-sandbox uv run kitctl ... # confine writes to one directory
```

`resolve` prints the effective native config a loadout would produce and every
Expand Down
105 changes: 66 additions & 39 deletions docs/apply.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
# Plan, apply and removal (M3)

Status 2026-10-21: proven in sandbox tests. Live apply is authorized by the
owner as an explicit opt-in per repository (see [Live apply](#live-apply)).
Every other write refuses unless the project is inside `$KITCTL_SANDBOX`.
Status 2026-10-21: writes go wherever you point them, after Kit shows the plan and
you confirm (see [Writing](#writing)). `$KITCTL_SANDBOX`, when set, confines every write
to one directory.

## Files in a project

| File | Role | Committed? |
|---|---|---|
| `kit.yaml` | Intent: `loadouts` (`name` or `name:profile`, one profile per loadout), `harnesses` (`claude`, `codex`, `cursor`; default `[claude, codex]`) and the optional `live: true` opt-in | yes |
| `kit.yaml` | Intent: `loadouts` (`name` or `name:profile`, one profile per loadout), `harnesses` (`claude`, `codex`, `cursor`; default `[claude, codex]`) (a `live: true` from the earlier opt-in is read and ignored) | yes |
| `kit.lock` | Ledger: every owned entry with its owners (loadout and profile), the parents and files Kit created, and the warehouse commit. A checksum detects hand edits. | yes |
| `.kit/` | Local recovery state: `lock`, `journal.json`, and `preimages/` (original bytes of files Kit edits). It contains its own `.gitignore` (`*`). | no |

Expand All @@ -28,47 +28,75 @@ kitctl status # loadouts, owned entries (ok/drift/missi
kitctl eject --yes # keep native files, drop kit.yaml, kit.lock, .kit/ and markers
```

## Live apply
## Writing

A write lands in a real repository only when every one of these holds, and
kitctl names each one that does not:
`use`, `off`, `apply` and `eject` print the plan, then ask `Write N files? [y/N]` on a
terminal. `--yes` skips the question (scripts, CI); without a terminal and without
`--yes`, nothing is written. `--dry-run` shows the plan and never asks.

1. `--live` is on the command line (`use`, `off`, `apply`, `eject`).
2. The **committed** `kit.yaml` says `live: true`. The opt-in is read from
`git show HEAD:./kit.yaml`, so editing the working copy is not enough: the
owner commits the decision.
3. No tracked file has uncommitted changes, so each apply is its own reviewable
commit and none of the owner's work is mixed into it. Untracked files do not
count. Files git does not track (`.mcp.json` when ignored, say) are restored
by `kitctl off` from `.kit/preimages`, not by git. The one exception is
recovering an interrupted apply, and only when every uncommitted file is
one its journal names.
4. HEAD is a named branch other than `main`, `master` and the remote's
default (`origin/HEAD`).
The undo is `kitctl off <loadout>`: it removes only that loadout's entries and restores
the original bytes from `.kit/preimages`, with or without git. Before writing, Kit warns
(and never refuses) when:

The opt-in records the owner's decision; it is not a lock. Anyone who can
commit to the repository, an agent with a shell included, can commit
`live: true`. Review that commit like any other.
- the directory is not a git repository (`off` is then the only undo);
- tracked files have uncommitted changes (Kit's edits will mix with them);
- HEAD is on `main`, `master` or the remote's default branch (a branch lets the change be
reviewed before it lands).

`plan`, `status` and `--dry-run` read only and never need the opt-in. A
sandbox is unchanged: inside `$KITCTL_SANDBOX` no opt-in is needed. Never point
`KITCTL_SANDBOX` at a real repository to get past these checks.
When `$KITCTL_SANDBOX` is set, every write must land inside it, so a test run or a first
try cannot touch anything else. `--live` and `live: true` from the earlier opt-in are accepted and do
nothing.

```sh
git switch -c kit/first-apply
kitctl use gestalt-flow --dry-run # review the diff
# add `live: true` to kit.yaml (create it with `schema_version: 1` if absent)
git add kit.yaml && git commit -m "kit: opt in to live apply"
kitctl use gestalt-flow --live --yes
git switch -c kit/first-apply # optional; Kit warns on main
kitctl use gestalt-flow --dry-run # the plan, nothing written
kitctl use gestalt-flow # the plan, then [y/N]
kitctl status # every owned entry should read ok
git add -A && git commit -m "kit: gestalt-flow" # the next live write needs a clean tree
kitctl off gestalt-flow --live --yes # the undo; restores the original bytes
kitctl off gestalt-flow # the undo; restores the original bytes
```

Live apply still writes project files only. User-scope (`~/`) targets, setup
steps and skill directories are not generated; they stay gaps with remedies.

Every write takes `--dry-run`. Without `--yes`, nothing is written.
## User scope

`--user` (the same as `--repo ~`) applies a loadout to your home instead of a project:
every session on the machine sees it, so Kit warns before writing. Intent and ledger live
in `~/.kit/kit.yaml` and `~/.kit/kit.lock`; `kitctl off <loadout> --user` restores the
original bytes like it does in a project.

| Project target | User target |
|---|---|
| `.claude/settings.json`, `.claude/skills/<n>/` | `~/.claude/…` (or `$CLAUDE_CONFIG_DIR/…`) |
| `CLAUDE.md` block | `~/.claude/CLAUDE.md` |
| `.codex/config.toml`, `.codex/hooks.json` | `~/.codex/…` (or `$CODEX_HOME/…`) |
| `AGENTS.md` block | `~/.codex/AGENTS.md` |
| `.agents/skills/<n>/`, `.cursor/mcp.json`, `.cursor/hooks.json` | `~/.agents/…`, `~/.cursor/…` |
| `.mcp.json` (Claude MCP) | a gap whose remedy is the `claude mcp add --scope user …` command: user servers live in `~/.claude.json`, which Claude Code rewrites all the time, so Kit does not own it |
| `.cursor/rules/kit-<loadout>.mdc`, `.cursor/skills/` | gaps: Cursor keeps user rules in its settings |

Codex reads single-skill disables only from user config, so in user scope a disabled skill
becomes one `[[skills.config]]` table (`name`, `enabled = false`) Kit owns by value; your
own tables stay. `$CLAUDE_CONFIG_DIR` and `$CODEX_HOME` move the targets, but only within
home; one pointing elsewhere is a conflict. Setup steps are listed, not run: they belong to
projects. Project trust gaps do not apply.

Kit does not write through symlinks, so a `~/.claude` or `~/.codex` linked from a dotfiles
repository (stow and the like) is refused; point `CLAUDE_CONFIG_DIR` / `CODEX_HOME` at the
real folder under home instead. Kit removes only its own files from `~/.kit/`, and a
comment inside a `[[skills.config]]` table Kit owns blocks `off` until you move it.

**Skills.** A skill primitive with a directory (`primitives/skills/<name>/` with a
`SKILL.md`) is copied to `.claude/skills/<name>/` and `.agents/skills/<name>/`
(`.cursor/skills/` for Cursor), one owned file per asset. A changed skill in the warehouse
updates the copy; `off` removes it. An existing file Kit did not write, a symlink, or a
non-UTF-8 asset is a conflict. External skills (`external: true`) are not copied.

**Setup steps.** After `kitctl use` writes a loadout's config, it lists the loadout's
`setup` steps and asks `Run N setup step(s)? [y/N]` (`--yes` runs them). A step whose
`when: file-exists:PATH` is missing is skipped with the reason, and `${plugin:NAME}` becomes
the installed Claude Code plugin's directory (skipped when the plugin is not installed).
Steps run in the project with no shell; their outputs belong to the tool, so `off` leaves
them. `kitctl use <loadout>` again re-runs them; `apply` only lists them.

Every write takes `--dry-run`. Without a terminal and without `--yes`, nothing is written.

## Generators

Expand Down Expand Up @@ -116,6 +144,5 @@ Cursor effect also comes with an `unverified` gap.
- **Formatting.** JSON keeps indentation, key order, line endings and the final
newline, but `json.dumps` expands compact arrays in files Kit edits. CRLF
files stay CRLF.
- **Not done yet.** `setup` steps are listed in the plan but not run. Skill
directory copies are not generated. User-scope (`~/`) targets are not
generated; brand-studio is entirely project-scoped.
- **Not done yet.** User-scope (`~/`) targets are not generated; brand-studio is
entirely project-scoped.
Loading
Loading