From a09fb8059c4620c61765883e920bd5ebc8898abb Mon Sep 17 00:00:00 2001 From: Topi Jarvinen Date: Sat, 25 Jul 2026 00:23:09 +0300 Subject: [PATCH 1/5] feat: fix the adoption path and make engines upgradable (kit schema v2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 0 of the kit refactor — the shared primitives the follow-on lanes need, plus the two bugs that broke adoption at step one. Adoption (was: broken for every adopter) - init.sh was committed mode 100644, so the documented `./init.sh` failed with "permission denied". Now 100755, pinned by a CI check. - The narrative-doc seeding could never fire: the kit SHIPS docs/handoff.md and docs/friction-log.md, so a `cp -r` or template-clone always landed them before init.sh ran and the "seed only if absent" guard was permanently false. Every adopter started with a `my-project` header and a `` placeholder that init.sh never stamped (it did not even prompt for the URL). Docs now render from docs/templates/*.tmpl, keyed on a `devkit-template: unrendered` marker that distinguishes the file the kit shipped from one someone is actually using. init.sh now prompts for tracker.url. All four narrative docs are seeded, not just two. Upgrade path (new) - kit.version stamps the config schema generation; re-running ./init.sh is now the supported upgrade, migrating an older config forward in place and never guessing over an existing value. - paths.engines is PROBED rather than defaulted. An adopter who vendored the engines under scripts/devkit/ was being migrated to `engines: scripts`, so every workflow's /… reference resolved to a directory with no engines in it — silently, since nothing validates the value. - The pre-push hook is installed, as a shim that execs the engine. Shipping it uninstalled made the kit's own mechanism-over-memory exemplar (Principle #8) inert in every adopting repo. - .mcp.json is added to .gitignore when it holds literal credentials, since dev_session.sh copies it into every lane worktree. Engines become kit-owned (prerequisite for ever replacing one) - scripts/lib/kitconfig.py: stdlib-only reader for the config subset the kit uses, verified byte-equal to PyYAML on the shipped config and on two real adopter configs. Engines can now read config without a third-party dependency, which is what lets pr_watch's review-bot markers move OUT of the engine. - config gains review.noise_markers / unavailable_markers / informational_checks / require_ci, and documents DEVKIT_CI_ENV_VARS. The engines are wired to them in the follow-on lane; this lands the schema so the three lanes don't collide on this file. Verified end-to-end: fresh adoption renders correctly through a pty; a re-run leaves an in-use handoff byte-identical; brain's real v1 config + scripts/devkit layout migrates to v2 with paths.engines detected and its 223-line handoff untouched. Refs #5 (config reader is now stdlib-only). --- .github/workflows/test.yml | 14 +- README.md | 30 +++ config/dev-model.yaml | 54 ++++ docs/friction-log-archive.md | 2 + docs/friction-log.md | 2 + docs/handoff-history.md | 2 + docs/handoff.md | 2 + docs/templates/friction-log-archive.md.tmpl | 4 + docs/templates/friction-log.md.tmpl | 20 ++ docs/templates/handoff-history.md.tmpl | 6 + docs/templates/handoff.md.tmpl | 26 ++ init.sh | 270 +++++++++++++++----- scripts/lib/kitconfig.py | 250 ++++++++++++++++++ scripts/tests/test_kitconfig.py | 167 ++++++++++++ 14 files changed, 781 insertions(+), 68 deletions(-) create mode 100644 docs/templates/friction-log-archive.md.tmpl create mode 100644 docs/templates/friction-log.md.tmpl create mode 100644 docs/templates/handoff-history.md.tmpl create mode 100644 docs/templates/handoff.md.tmpl create mode 100644 scripts/lib/kitconfig.py create mode 100644 scripts/tests/test_kitconfig.py diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c90ac2b..bb6c830 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -15,6 +15,18 @@ jobs: - name: Install test dependencies run: python -m pip install pytest pyyaml - name: Check shell syntax - run: bash -n init.sh scripts/dev_session.sh scripts/reconcile_sessions.sh scripts/lib/repo_root.sh scripts/hooks/pre-push + run: | + bash -n scripts/dev_session.sh scripts/reconcile_sessions.sh scripts/lib/repo_root.sh scripts/hooks/pre-push + sh -n init.sh + - name: init.sh must be executable + # The documented first command is `./init.sh`; a non-executable mode makes + # it fail for every adopter at step one. + run: test -x init.sh - name: Run state and portability tests run: python -m pytest scripts/lib/state_paths/tests scripts/tests -q + - 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. + run: | + python -m pip uninstall -y pyyaml + python -m pytest scripts/tests/test_kitconfig.py -q diff --git a/README.md b/README.md index da57aaf..079dba1 100644 --- a/README.md +++ b/README.md @@ -84,6 +84,11 @@ patterns to rules). ## Quickstart +**Prerequisites.** `init.sh` itself needs only POSIX `sh`, `awk`, and `git`. The +engines need [`uv`](https://docs.astral.sh/uv/) (they're PEP-723 single-file +scripts), `git`, and — for `pr-watch` / `parallel` — the GitHub CLI `gh`, +authenticated. No PyYAML: the config reader is stdlib-only. + ```sh # Click "Use this template" on GitHub and clone the result — or, into an # existing repo, copy the kit's contents in from the root: @@ -94,9 +99,34 @@ cp -r /path/to/agentic-dev-kit/. . # -> start your agent session and invoke session-start ``` +`init.sh` stamps your answers into `config/dev-model.yaml`, renders the narrative +docs from `docs/templates/`, installs the pre-push hook, and adds the state +sandbox to `.gitignore`. It never overwrites a narrative doc that is already in +use — only one still carrying the shipped `devkit-template: unrendered` marker. + Ten minutes, start to finish. For a full worked example of a first session — from adoption through `wrap-up` — see **[`docs/getting-started.md`](docs/getting-started.md)**. +## Upgrading an already-adopted repo + +**Pull the new kit files, then re-run `./init.sh`.** That is the supported upgrade +path, and it is safe to run any number of times: + +- **Config** — `init.sh` migrates an older schema forward *in place*, only ever + adding missing keys. Your existing values are never guessed over. `kit.version` + records which generation you're on. +- **`paths.engines`** — probed from where your engines actually are, so a repo that + vendored them under `scripts/devkit/` is migrated to that path rather than a + wrong default. +- **Narrative docs** — a handoff or friction log you're actually using is left + byte-identical; only an unrendered skeleton is (re-)rendered. +- **Hooks** — reinstalled as a shim that execs the engine, so a hook stays current + with the engine rather than going stale as a copy. +- **Engines** — replace the files. Engines are **kit-owned**: everything + project-specific (review-bot markers, informational checks, CI policy, paths) + 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. + ### Agent runtime adapters The workflow definitions under `docs/agentic-dev-kit/workflows/` are shared. The diff --git a/config/dev-model.yaml b/config/dev-model.yaml index b115313..b134614 100644 --- a/config/dev-model.yaml +++ b/config/dev-model.yaml @@ -10,6 +10,15 @@ # or edit this file directly. # ───────────────────────────────────────────────────────────────────────────── +kit: + # Which generation of the kit's config schema this repo is on. `init.sh` stamps + # it and migrates an older config forward in place, so re-running `./init.sh` + # after pulling a kit update is the supported upgrade path. Never edit by hand: + # lowering it re-runs migrations that are already applied. + # 1 — original single-runtime schema (no `runtime:`, no `models.tiers`) + # 2 — runtime adapters + capability tiers + engine-facing review/CI config + version: 2 + project: # Human-readable project name, used in skill prompts and doc headers. name: my-project @@ -73,6 +82,44 @@ review: fallback_commands: claude: "/code-review" codex: "/review" + # Everything below is read by scripts/pr_watch.py. It used to live as literals + # inside that engine, which meant adopting the kit required *editing the + # engine* — and an edited engine can never be replaced by a kit update. Keeping + # it here is what makes engines kit-owned and upgradable (Principle #10). + # + # Comment bodies matching any of these are auto-noise, not findings to act on. + # Matched case-insensitively as substrings. Keep the list tight — over-filtering + # hides a real review. + noise_markers: + - "" + - "actionable comments posted: 0" + - "review skipped" + - "" + # Bodies signalling the reviewer could not run. Deliberately NOT noise: these + # surface and block `done`, because a blocked bot is an action signal (run the + # configured fallback), never a silent review waiver. + unavailable_markers: + - "bugbot needs on-demand usage enabled" + - "review limit reached" + - "rate limited by coderabbit" + - "couldn't start this review" + - "review skipped" + - "no review credits" + # Status checks that are advisory only and must never block `done`. A review + # bot's check can sit PENDING forever after a trivial follow-up commit; its real + # findings arrive as comments (which DO block). Matched case-insensitively + # against the check name. + informational_checks: [coderabbit] + # Whether a PR must have at least one real (non-informational) CI check before + # pr_watch will report `done`. True is the safe default: it stops an autonomous + # merge on a PR whose CI never ran. Set false for a repo with no CI at all — + # otherwise pr-watch can never converge and `dev_session.sh merge` always + # refuses. With false, `done` additionally requires current-head review evidence + # (it already does) — that receipt becomes the only quality gate, so set this + # deliberately. + require_ci: true notify: backend: slack # slack | none — where skills DM the operator @@ -102,3 +149,10 @@ state: # see scripts/lib/state_paths/. Those are kit constants, not adopter config; listed # here only so the convention is discoverable. `dirname` is the top-level state dir. dirname: state + # The unsandboxed-write guard skips a legitimate scheduled/CI writer, detected by + # env var. state_paths stays import-free (it must work from a git hook), so this is + # an ENV contract, not a value it reads from here — documented here for + # discoverability only: + # DEVKIT_CI_ENV_VARS comma-separated var names whose presence means "cron/CI". + # Default: JOB_NAME,CI,GITHUB_ACTIONS,GITLAB_CI,BUILDKITE + # Set it in your runner if your scheduler exports something else. diff --git a/docs/friction-log-archive.md b/docs/friction-log-archive.md index 64f5915..fa37274 100644 --- a/docs/friction-log-archive.md +++ b/docs/friction-log-archive.md @@ -1,3 +1,5 @@ + + # Friction Log Archive Graduated friction entries live here after they have been routed to the tracker or diff --git a/docs/friction-log.md b/docs/friction-log.md index abc44c4..0aab1d9 100644 --- a/docs/friction-log.md +++ b/docs/friction-log.md @@ -1,3 +1,5 @@ + + # Friction Log > **Lean inbox (Principle #2 — the friction flywheel).** Friction surfaced during real diff --git a/docs/handoff-history.md b/docs/handoff-history.md index 620e059..9606cbd 100644 --- a/docs/handoff-history.md +++ b/docs/handoff-history.md @@ -1,3 +1,5 @@ + + # Handoff History Archived session narratives from the living handoff. Keep active direction and the diff --git a/docs/handoff.md b/docs/handoff.md index 6cb8159..3ee22df 100644 --- a/docs/handoff.md +++ b/docs/handoff.md @@ -1,3 +1,5 @@ + + # my-project — Living Plan (Handoff) > **Forward-looking handoff (Principle #1).** Read this at the start of every session diff --git a/docs/templates/friction-log-archive.md.tmpl b/docs/templates/friction-log-archive.md.tmpl new file mode 100644 index 0000000..64f5915 --- /dev/null +++ b/docs/templates/friction-log-archive.md.tmpl @@ -0,0 +1,4 @@ +# Friction Log Archive + +Graduated friction entries live here after they have been routed to the tracker or +promoted into a repeated-pattern rule. diff --git a/docs/templates/friction-log.md.tmpl b/docs/templates/friction-log.md.tmpl new file mode 100644 index 0000000..97bf96c --- /dev/null +++ b/docs/templates/friction-log.md.tmpl @@ -0,0 +1,20 @@ +# Friction Log + +> **Lean inbox (Principle #2 — the friction flywheel).** Friction surfaced during real +> use — a bug, an awkward workflow, a recurring annoyance — recorded the moment it's +> fresh, at session end. A periodic triage (`triage-friction-log`) reads new entries and +> routes each one: single incidents go **down** to the tracker; a genuine, multi-occurrence +> **pattern** graduates **up** into a rule or skill change. Route down by default, up only +> on repetition — so the flywheel self-regulates instead of ratcheting every week. +> +> Each entry: the observed issue, the date surfaced, a rough severity (**H**igh / **M**edium +> / **L**ow), and a proposed fix or next step. Link related PRs, commits, or tracker items +> when available. Graduated entries are swept to +> [`friction-log-archive.md`](friction-log-archive.md) so this file stays just the current +> inbox plus the most-recent graduation marker. +> +> Tracker board: {{TRACKER_URL}} + +## {{DATE}} — inbox + +- ** (severity: ).** diff --git a/docs/templates/handoff-history.md.tmpl b/docs/templates/handoff-history.md.tmpl new file mode 100644 index 0000000..620e059 --- /dev/null +++ b/docs/templates/handoff-history.md.tmpl @@ -0,0 +1,6 @@ +# Handoff History + +Archived session narratives from the living handoff. Keep active direction and the +next step in `handoff.md`; this file is append-only history. + +## Session log diff --git a/docs/templates/handoff.md.tmpl b/docs/templates/handoff.md.tmpl new file mode 100644 index 0000000..eecbe4b --- /dev/null +++ b/docs/templates/handoff.md.tmpl @@ -0,0 +1,26 @@ +# {{PROJECT_NAME}} — Living Plan (Handoff) + +> **Forward-looking handoff (Principle #1).** Read this at the start of every session +> (`session-start`); update it at the end (`wrap-up`). This file — not an agent's +> memory, not a scratch note — is the single source of truth for what's done, in +> progress, and next. +> +> Older session blocks graduate to [`handoff-history.md`](handoff-history.md) once this +> file crosses its line budget (a warn-only tripwire — `{{ENGINE_DIR}}/check_doc_budget.py`). +> Session-scoped scratch plans are exactly that: scratch. This is the handoff. + +Last updated: {{DATE}} — + +## Latest session — {{DATE}} + +**Theme —** + +- +- +- + +▶ Next: + +______________________________________________________________________ + +> Older session entries live in [`handoff-history.md`](handoff-history.md). diff --git a/init.sh b/init.sh index 685d7ae..b0d3e43 100644 --- a/init.sh +++ b/init.sh @@ -186,11 +186,30 @@ append_to_section() { # documented as idempotent; silently leaving an old config without these keys # would make the new runtime-aware launchers appear bootstrapped while falling # back to the wrong behavior. +# Where this repo's engines actually live. Probing beats defaulting: an adopter +# who vendored the kit under scripts/devkit/ (the documented namespaced layout) +# would otherwise be migrated to `engines: scripts`, and every workflow's +# `/…` reference would resolve to a path with no engines in it — +# silently, since nothing validates the value. Falls back to `scripts` only when +# no engine is found anywhere. +detect_engines_dir() { + for candidate in scripts scripts/devkit scripts/kit scripts/agentic-dev-kit tools/devkit bin/devkit; do + for probe in check_doc_budget.py pr_watch.py dev_session.sh; do + if [ -f "$candidate/$probe" ]; then + printf '%s\n' "$candidate" + return 0 + fi + done + done + printf 'scripts\n' +} + migrate_runtime_schema() { if ! grep -q '^ engines:' "$CONFIG_FILE"; then - append_to_section "paths:" ' # Directory containing the deterministic kit engines. - engines: scripts' - echo "added paths.engines to config/dev-model.yaml" + detected_engines="$(detect_engines_dir)" + append_to_section "paths:" " # Directory containing the deterministic kit engines. + engines: $detected_engines" + echo "added paths.engines: $detected_engines to config/dev-model.yaml" fi if ! grep -q '^runtime:' "$CONFIG_FILE"; then @@ -236,6 +255,139 @@ migrate_runtime_schema() { fi } +# Schema additions introduced after the v2 template release. Same idempotent +# shape as migrate_runtime_schema: only ever ADD a missing block, never guess +# over an existing value — re-running init.sh is the supported upgrade path, so +# a migration must be safe to apply to a config that already has it. +migrate_kit_schema() { + if ! grep -q '^kit:' "$CONFIG_FILE"; then + # Prepend, so the version stamp reads first. There is no earlier section to + # anchor before, so insert_before_section targets the first one that exists. + insert_before_section "project:" 'kit: + # Which generation of the kit'"'"'s config schema this repo is on. `init.sh` stamps + # it and migrates an older config forward in place, so re-running `./init.sh` + # after pulling a kit update is the supported upgrade path. + version: 2 +' + echo "stamped kit.version=2 in config/dev-model.yaml" + fi + + if ! grep -q '^ noise_markers:' "$CONFIG_FILE"; then + append_to_section "review:" ' # Read by pr_watch.py. These used to be literals inside the engine, which meant + # adopting required EDITING the engine — and an edited engine can never be + # replaced by a kit update (Principle #10). + noise_markers: + - "" + - "actionable comments posted: 0" + - "review skipped" + - "" + unavailable_markers: + - "bugbot needs on-demand usage enabled" + - "review limit reached" + - "rate limited by coderabbit" + - "couldn'"'"'t start this review" + - "review skipped" + - "no review credits" + informational_checks: [coderabbit] + # False only for a repo with NO CI at all — otherwise pr-watch never converges. + require_ci: true' + echo "added review marker/CI config to config/dev-model.yaml" + fi +} + +# ── narrative-doc templates ────────────────────────────────────────────── +# The kit SHIPS docs/handoff.md and docs/friction-log.md, so a `cp -r` or a +# "Use this template" clone always lands them before init.sh runs — which used +# to make the "seed only if absent" guard permanently false, and every adopter +# started with an unrendered skeleton. The marker below is what distinguishes +# "the pristine file the kit shipped" from "a handoff someone is actually +# using": a rendered/edited file has no marker and is never touched. +TEMPLATE_MARKER="devkit-template: unrendered" + +# _render