Fix the adoption path and make engines upgradable (kit schema v2)#8
Conversation
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 `<tracker.url — stamped by init.sh>` 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 <engine-dir>/… 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).
|
Warning Review limit reached
Next review available in: 38 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (7)
📝 WalkthroughWalkthroughChangesKit bootstrap and portability
Sequence Diagram(s)sequenceDiagram
participant Developer
participant init.sh
participant dev-model.yaml
participant Templates
participant NarrativeDocs
participant GitHooks
Developer->>init.sh: run ./init.sh
init.sh->>dev-model.yaml: migrate schema and read paths
init.sh->>Templates: render configured templates
Templates->>NarrativeDocs: seed missing or unrendered docs
init.sh->>GitHooks: install pre-push engine shim
init.sh-->>Developer: report kit schema v2 completion
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
The CI guard added in this PR caught its own regression: a sandbox adoption test rewrote the working-tree file at the default umask, and the subsequent `git add -A` re-staged mode 644 over the earlier update-index --chmod=+x. Exactly the failure the check exists to catch.
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (1)
init.sh (1)
523-532: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueCompletion banner hardcodes "kit schema v2".
Once
kit.versionincrements past 2, this literal string needs a manual update. Low priority since it's cosmetic and the schema is brand new.🤖 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 `@init.sh` around lines 523 - 532, Update the completion banner near the final echo statements to derive the displayed kit schema version from the configured kit.version value instead of hardcoding “v2”. Keep the existing completion messaging unchanged while ensuring future schema increments are reflected automatically.
🤖 Prompt for all review comments with 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.
Inline comments:
In `@config/dev-model.yaml`:
- Around line 93-109: Remove the duplicate "review skipped" marker from
noise_markers while retaining it in unavailable_markers in config/dev-model.yaml
lines 93-109 and the corresponding migrate_kit_schema heredoc in init.sh lines
276-295. Keep all other markers unchanged so the marker is treated only as an
unavailable-review signal in both the shipped configuration and migration
template.
In `@docs/templates/friction-log.md.tmpl`:
- Around line 12-14: Update the narrative links in
docs/templates/friction-log.md.tmpl lines 12-14 and
docs/templates/handoff.md.tmpl lines 8-10 to use the configured
friction_log_archive and handoff_history paths, or compute links relative to
each rendered document; preserve the existing prose while ensuring customized
installations produce valid internal links.
In `@scripts/lib/kitconfig.py`:
- Around line 144-160: Update the dash-item parsing branch in the YAML list
handling to treat an item as a mapping only when the text before the colon
matches the supported key/identifier format; otherwise coerce and append the
entire item as a scalar. Preserve the existing quoted/bracketed exclusions,
mapping stack behavior, and skip/avoid-guessing contract.
In `@scripts/tests/test_kitconfig.py`:
- Around line 122-129: Escape the literal dot in the pytest.raises match pattern
within test_get_is_fail_loud_without_a_default, so the KeyError assertion
matches the exact “a.missing” key while satisfying Ruff RUF043.
---
Nitpick comments:
In `@init.sh`:
- Around line 523-532: Update the completion banner near the final echo
statements to derive the displayed kit schema version from the configured
kit.version value instead of hardcoding “v2”. Keep the existing completion
messaging unchanged while ensuring future schema increments are reflected
automatically.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 81bd914b-b581-4677-9620-43e359bc6acb
📒 Files selected for processing (14)
.github/workflows/test.ymlREADME.mdconfig/dev-model.yamldocs/friction-log-archive.mddocs/friction-log.mddocs/handoff-history.mddocs/handoff.mddocs/templates/friction-log-archive.md.tmpldocs/templates/friction-log.md.tmpldocs/templates/handoff-history.md.tmpldocs/templates/handoff.md.tmplinit.shscripts/lib/kitconfig.pyscripts/tests/test_kitconfig.py
git rev-parse --git-path hooks ignores core.hooksPath, so on a repo that sets it (pre-commit, several monorepo layouts) the shim landed in a directory git never reads — an inert hook, the exact failure this change removes. Also make the template-marker probe a fixed-string grep. Found reviewing this PR's own diff.
Differential-testing the new reader against PyYAML across 20 constructs found
a silent-corruption bug. A block list at the SAME indent as its key —
doc_budgets:
- path: docs/handoff.md
budget: 400
which is valid YAML and what most formatters emit — parsed to
{'doc_budgets': None, 'budget': 400}: the list was never created, its dashes
were dropped, and its nested keys leaked into the PARENT mapping. No error.
The single 'indent <= frame_indent' pop rule cannot express this, because a
dash at exactly the list's indent is the next sibling item, while a KEY at that
indent closes the list. Frames now carry a kind and pop under a rule that
distinguishes the two.
The kit's own config uses the indented style, so nothing shipped was
mis-parsed — but any adopter hand-editing their config, or any formatter run,
would have silently corrupted it. Found by the independent review pass on this
PR; regression tests cover both styles plus the nested-parent case.
101 tests pass; all three real configs still match PyYAML byte-for-byte.
Four findings from the independent review pass, all valid: 1. (major) A dash item containing ':' anywhere was treated as a mapping, so `- https://example.com/path` parsed to {"https": "//example.com/path"} instead of the string. YAML requires the colon be followed by whitespace or end-of-line; the guard now matches that rather than testing for a bare ':'. The module's own docstring promises unsupported constructs are skipped, not guessed at — this path guessed. 2. (major) "review skipped" appeared in BOTH noise_markers and unavailable_markers. It was dead weight in noise_markers (is_noise() checks unavailability first and returns False) but read as a precedence ambiguity. Removed from noise_markers in the shipped config and in migrate_kit_schema's heredoc, with a test asserting the two lists stay disjoint. 3. (minor) The narrative templates hard-coded sibling filenames while paths.* are configurable, so an adopter who repointed paths.handoff_history shipped a doc whose "older entries live in …" link 404s. Links now render from config: a bare filename when the docs are siblings, the repo-relative path otherwise. 4. (minor) re.escape on a pytest match= pattern (RUF043). Also corrected init.sh's header and usage() text, which still described the old seed-only-if-absent behavior and omitted the migration, hook install, and the fact that re-running is the upgrade path. 101 tests pass; all three real configs still match PyYAML byte-for-byte.
|
All four findings addressed — thanks, two of them were real. 1. Dash-item colon heuristic (major) — fixed. Correct diagnosis. 2. Worth recording what this PR's own review surfaced: your real review body contains the string "Review skipped" in a collapsed section, so 3. Template links vs configured paths (minor) — fixed. Real gap, and the same Principle #10 point this PR is about. Links now render from config: a bare filename when the docs are siblings (the common case), the repo-relative path otherwise. Verified by repointing 4. RUF043 (minor) — fixed. Also self-caught while reviewing: 106 tests pass; all three real adopter configs still match PyYAML byte-for-byte. |
…ks, memory budget) (#13) * feat(claude): add slash-command frontmatter for discoverability Every .claude/commands/*.md lacked YAML frontmatter, so Claude Code surfaced each command's raw first line as its description (a workflow-doc pointer, or a v0-status blockquote for the two engine-blocked skills) instead of a real one. Add `description:` (reusing the wording already written in the matching .agents/skills/*/SKILL.md where one exists) and `argument-hint:` where a command takes a structured argument. No command body text changed. * docs(claude): correct the two engine-blocked skill banners triage-friction-log.md and post-merge-systemize.md both opened with a "v0 kit status — engine not yet vendored" banner, which reads as unwritten work. It isn't: working deterministic engines for both already exist in the kit maintainer's own toolkit repo, blocked on a tracker-adapter abstraction so they can target any configured tracker.backend instead of one hardcoded client. Rewrite both banners to say that plainly and link the tracking issues (agentic-dev-kit#6 for triage, #7 for systemize). Also adds frontmatter to both (folded into this commit since it touches the same banner block). * feat(hooks): port pr_followup_hook.py as a PostToolUse mechanism Port the PR follow-through nag from a reference implementation (cs-toolkit's scripts/hooks/pr_followup_hook.py) so the kit's Principle #5 policy ("watch the PR through to green") is a PostToolUse hook instead of prose asking the agent to run /pr-watch unasked (Principle #8: "a rule that lives only in a doc is a wish"). Generalized for the kit: reads review.bots, review.fallback_commands.claude, and paths.engines from config/dev-model.yaml via scripts/lib/kitconfig.py instead of naming a review bot as a literal. Falls back to generic wording if config can't be read, rather than skipping the reminder. No-ops when JOB_NAME is set (cron/CI) and always exits 0. Tests cover: triggers on `gh pr create` / `gh pr ready`, does not trigger on `gh pr view` or unrelated commands, no-ops under JOB_NAME, exits 0 on malformed/unexpected stdin shapes, and that the reminder text is genuinely config-sourced rather than hardcoded. * feat(scripts): port check_memory_budget.py, the auto-memory tripwire Port the doc-budget tripwire applied to Claude Code's own per-project auto-memory index (MEMORY.md) from a reference implementation (cs-toolkit's scripts/check_memory_budget.py). Mirrors check_doc_budget.py's CLI exactly (--quiet/--strict/--json, same exit-code contract) and reuses scripts/lib/kitconfig.py for portable repo-root discovery (walking up for .git, so the derived repo-slug stays correct when the kit is vendored under a nested paths.engines dir) instead of the reference's fixed-depth `parent.parent`. Deliberately NOT config/dev-model.yaml `doc_budgets`-driven: that list is iterated unconditionally by check_doc_budget.py, which requires every entry's path to resolve to an existing repo-relative file with archive/remedy fields. The memory index lives outside the repo at a machine- and repo-slug-specific path (not portable to commit, not swept to an in-repo archive), so adding it there would make every check_doc_budget.py run — including the SessionStart hook it now shares — fail loudly wherever no memory has been saved yet. The byte/line-length budgets stay documented platform constants (overridable via --max-bytes / --max-line-chars), same as check_doc_budget.py keeps its own tuning out of adopter config. config/dev-model.yaml is untouched in this change. * feat(claude): ship .claude/settings.json — wire the tripwires as hooks The doc-budget tripwire (check_doc_budget.py) already advertised itself as "safe to run from a SessionStart hook" but nothing wired it, so it only fired if someone remembered to run it during /wrap-up — memory, not mechanism (Principle #8). Wire it, plus the new check_memory_budget.py, into a SessionStart hook (matcher "startup"); wire the new pr_followup_hook.py into a PostToolUse hook (matcher "Bash", pre-filtered with `if: "Bash(gh pr *)"`). Both SessionStart hooks are guarded `[ -z "$JOB_NAME" ] && … || true` so a cron/CI session never runs them and the hook always exits 0. Also ships a `permissions.allow` list for the kit's own read-only forge calls (gh pr view/list/checks, gh run view, uv run scripts/pr_watch.py) so a lane doesn't stall on a permission prompt for calls the kit itself makes routinely. Hardcoded, documented compromise: the hook commands reference scripts/check_doc_budget.py, scripts/check_memory_budget.py, and scripts/hooks/pr_followup_hook.py directly rather than resolving paths.engines dynamically. A settings.json hook command is a static string; resolving paths.engines first would require invoking an interpreter to read config before knowing which interpreter/path to invoke, which is circular for a bare shell command. An adopter who vendors the kit's engines under a different paths.engines value must edit these three command strings by hand (one line each) after adopting. permissions.allow's `uv run scripts/pr_watch.py:*` entry carries the same caveat. Verified against the official Claude Code docs (code.claude.com/docs/en/ hooks.md and hooks-guide.md), not assumed from the reference example alone: SessionStart matcher values (startup/resume/clear/compact/fork), PostToolUse matcher "Bash", the {type, command, timeout-in-seconds} hook-entry shape, the `if` field's permission-rule syntax and that it applies to PostToolUse (not just PreToolUse), $CLAUDE_PROJECT_DIR as a real hook env var, the `hookSpecificOutput.additionalContext` shape for PostToolUse specifically (confirmed via a direct fetch of hooks.md after an initial secondary source claimed, incorrectly, that additionalContext was UserPromptSubmit-only), and the permissions.allow `Bash(cmd:*)` colon-suffix form as equivalent to a trailing `Bash(cmd *)`. * feat(build): add thin Makefile — install-hooks, test `install-hooks` does not reimplement the pre-push hook shim installer: init.sh's own install_hooks() (git-worktree-aware, honors core.hooksPath, regenerates a shim rather than copying the hook body or symlinking it) stays the single source of truth. Running the whole of `./init.sh` here would also re-trigger its interactive project/tracker prompts and its docs/ narrative-template seeding step — confirmed by hand: on a repo whose narrative docs still carry the `devkit-template: unrendered` marker (this kit's own repo, before an adopter has claimed them), a bare `./init.sh` run rewrites docs/handoff.md and friends as a side effect, which is out of bounds for a target named "install-hooks" and, in this repo specifically, out of this lane's own touch scope. So the target extracts and runs just the install_hooks() function body straight out of init.sh via `sed` + `eval` — it always reflects init.sh's current logic, never a stale/diverged copy — after resolving paths.engines the same way init.sh does, via scripts/lib/kitconfig.py. `test` runs the lane contract's own local gate: the state_paths + scripts pytest suites. --------- Co-authored-by: Topi Jarvinen <topi.jarvinen@gmail..com>
* feat: kit_doctor + /upgrade — make an already-adopted install diagnosable Phase 2. Re-running ./init.sh (landed in #8) already handles the config half of an upgrade. This is the engine half, which was the part nothing could see. The problem, measured rather than assumed. Surveying the real installs turned up four distinct shapes, none of them versioned at the engine level: · an ancestor with no config surface at all · a v1-schema repo with 2 of 6 engines and a paths.engines value pointing at a directory containing no engine — so every workflow's <engine-dir>/… reference resolved to nothing, silently, because nothing validates that value · a correctly namespaced v2 repo whose pr_watch.py was 3 features behind · the template itself Nothing could tell an older engine from a hand-patched one, which is what makes "just copy the new files in" unsafe. scripts/kit_doctor.py — read-only drift report against a released kit-manifest.json (path -> sha256). Per kit-owned file: unchanged / differs / missing / unknown-version. Plus the four installation-level checks nothing else performs: schema generation, whether paths.engines actually holds engines, whether the pre-push hook is installed rather than merely shipped, and whether the narrative docs were rendered. Two deliberate design calls: · `differs` does NOT claim a cause. A hash mismatch cannot distinguish "older version" from "hand-edited", and labelling an old file "locally modified" sends someone hunting for edits they never made — this was caught by running the tool against a real adopter, which reported 4 files as locally-modified when they were almost certainly just old. It now narrows by schema version and leaves the call to the operator. · paths.engines indirection is handled by remapping the manifest's kit-layout prefix, so a repo with engines under scripts/devkit/ compares correctly against an unmodified manifest. Adopter-owned paths are never compared — they are supposed to differ, and reporting them would bury the signal. That list is explicit (not inferred) so an installer can consume the same boundary. /upgrade (.claude/commands/upgrade.md) — drives the sequence: detect which of the four shapes this repo is, migrate config via init.sh, then refresh per file keyed on kit_doctor's states. Routes a config-less repo to /adopt instead of guessing a config into existence. Names the case worth routing UPSTREAM: a local edit that is genuinely ahead of the kit should become a kit PR, not be overwritten. Also closes #5: check_doc_budget.py and archive_plan_sessions.py now read config through the stdlib kitconfig instead of PyYAML, so every shipped engine is dependency-free — including the SessionStart hook path added in #13. This is the same invariant kit_doctor asserts: an engine an adopter must edit (or install a dependency for) can never be safely replaced. CI gates the manifest against the tree, since a stale manifest silently degrades every adopter's report to unknown-version. 184 tests pass. Verified against both real adopter repos read-only: brain's missing-engine breakage and OpenKitchen's 4 drifted files are both correctly diagnosed, with the engines-dir failure called out explicitly. Closes #5. * fix(kit_doctor): honor core.hooksPath when detecting the installed hook Found by the fallback review pass on this PR. `.git/hooks` is not the only place git reads hooks from — core.hooksPath overrides it, and pre-commit plus several monorepo layouts set it. Checking only `.git/hooks` reports a correctly-installed hook as missing, so the report would tell an adopter to re-run an install that had already worked. This is the same bug class fixed in init.sh for the WRITE side earlier in this stack, reintroduced on the READ side. Read via a plain .git/config scan rather than shelling out to git, keeping kit_doctor subprocess-free. Also strip a trailing slash from paths.engines before remapping. --------- Co-authored-by: Topi Jarvinen <topi.jarvinen@gmail..com>
Found by the fallback review pass on this PR. getting-started.md still described init.sh as seeding the narrative docs 'only if they don't already exist' — the rule #8 replaced, and the one that could never fire (the kit ships those files, so a copy-in always landed them first). Swept the other docs/handoff.md references while I was there and deliberately left them: README's what's-inside table, parallel-dev.md, adopt.md, and the two flywheel skills all describe what an ADOPTER gets, where the plain filenames are correct. Only this repo's own config uses kit-*.md.
* chore: wrap up — the kit now practises Principle #1 on itself Session wrap-up, and the resolution of the dogfooding gap the assessment found: this repo's own handoff and friction log were unrendered skeletons, so the kit was not doing the one thing Principle #1 requires. It could not simply be fixed by filling them in. docs/handoff.md and docs/friction-log.md are the SKELETONS shipped to adopters — writing real session blocks there would ship this repo's narrative into every adopting repo and destroy the 'devkit-template: unrendered' marker the seeding logic depends on. So the kit's own files are docs/kit-*.md, with the indirection explained at the config keys. An adopter's config keeps the plain names; only the template repo needs this. - config paths + doc_budgets repointed to docs/kit-*.md - docs/kit-handoff.md: first real session block (six PRs, 83 -> 196 tests, what was decided, what was learned, and the Phase 3 next step) - docs/kit-friction-log.md: the friction that is NOT yet issue-shaped, plus a note that most of this session's friction routed straight to GitHub Issues — which is Principle #2's prescribed routing, not a neglected inbox - kit_doctor: kit-*.md added to ADOPTER_OWNED so they are never compared - the marker test split in two: shipped skeletons asserted by LITERAL filename (reading paths.handoff would now check the kit's live plan instead), plus a new test asserting the kit's own plan is real — no marker, no placeholder dates Filed while wrapping up: #18 (the cp -r quickstart can't tell kit-owned from adopter-owned; the manifest already encodes the boundary, nothing consumes it for installing — landing that would let kit-*.md go back to the plain names) and #19 (a receipt can be recorded while the primary bot is merely queued, which is how #16's findings landed post-merge). 196 tests pass. check_doc_budget and kit_doctor both clean: the kit's own narrative docs now report 'in use' rather than 'still an unrendered template'. * docs: correct getting-started's stale seeding description Found by the fallback review pass on this PR. getting-started.md still described init.sh as seeding the narrative docs 'only if they don't already exist' — the rule #8 replaced, and the one that could never fire (the kit ships those files, so a copy-in always landed them first). Swept the other docs/handoff.md references while I was there and deliberately left them: README's what's-inside table, parallel-dev.md, adopt.md, and the two flywheel skills all describe what an ADOPTER gets, where the plain filenames are correct. Only this repo's own config uses kit-*.md. --------- Co-authored-by: Topi Jarvinen <topi.jarvinen@gmail..com>
Phase 0 of the kit refactor — the shared primitives the three follow-on lanes all need (
config/dev-model.yaml,init.sh), landed in one PR so those lanes have disjoint footprints and can run in parallel.What was broken
./init.sh100644— the documented first command failed for every adopterdocs/handoff.md+docs/friction-log.md, so acp -ror template-clone always landed them first and the "seed only if absent" guard was permanently false. Adopters got amy-projectheader and a<tracker.url — stamped by init.sh>line thatinit.shnever stamped — it didn't even prompt for the URL.scripts/hooks/pre-pushmake install-hookstarget that doesn't exist — leaving the kit's own Principle #8 exemplar inert everywhere.migrate_runtime_schema()already existed but only runs frominit.sh— which couldn't run. That is whybrainis still on the v1 schema.What this does
Adoption — mode fixed (pinned by a CI check); docs render from
docs/templates/*.tmplkeyed on adevkit-template: unrenderedmarker that tells "the file the kit shipped" from "a handoff someone is using"; all four narrative docs seeded;tracker.urlprompted.Upgrade path —
kit.versionstamps the schema generation; re-running./init.shis now the supported upgrade and migrates in place without guessing over existing values;paths.enginesis probed rather than defaulted (a repo with engines underscripts/devkit/was being migrated toengines: scripts, silently pointing every<engine-dir>/…reference at an empty directory); hooks installed as a shim that execs the engine, so they don't go stale.Engines become kit-owned —
scripts/lib/kitconfig.py, a stdlib-only config reader, byte-equal to PyYAML on the shipped config and on two real adopter configs. This is the prerequisite for movingpr_watch.py's review-bot markers out of the engine: as long as adopting requires editing an engine, no engine can ever be replaced by an update. Closes the direction of #5.Verification
# acme-widgets — Living Plan, tracker URL substituted, engine path substitutedbrain's real v1 config +scripts/devkit/layout: migrates to v2,paths.engines: scripts/devkitdetected, its 223-line handoff untouched, nothing lostNext (this stack)
Three disjoint lanes off this base: L1 engines (#3
origin/mainhardcode, no-CI convergence, markers→config,JOB_NAME), L2 wiring (command frontmatter,settings.json,pr_followup_hook), L3 docs. Thenkit_doctor+/upgrade, then the cs-toolkit back-port. Engine extraction tracked in #6 / #7.Summary by CodeRabbit
New Features
Documentation
Bug Fixes