From 21b322e3ba6a406ecd1942ffd68d536d8b970fbc Mon Sep 17 00:00:00 2001 From: Connor England Date: Thu, 22 Oct 2026 14:13:01 -0500 Subject: [PATCH 1/3] Sync npm README with the root README scripts/prepare_npm.py copies the root README over npm/README.md at build time; the committed copy was behind (agents, doctor budget). This is that generated copy, unchanged. --- npm/README.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/npm/README.md b/npm/README.md index 49ac01d..23ed558 100644 --- a/npm/README.md +++ b/npm/README.md @@ -1,6 +1,6 @@ # Kit (`kitctl`) -Kit manages the skills, MCP servers, hooks, plugins and instruction blocks your coding +Kit manages the skills, agents, 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` @@ -12,7 +12,9 @@ puts back exactly what was there. - 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). +- copy warehouse skill folders and worker agents, and run a loadout's setup steps (after + asking); +- estimate each harness's always-on listing and hold it to a loadout `budget` (`doctor`). Kit never writes trust: approving hooks, MCP servers and projects stays with you. @@ -64,6 +66,10 @@ name. It never fixes anything or starts an MCP server: uv run kitctl doctor --loadout brand-studio --root ~ --root path/to/repo ``` +It also estimates the always-on listing each harness carries for the loadout plus the +roots' `kit.yaml` loadouts (instruction block, listed skills and agents, `ceil(chars / 4)`) +and warns when it is over a loadout's `budget` (see [the model](docs/model.md#budget)). + `use`, `off`, `plan`, `apply`, `status` and `eject` manage a project's native 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 From 3c1f119e051fc9900218c124fd7815463025955d Mon Sep 17 00:00:00 2001 From: Connor England Date: Thu, 22 Oct 2026 14:13:02 -0500 Subject: [PATCH 2/3] Publishing: 0.1.0 release record and 0.1.1 steps Record what 0.1.0 published (tag, runs, hashes, npm integrity). The 0.1.1 steps fix two problems met while publishing 0.1.0: verify TestPyPI from the downloaded wheel (uv will not mix indexes for ruamel-yaml), and build the npm tarball around the PyPI wheel so both registries ship the same bytes. --- docs/publishing.md | 42 +++++++++++++++++++++++++++++++++--------- 1 file changed, 33 insertions(+), 9 deletions(-) diff --git a/docs/publishing.md b/docs/publishing.md index f75d4fd..de800d5 100644 --- a/docs/publishing.md +++ b/docs/publishing.md @@ -1,20 +1,44 @@ # Publishing -## 0.1.0 — prepared 2026-10-21, not yet published +## 0.1.1 — prepared 2026-10-22, not yet published -The owner publishes; nothing is uploaded from a cloud session. Same procedure as 0.0.1 -below, with the new tag: +Fixes piped output on Windows (see CHANGELOG). Same procedure as 0.1.0, with two +corrections learned while publishing 0.1.0: 1. Merge the release PR to main and confirm CI is green on Linux and Windows. -2. Tag the merge commit: `git tag v0.1.0 && git push origin v0.1.0`. -3. `gh workflow run publish-python.yml --ref v0.1.0 -f target=testpypi`, then - `uvx --index https://test.pypi.org/simple/ --from kitctl==0.1.0 kitctl --version`. -4. `gh workflow run publish-python.yml --ref v0.1.0 -f target=pypi`, then - `uvx --from kitctl==0.1.0 kitctl --version`. -5. npm, from the same tag: the [npm](#npm) steps with `kitctl-0.1.0.tgz`. +2. Tag the merge commit: `git tag v0.1.1 && git push origin v0.1.1`. +3. `gh workflow run publish-python.yml --ref v0.1.1 -f target=testpypi`. Verify the + TestPyPI wheel itself, with every dependency from PyPI: download + `kitctl-0.1.1-py3-none-any.whl` from TestPyPI, check its sha256 against the index, then + `uvx --default-index https://pypi.org/simple --from kitctl --version`. + (`uvx --index https://test.pypi.org/simple/ ...` fails: TestPyPI has `ruamel-yaml`, but + not at `>=0.18.10,<0.19`, and uv takes each package from the first index that has it.) +4. `gh workflow run publish-python.yml --ref v0.1.1 -f target=pypi`, then + `uvx --from kitctl==0.1.1 kitctl --version`. +5. npm, from the same tag: the [npm](#npm) steps with `kitctl-0.1.1.tgz`, in a clone made + with `git -c core.autocrlf=false clone --branch v0.1.1 ...`. After `prepare_npm.py`, + replace `npm/python/kitctl-0.1.1-py3-none-any.whl` with the wheel downloaded from PyPI + and rerun `npm test`, so npm ships the same bytes as PyPI. (A Windows build differs from + the Linux CI wheel at least in each zip entry's "made on" byte, and in line endings when + `core.autocrlf` is on.) 6. Smoke test in a temporary HOME: `kitctl init`, `kitctl validate`, `kitctl use starter --repo --yes`, `kitctl off starter --repo --yes`. +## 0.1.0 — verified release record, 2026-10-22 + +- Tag v0.1.0, commit 42351245d127e61a12b430bed795ab63f4f9bd8d. +- [TestPyPI](https://test.pypi.org/project/kitctl/0.1.0/) by + [run 39787975633](https://github.com/ConnorBritain/kitctl/actions/runs/39787975633); + [PyPI](https://pypi.org/project/kitctl/0.1.0/) by + [run 39788913708](https://github.com/ConnorBritain/kitctl/actions/runs/39788913708). Both + indexes hold the same files: wheel sha256 + b5a5b96479092e952096243313ffa914584dd9e518c91884da22a2f359d3e377, sdist sha256 + aa249b7df571d74fa0fd4f9ea2a12a658d79aaf6dd9259151003a440432c09bb. +- [npm kitctl 0.1.0](https://www.npmjs.com/package/kitctl/v/0.1.0), integrity + sha512-KV9DqP8resEkYc6L0hmeZhqa0S8u0fnLxpVTqfu/Ej6J5PtB4PoftASqi8zIqeROFvwgSWmopMMmKQrLgrb9rg==; + it bundles the PyPI wheel byte for byte. +- Smoke test in a temporary HOME passed; `off` left the repository byte-identical. + # Publishing 0.0.1 ## Verified release record — 2026-10-11 From a2a54efc5d351ecbcf90faab0958175f51e24a33 Mon Sep 17 00:00:00 2001 From: Connor England Date: Thu, 22 Oct 2026 14:13:03 -0500 Subject: [PATCH 3/3] Print UTF-8 on every platform; prepare 0.1.1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On Windows, piped stdout uses the ANSI code page (cp1252), so kitctl plan, apply and use stopped with "'charmap' codec can't encode character '\u2192'" before showing the plan. main() now reconfigures stdout and stderr to UTF-8, keeping each stream's error handler. A subprocess test runs kitctl with PYTHONIOENCODING=cp1252 and checks help ("—") and an error path ("→"). Bump 0.1.0 -> 0.1.1 in pyproject, the package, the npm launcher, uv.lock and both version tests. --- CHANGELOG.md | 10 +++++++++- npm/bin/kitctl.mjs | 2 +- npm/package.json | 2 +- npm/test/cli.test.mjs | 2 +- pyproject.toml | 2 +- src/kitctl/__init__.py | 2 +- src/kitctl/cli.py | 6 ++++++ tests/test_inventory.py | 2 +- tests/test_output_encoding.py | 26 ++++++++++++++++++++++++++ uv.lock | 2 +- 10 files changed, 48 insertions(+), 8 deletions(-) create mode 100644 tests/test_output_encoding.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d1cce8..3066a5d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,15 @@ ## Unreleased -## 0.1.0 — not yet published (tag `v0.1.0`; see docs/publishing.md) +## 0.1.1 — not yet published (tag `v0.1.1`; see docs/publishing.md) + +- Output is UTF-8 on every platform. On Windows, piped output (an agent, CI, `| more`) + used the ANSI code page, so `plan`, `apply` and `use` stopped with `'charmap' codec can't + encode character '→'` before showing the plan, and help printed `Kit � inventory`. +- Docs: docs/publishing.md now says to verify TestPyPI from the downloaded wheel, and build the npm tarball + around the PyPI wheel. + +## 0.1.0 — 2026-10-22 (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. diff --git a/npm/bin/kitctl.mjs b/npm/bin/kitctl.mjs index 7d4b8c9..a9b812d 100755 --- a/npm/bin/kitctl.mjs +++ b/npm/bin/kitctl.mjs @@ -3,7 +3,7 @@ import { spawnSync } from "node:child_process"; import { existsSync } from "node:fs"; import { fileURLToPath } from "node:url"; -const wheel = fileURLToPath(new URL("../python/kitctl-0.1.0-py3-none-any.whl", import.meta.url)); +const wheel = fileURLToPath(new URL("../python/kitctl-0.1.1-py3-none-any.whl", import.meta.url)); if (!existsSync(wheel)) { console.error("kitctl: bundled Python wheel is missing; reinstall the npm package."); process.exit(1); diff --git a/npm/package.json b/npm/package.json index f339b6d..d33e476 100644 --- a/npm/package.json +++ b/npm/package.json @@ -1,6 +1,6 @@ { "name": "kitctl", - "version": "0.1.0", + "version": "0.1.1", "description": "Kit: inventory and manage agent skills, MCP connectors, hooks, and loadouts", "type": "module", "bin": { "kitctl": "bin/kitctl.mjs" }, diff --git a/npm/test/cli.test.mjs b/npm/test/cli.test.mjs index 6444e52..5a5cc69 100644 --- a/npm/test/cli.test.mjs +++ b/npm/test/cli.test.mjs @@ -14,7 +14,7 @@ test("bundled wheel works without a PyPI release and forwards arguments", () => try { const version = spawnSync(process.execPath, [bin, "--version"], { env, encoding: "utf8" }); assert.equal(version.status, 0, version.stderr); - assert.match(version.stdout, /^kitctl 0\.1\.0\r?\n$/); + assert.match(version.stdout, /^kitctl 0\.1\.1\r?\n$/); const repo = path.join(root, "repo with spaces"); mkdirSync(repo); writeFileSync(path.join(repo, "AGENTS.md"), "Fixture instructions"); diff --git a/pyproject.toml b/pyproject.toml index e756919..b798b36 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "kitctl" -version = "0.1.0" +version = "0.1.1" description = "Inventory and manage agent skills, MCP connectors, hooks, and loadouts." readme = "README.md" requires-python = ">=3.11" diff --git a/src/kitctl/__init__.py b/src/kitctl/__init__.py index af6123c..36579b9 100644 --- a/src/kitctl/__init__.py +++ b/src/kitctl/__init__.py @@ -1,3 +1,3 @@ """Kit: inventory and manage agent tooling through deterministic loadouts.""" -__version__ = "0.1.0" +__version__ = "0.1.1" diff --git a/src/kitctl/cli.py b/src/kitctl/cli.py index 6c3e3ff..4c31725 100644 --- a/src/kitctl/cli.py +++ b/src/kitctl/cli.py @@ -21,6 +21,12 @@ def main(argv: list[str] | None = None) -> int: + # Windows pipes default to the ANSI code page, which cannot print the "—" and "→" in + # help, plans and instruction blocks. + for stream in (sys.stdout, sys.stderr): + reconfigure = getattr(stream, "reconfigure", None) + if reconfigure is not None: + reconfigure(encoding="utf-8", errors=getattr(stream, "errors", None)) parser = argparse.ArgumentParser( prog="kitctl", description="Kit — inventory and manage agent tooling." ) diff --git a/tests/test_inventory.py b/tests/test_inventory.py index fa40db1..3b60b50 100644 --- a/tests/test_inventory.py +++ b/tests/test_inventory.py @@ -64,4 +64,4 @@ def test_version(capsys: pytest.CaptureFixture) -> None: with pytest.raises(SystemExit) as result: main(["--version"]) assert result.value.code == 0 - assert capsys.readouterr().out == "kitctl 0.1.0\n" + assert capsys.readouterr().out == "kitctl 0.1.1\n" diff --git a/tests/test_output_encoding.py b/tests/test_output_encoding.py new file mode 100644 index 0000000..28b6474 --- /dev/null +++ b/tests/test_output_encoding.py @@ -0,0 +1,26 @@ +import os +import subprocess +import sys +from pathlib import Path + + +# Windows sends piped output through the ANSI code page (cp1252), and kitctl's help, plans and +# instruction blocks contain "—" and "→"; without UTF-8 output, every scripted or agent-driven +# run (`kitctl plan | more`, CI, a coding agent) dies before it can show its plan. +def test_piped_output_survives_a_non_utf8_code_page(tmp_path: Path) -> None: + env = {**os.environ, "PYTHONIOENCODING": "cp1252"} + env.pop("PYTHONUTF8", None) + done = subprocess.run( + [sys.executable, "-m", "kitctl", "--help"], capture_output=True, env=env, check=False + ) + assert done.returncode == 0, done.stderr.decode("utf-8", "replace") + assert "Kit — inventory".encode() in done.stdout + missing = tmp_path / "a→b" + failed = subprocess.run( + [sys.executable, "-m", "kitctl", "inventory", "--root", str(missing)], + capture_output=True, + env=env, + check=False, + ) + assert failed.returncode == 2, failed.stderr.decode("utf-8", "replace") + assert "a→b".encode() in failed.stderr diff --git a/uv.lock b/uv.lock index 31f96ff..241b89b 100644 --- a/uv.lock +++ b/uv.lock @@ -26,7 +26,7 @@ wheels = [ [[package]] name = "kitctl" -version = "0.1.0" +version = "0.1.1" source = { editable = "." } dependencies = [ { name = "ruamel-yaml" },