diff --git a/.claude/commands/upgrade.md b/.claude/commands/upgrade.md new file mode 100644 index 0000000..7bcad13 --- /dev/null +++ b/.claude/commands/upgrade.md @@ -0,0 +1,146 @@ +--- +description: Upgrade an already-adopted agentic-dev-kit installation to the current kit — migrate the config schema, refresh kit-owned engines, and diff anything that drifted. The counterpart to /adopt (first install) and to re-running ./init.sh (config only). Use when pulling a kit update into a repo that already has the kit. +argument-hint: "[--dry-run]" +--- + +Upgrade this repo's agentic-dev-kit installation. Non-destructive: runs on a branch, and +never replaces a file without knowing it is safe to replace. + +> **The invariant this rests on.** Engines are **kit-owned**; config is **adopter-owned**. +> Everything project-specific — paths, tracker, review-bot markers, CI policy, model +> tiers — lives in `config/dev-model.yaml`, so an engine should never need editing to +> adopt it. That is what makes an upgrade a file copy instead of a manual merge. If this +> run finds engines you had to edit, that is a **kit bug**: report it rather than +> carrying the patch forward. + +## Step 0 — Establish what shape this repo is in + +Four shapes exist in the wild and they need different handling. Determine which: + +```bash +ls config/dev-model.yaml 2>/dev/null && echo "has config" || echo "NO CONFIG" +``` + +- **No `config/dev-model.yaml`** → this repo predates the config surface entirely (a kit + *ancestor*, or a partial hand-install). **Stop and run `/adopt` instead** — there is no + schema to migrate from. Say so plainly rather than guessing a config into existence. +- **Config present** → continue. `kit.version` tells you the schema generation; its + absence means v1 (pre-`runtime:`, pre-`models.tiers`). + +Also fetch the kit you are upgrading *to*, if it isn't already local: + +```bash +git clone --depth 1 https://github.com/topij/agentic-dev-kit /tmp/agentic-dev-kit +``` + +Everything below copies **from** that checkout **into** this repo. + +## Step 1 — Diff the installation (read-only) + +```bash +uv run /kit_doctor.py --manifest /tmp/agentic-dev-kit/kit-manifest.json +``` + +Read `` from `paths.engines`. If `kit_doctor.py` isn't installed yet, run the +kit's copy against this repo: `uv run /tmp/agentic-dev-kit/scripts/kit_doctor.py --root . +--manifest /tmp/agentic-dev-kit/kit-manifest.json`. + +The report gives you, per kit-owned file: `unchanged` / `differs` / `missing` / +`unknown-version`, plus four installation-level checks. **Read all four** — each is a +silent failure mode: + +- **config schema version** — unversioned or behind means migrations are pending. +- **`paths.engines` resolves to a directory that actually holds engines.** A `✗` here is + the live breakage where every workflow's `/…` reference points at nothing. + Nothing else validates this value, so nothing else would have told you. +- **pre-push hook installed** — a shipped-but-uninstalled hook binds nothing. +- **narrative docs rendered** — a doc still carrying `devkit-template: unrendered` means + the adoption never completed its seeding step. + +`differs` deliberately does **not** claim a cause: a hash mismatch cannot distinguish +"older kit version" from "hand-edited". The report narrows it by schema version; you +confirm with an actual diff in Step 3. + +## Step 2 — Migrate the config (idempotent) + +```bash +./init.sh +``` + +This is the supported config upgrade path, and it is safe to re-run any number of times. +It only ever **adds** missing keys, never guesses over an existing value; it probes +`paths.engines` from where engines actually are rather than defaulting; it stamps +`kit.version`; it installs the pre-push hook as a shim (honoring `core.hooksPath`); and +it leaves a narrative doc that is genuinely in use byte-identical, re-rendering only one +that still carries the unrendered marker. + +Press Enter through every prompt to keep current values. Then re-read the diff of +`config/dev-model.yaml` and confirm nothing you rely on changed. + +## Step 3 — Refresh engines, by state + +Work through `kit_doctor`'s file list. **Branch first:** + +```bash +git checkout -b chore/kit-upgrade +``` + +- **`unchanged`** → copy the new version straight in. It is provably untouched, so there + is nothing to lose. +- **`missing`** → decide, don't assume. A sized-down adoption omits engines deliberately + (one surveyed repo installs 2 of 6 on purpose). Ask the operator whether each missing + piece is wanted before installing it. If a piece stays out, note it in the PR body so + the next upgrade doesn't re-litigate it. +- **`differs`** → `diff` the local file against the kit's, and read the diff: + - Only kit-authored changes (the local copy is simply older) → replace it. + - Local edits present → for each, find where that value now lives in + `config/dev-model.yaml` and move it there, then take the kit's engine. If there is no + config key for it, **stop**: that is the kit bug the invariant above describes. File + it upstream and keep the local patch, clearly flagged, until it lands. + - **Local edits that are genuinely ahead of the kit** — a fix made here first — are the + one case to route *upstream* instead: open a PR against the kit rather than + overwriting your better version. +- **`unknown-version`** → the manifest has no entry, so drift is unjudgeable. Treat as + `differs` and diff by hand. + +Never batch-replace the whole list because most of it was `unchanged`. The `differs` +entries are exactly where the risk is. + +## Step 4 — Refresh the non-engine pieces + +- **Shared workflows** (`docs/agentic-dev-kit/workflows/`) — same state logic as engines. + These are prompts an agent reads verbatim; a stale one silently teaches old behavior. +- **Runtime adapters** (`.claude/commands/`, `.agents/skills/`) — install any the kit has + that this repo lacks; keep the adopter's version where one already exists. +- **Templates** (`docs/templates/`) — refresh freely; the *rendered* docs are yours and + are never touched. +- **`.claude/settings.json`** — if this repo has its own, **merge** the kit's hooks and + permissions into it rather than replacing; it likely carries project-specific entries. + +## Step 5 — Verify + +```bash +uv run /kit_doctor.py --manifest /tmp/agentic-dev-kit/kit-manifest.json +python -m pytest /lib/state_paths/tests /tests -q +uv run /check_doc_budget.py +``` + +`kit_doctor` should now report zero `differs` and zero `unknown-version`, with `missing` +containing only deliberately-omitted pieces. Anything else means Step 3 left something. + +> **Known gotcha:** the `state_paths` tests fail when run from inside a worktree carrying +> a `.devkit_state_root` marker — the fixture neutralizes the environment but not marker +> discovery. Run the gate from the main checkout, and see kit issue #10. + +## Step 6 — Record the friction, then hand off + +Append anything this upgrade surfaced to the friction log (`paths.friction_log`) — an +engine you had to edit, a config key that didn't exist, a `missing` piece the report +couldn't classify. Tag `[kit]` on anything that is a kit-side fix and open an issue +upstream. That is Principle #2 applied to the kit itself, and it is how the four shapes +this skill handles were discovered in the first place. + +Open a **draft PR** summarizing: schema version before → after, which engines were +refreshed / diffed / deliberately skipped, and any local-edit-vs-config resolution you +made. Leave the merge to the operator — an upgrade touches the machinery every other +workflow runs on. diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index bb6c830..14ff6f1 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -24,6 +24,11 @@ jobs: run: test -x init.sh - name: Run state and portability tests run: python -m pytest scripts/lib/state_paths/tests scripts/tests -q + - name: kit-manifest.json must be in sync + # A stale manifest silently degrades every adopter's drift report to + # "unknown-version", so the self-check is a gate, not a convenience. + # Stdlib-only by design, so no uv needed here. + run: python scripts/kit_doctor.py - name: Engines must work without PyYAML # The stdlib config reader exists so engines carry no third-party # dependency. Prove it in an env where PyYAML genuinely is not installed. diff --git a/README.md b/README.md index 7fb666e..9d4228b 100644 --- a/README.md +++ b/README.md @@ -127,6 +127,40 @@ path, and it is safe to run any number of times: lives in `config/dev-model.yaml`, so you should never need to edit an engine to adopt it. If you have, that's a bug — please report it. +### Which files actually drifted — `kit_doctor` + +Re-running `init.sh` handles the config. For the **engines**, the problem is that a +copy-in has no version marker and no record of whether it was edited, so nothing can +tell an older engine from a locally-patched one: + +```sh +uv run scripts/kit_doctor.py # or: python scripts/kit_doctor.py — stdlib only +``` + +Per kit-owned file it reports `unchanged` (safe to replace outright), `differs` (diff +before replacing), `missing`, or `unknown-version`. `differs` deliberately does **not** +claim a cause — a hash mismatch can't distinguish "older version" from "hand-edited", so +it narrows by schema version and leaves the call to you. Adopter-owned paths (your +config, your narrative docs) are never compared; they're *supposed* to differ. + +It also checks four things nothing else validates: + +- your config's schema generation vs. the kit's +- that **`paths.engines` points at a directory that actually holds engines** — a `✗` here + is the silent breakage where every workflow's `/…` reference resolves to + nothing +- that the pre-push hook is installed, not merely shipped +- that the narrative docs were rendered, not left as unrendered templates + +`kit-manifest.json` is the hash set it compares against, regenerated at release +(`--generate-manifest`) and gated in CI so it can't go stale. + +**[`/upgrade`](.claude/commands/upgrade.md)** drives the whole sequence — shape detection, +config migration, then per-file refresh keyed on those states — non-destructively, on a +branch. It's the counterpart to [`/adopt`](.claude/commands/adopt.md) (first install). A +repo with no `config/dev-model.yaml` at all predates the config surface and routes to +`/adopt` instead. + ### Agent runtime adapters The workflow definitions under `docs/agentic-dev-kit/workflows/` are shared. The diff --git a/kit-manifest.json b/kit-manifest.json new file mode 100644 index 0000000..9d06c12 --- /dev/null +++ b/kit-manifest.json @@ -0,0 +1,106 @@ +{ + "adopter_owned": [ + "config/dev-model.yaml", + "docs/handoff.md", + "docs/handoff-history.md", + "docs/friction-log.md", + "docs/friction-log-archive.md", + "README.md", + ".gitignore" + ], + "files": { + "docs/agentic-dev-kit/safety-critical-changes.md": { + "role": "doctrine", + "sha256": "cc3f3a76a22ba0a8552539bd2b85f07e10e59485eae2c3749f06036bf7db2e91" + }, + "docs/agentic-dev-kit/workflows/parallel.md": { + "role": "workflow", + "sha256": "519fa1c623b75ba8cc0c6a3377f49a9681f47ada49f141aaf130d4007fffd460" + }, + "docs/agentic-dev-kit/workflows/pr-watch.md": { + "role": "workflow", + "sha256": "169d3e27e149a49f736931b838d117a2fe6ad3ac6ddcaefccb99a51b97b505c8" + }, + "docs/agentic-dev-kit/workflows/session-start.md": { + "role": "workflow", + "sha256": "f7a0a3ba8b65bc82f19764de6430bf82d0ee11d08447936b03de0056ac152c3a" + }, + "docs/agentic-dev-kit/workflows/wrap-up.md": { + "role": "workflow", + "sha256": "63c26e6337c44653a4b710af68a5bad03e61e4fe0a2231479df4ab105053e071" + }, + "docs/templates/friction-log-archive.md.tmpl": { + "role": "template", + "sha256": "af4a138e4a5ebe5634c55e7e417cd6b9f362bf3a35182b033f72876cd88180cf" + }, + "docs/templates/friction-log.md.tmpl": { + "role": "template", + "sha256": "33e5bf0fe285ebbc248e062124d52f86bb78c47767f185d3d04df192db09c66d" + }, + "docs/templates/handoff-history.md.tmpl": { + "role": "template", + "sha256": "111cf71a4c9b2a296f4fcf5ea02c6516907710e5b4fe0a7b3074f444f862ae69" + }, + "docs/templates/handoff.md.tmpl": { + "role": "template", + "sha256": "1654465836a9e70a6e81d76208783e9a12d65343d1e9c3091f1d30c25a437ce7" + }, + "scripts/archive_plan_sessions.py": { + "role": "engine", + "sha256": "fb15bd2dbe03257e6304f6e40f5af14bdffa66288fe0cc7ef2cdc38d0b4f9c76" + }, + "scripts/check_doc_budget.py": { + "role": "engine", + "sha256": "a402424a12278e6a2ba55f90692aef9e44e9f030932529b5e85208590fc4e6dd" + }, + "scripts/dev_session.sh": { + "role": "engine", + "sha256": "80311d61d9210c633560cd493402724e64aba9ff95b0ab02a40c97fb130d22eb" + }, + "scripts/hooks/pre-push": { + "role": "hook", + "sha256": "c469700ff61a1506fa5951b4d5ab3ac95f3213227d8838efa95eef0492516fba" + }, + "scripts/kit_doctor.py": { + "role": "engine", + "sha256": "23303dfac720e6f20cd1b1131d44b44893bbde8d061d18eadbb26a86a4252eef" + }, + "scripts/lib/devmodel_config.py": { + "role": "engine", + "sha256": "53458a5d0c72b1fe5e5e5f6e8e5881c08c8c03c255493ae841391c5a8d6c1a29" + }, + "scripts/lib/kitconfig.py": { + "role": "engine", + "sha256": "41c01a2956185d898f6cb75865d713845630a2d8335976f1460de1ed5f875317" + }, + "scripts/lib/repo_root.sh": { + "role": "engine", + "sha256": "980cbf5596cea67033a5dd02d53630f2a92c24afd693ba7727d5fc50303ff555" + }, + "scripts/lib/state_paths/__init__.py": { + "role": "engine", + "sha256": "691a7df06a3bf9019b5faca8b8ebdb9d54463d231d492a632fd54c16cd51a2c6" + }, + "scripts/lib/state_paths/paths.py": { + "role": "engine", + "sha256": "f54fd7d970402f8581f6c5080aa9614ce41cb2dafa2010c08406129731799397" + }, + "scripts/lib/state_paths/repo_root.py": { + "role": "engine", + "sha256": "d9412f1fe395b944a86126bcc39f85d6838382b0fc29d68c8b29b965cd530b5a" + }, + "scripts/lib/state_paths/resolver.py": { + "role": "engine", + "sha256": "e39fca241d6fbf28596ed3954f28a55e65831181cedb02c7c15e372895bb5bd2" + }, + "scripts/pr_watch.py": { + "role": "engine", + "sha256": "8e1fa0e8e14cc4e26172a91ea675194f7f985592a139ac5a09dd9def97deecd1" + }, + "scripts/reconcile_sessions.sh": { + "role": "engine", + "sha256": "825796c961162b4b69ecacd4c6a228a3395213fc6323df957b0a123896e59a53" + } + }, + "kit_version": 2 +} diff --git a/scripts/archive_plan_sessions.py b/scripts/archive_plan_sessions.py index b56b195..0fa14d3 100755 --- a/scripts/archive_plan_sessions.py +++ b/scripts/archive_plan_sessions.py @@ -1,7 +1,7 @@ #!/usr/bin/env -S uv run --script # /// script # requires-python = ">=3.12" -# dependencies = ["pyyaml"] +# dependencies = [] # /// """Sweep old session blocks out of the live handoff into its history document. @@ -42,12 +42,11 @@ import sys from pathlib import Path -import yaml sys.path.insert(0, str(Path(__file__).resolve().parent / "lib")) -from devmodel_config import _repo_root, load_config, resolve_path # noqa: E402 +from kitconfig import load_config, repo_root, resolve_path # noqa: E402 -REPO_ROOT = _repo_root() +REPO_ROOT = repo_root() SEP = "______________________________________________________________________\n" SESSION_PREFIXES = ("## Latest session", "## Earlier session", "## Session — ") # Recent sessions may write *dated* headings (`## June 5 Fri (cont.) — …`) or a diff --git a/scripts/check_doc_budget.py b/scripts/check_doc_budget.py index 74f83bb..dfd4c77 100755 --- a/scripts/check_doc_budget.py +++ b/scripts/check_doc_budget.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # /// script # requires-python = ">=3.12" -# dependencies = ["pyyaml"] +# dependencies = [] # /// """Warn-only tripwire: nudge an archive sweep when a handoff doc grows too large. @@ -18,7 +18,7 @@ The tracked docs + their budgets come from ``config/dev-model.yaml``'s ``doc_budgets`` list (``{path, budget, archive, remedy}``) — see -``scripts/lib/devmodel_config.py``. Adjust the budgets there, not here. +``scripts/lib/kitconfig.py``. Adjust the budgets there, not here. Usage: @@ -43,12 +43,12 @@ from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parent / "lib")) -from devmodel_config import _repo_root, get, load_config # noqa: E402 +from kitconfig import get, load_config, repo_root # noqa: E402 # Discover the repo root by walking up for a `.git` marker (via devmodel_config) # rather than assuming a fixed `scripts/