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..e2d159c 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,43 @@ 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" + - "" + # 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 +148,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..96269b5 --- /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_ARCHIVE}}`]({{FRICTION_ARCHIVE}}) 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..4832453 --- /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}}`; 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..830bd71 --- /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}}`]({{HANDOFF_HISTORY}}) 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}}`]({{HANDOFF_HISTORY}}). diff --git a/init.sh b/init.sh old mode 100644 new mode 100755 index 685d7ae..7aee470 --- a/init.sh +++ b/init.sh @@ -1,10 +1,12 @@ #!/bin/sh # init.sh — bootstrap for the agentic-dev-kit. # -# Run once from the root of the repo you copied this kit into. Idempotent: -# re-running re-prompts (showing the current value as the default) and only -# ever adds missing lines — it never clobbers docs/handoff.md, -# docs/friction-log.md, or duplicates .gitignore entries. +# Run from the root of the repo you copied this kit into — at adoption, and +# again after pulling a kit update (that is the supported upgrade path). +# Idempotent: re-running re-prompts (showing the current value as the default), +# migrates an older config schema forward without guessing over existing +# values, and never clobbers a narrative doc that is already in use — only one +# still carrying the shipped `devkit-template: unrendered` marker. # # Requires: sh, awk, grep, mv. No non-stdlib dependencies. @@ -23,11 +25,15 @@ Bootstraps the agentic-dev-kit in the current repo: review.bots — each showing the current value in config/dev-model.yaml as the default. Press Enter to keep the default. 2. Stamps the answers into config/dev-model.yaml in place. - 3. Seeds docs/handoff.md and docs/friction-log.md from the kit's - skeletons, but ONLY if those files don't already exist. - 4. Appends the kit's state-sandbox paths to .gitignore if they're + 3. Migrates an older config schema forward in place (kit.version) and + stamps the current generation. + 4. Renders the four narrative docs from docs/templates/ — but only when a + target is missing or still carries the unrendered marker, so a handoff + you are actually using is left byte-identical. + 5. Appends the kit's state-sandbox paths to .gitignore if they're missing (never duplicates a line on re-run). - 5. Prints the runtime-specific session-start invocation. + 6. Installs the pre-push hook as a shim (honoring core.hooksPath). + 7. Prints the runtime-specific session-start invocation. Safe to re-run at any time. Run it from the repo root (the directory that contains config/dev-model.yaml). @@ -186,11 +192,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 +261,150 @@ 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" + - "" + 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