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/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 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 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" },