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
146 changes: 146 additions & 0 deletions .claude/commands/upgrade.md
Original file line number Diff line number Diff line change
@@ -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 <engine-dir>/kit_doctor.py --manifest /tmp/agentic-dev-kit/kit-manifest.json
```

Read `<engine-dir>` 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 `<engine-dir>/…` 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.
Comment on lines +64 to +70

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Create the upgrade branch before running migration.

Step 2 mutates configuration, hooks, and documentation, but the branch is created only in Step 3. This contradicts the “runs on a branch” guarantee and can leave migration changes on the operator’s current branch. Move the branch-creation step before ./init.sh.

Also applies to: 82-86

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.claude/commands/upgrade.md around lines 64 - 70, Move the branch-creation
step from Step 3 to before the Step 2 migration command in the upgrade
instructions. Ensure ./init.sh runs only after the upgrade branch has been
created, while preserving the existing branch workflow and idempotent migration
guidance.

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
Comment on lines +64 to +73

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Run the fetched kit’s migrator, not a potentially stale local ./init.sh.

This procedure fetches the current kit but then invokes the target repository’s existing init.sh before refreshing it. An older adopted installation may therefore miss the new schema migrations while appearing to complete successfully. Run /tmp/agentic-dev-kit/init.sh against this repository, or refresh init.sh before invoking it.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.claude/commands/upgrade.md around lines 64 - 73, Update the Step 2
migration command to execute the freshly fetched kit migrator at
/tmp/agentic-dev-kit/init.sh against the target repository, rather than the
repository’s potentially stale ./init.sh. Alternatively, explicitly refresh the
local init.sh before running it, while preserving the documented idempotent
migration behavior.

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 <engine-dir>/kit_doctor.py --manifest /tmp/agentic-dev-kit/kit-manifest.json
python -m pytest <engine-dir>/lib/state_paths/tests <engine-dir>/tests -q
uv run <engine-dir>/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.
5 changes: 5 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
34 changes: 34 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```
Comment on lines +136 to +138

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use a shell comment instead of an em dash.

python scripts/kit_doctor.py — stdlib only passes the Unicode em dash and words as arguments, so copying this alternative can fail argument parsing. Use python scripts/kit_doctor.py # stdlib only or move the note outside the code block.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@README.md` around lines 136 - 138, Update the README command example to
replace the Unicode em dash note with a valid shell comment, ensuring the
alternative python invocation passes no unintended arguments; preserve the
existing uv invocation and explanatory note.


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 `<engine-dir>/…` 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
Expand Down
106 changes: 106 additions & 0 deletions kit-manifest.json
Original file line number Diff line number Diff line change
@@ -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
}
7 changes: 3 additions & 4 deletions scripts/archive_plan_sessions.py
Original file line number Diff line number Diff line change
@@ -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.

Expand Down Expand Up @@ -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
Expand Down
8 changes: 4 additions & 4 deletions scripts/check_doc_budget.py
Original file line number Diff line number Diff line change
@@ -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.

Expand All @@ -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:

Expand All @@ -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/<script>.py` depth — so this keeps working
# when the kit is vendored under a nested dir (e.g. scripts/devkit/).
REPO_ROOT = _repo_root()
REPO_ROOT = repo_root()
Comment on lines +46 to +51

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

main() doesn't catch KeyError from the new fail-loud get().

kitconfig.get(config, "doc_budgets") (line 76) has no default, so per its documented contract it raises KeyError when the key is absent — but main()'s except (FileNotFoundError, ValueError) (line 150) doesn't include KeyError. A config missing doc_budgets now crashes with an unhandled traceback instead of the intended graceful error: ... + exit 2, directly undercutting the PR's goal (and issue #5's explicit requirement) of warning and exiting cleanly on missing/unreadable config.

🛡️ Proposed fix
     try:
         statuses = evaluate(args.root, args.config)
-    except (FileNotFoundError, ValueError) as exc:
+    except (FileNotFoundError, ValueError, KeyError) as exc:
         print(f"error: {exc}", file=sys.stderr)
         return 2
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/check_doc_budget.py` around lines 46 - 51, Update main() to catch
KeyError alongside FileNotFoundError and ValueError when loading or validating
configuration, so a missing doc_budgets key follows the existing graceful error
message and exit-2 path instead of producing an unhandled traceback. Keep the
existing get(config, "doc_budgets") behavior unchanged.



@dataclass(frozen=True)
Expand Down
Loading
Loading