From fedcfde59c0d3b7f5fbdb7364cb8474146a57d59 Mon Sep 17 00:00:00 2001 From: Topi Jarvinen Date: Sat, 25 Jul 2026 01:15:51 +0300 Subject: [PATCH 1/6] 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. --- .claude/commands/adopt.md | 4 ++++ .claude/commands/parallel.md | 5 +++++ .claude/commands/pr-watch.md | 5 +++++ .claude/commands/session-start.md | 4 ++++ .claude/commands/wrap-up.md | 4 ++++ 5 files changed, 22 insertions(+) diff --git a/.claude/commands/adopt.md b/.claude/commands/adopt.md index 31bd92c..cf9c606 100644 --- a/.claude/commands/adopt.md +++ b/.claude/commands/adopt.md @@ -1,3 +1,7 @@ +--- +description: Selectively adopt agentic-dev-kit into an existing repo — inspect what's already present, propose an install plan the operator confirms, then install only the missing pieces without clobbering existing files. Use when integrating the kit into a mature repository rather than a fresh one. +--- + Selectively adopt agentic-dev-kit into an **existing** repo — install only the pieces the repo lacks, point the config at conventions it already has, and never clobber a file. The counterpart to `init.sh` (which assumes a fresh/near-empty repo). Runs diff --git a/.claude/commands/parallel.md b/.claude/commands/parallel.md index 57c1ee4..8b64847 100644 --- a/.claude/commands/parallel.md +++ b/.claude/commands/parallel.md @@ -1,3 +1,8 @@ +--- +description: Plan, launch, inspect, reconcile, and remove isolated development lanes backed by Git worktrees and per-lane state sandboxes. Use when work can be split into disjoint file footprints, when the user requests parallel development, or when inspecting existing agent lanes. +argument-hint: "[list|plan|new|pr-watch|merge|rm|path] [args...]" +--- + Read `docs/agentic-dev-kit/workflows/parallel.md` completely and follow it. Treat `$ARGUMENTS` as the requested parallel-development action and arguments. diff --git a/.claude/commands/pr-watch.md b/.claude/commands/pr-watch.md index c732d5a..24dfdad 100644 --- a/.claude/commands/pr-watch.md +++ b/.claude/commands/pr-watch.md @@ -1,3 +1,8 @@ +--- +description: Drive a pull request through the deterministic poll, fix, acknowledge, and re-poll loop until CI is green and review findings are resolved. Use after opening or updating a pull request, when asked to watch CI or reviews, or when a task must continue until its PR is green and clean. +argument-hint: "[pr-number]" +--- + Read `docs/agentic-dev-kit/workflows/pr-watch.md` completely and follow it. Treat `$ARGUMENTS` as the optional PR number or additional watch context. Resolve diff --git a/.claude/commands/session-start.md b/.claude/commands/session-start.md index faebf35..3449432 100644 --- a/.claude/commands/session-start.md +++ b/.claude/commands/session-start.md @@ -1,3 +1,7 @@ +--- +description: Build a concise start-of-session briefing from the living handoff, friction log, repository state, open pull requests, CI, and configured tracker. Use at the beginning of a development session, when resuming work after a gap, or when asked what the repository should do next. +--- + Read `docs/agentic-dev-kit/workflows/session-start.md` completely and follow it. Treat `$ARGUMENTS` as additional session context. Resolve all configured paths from diff --git a/.claude/commands/wrap-up.md b/.claude/commands/wrap-up.md index 0804091..ddd344f 100644 --- a/.claude/commands/wrap-up.md +++ b/.claude/commands/wrap-up.md @@ -1,3 +1,7 @@ +--- +description: Close a meaningful development session by updating the living handoff, capturing workflow friction, checking document budgets, and preserving one clear next step. Use when the user asks to wrap up, hand off, end a session, or record what shipped for the next agent. +--- + Read `docs/agentic-dev-kit/workflows/wrap-up.md` completely and follow it. Treat `$ARGUMENTS` as additional wrap-up context. Resolve all configured paths from From 87f02d9196e0cbbe8a1397ce840f67b091b35dea Mon Sep 17 00:00:00 2001 From: Topi Jarvinen Date: Sat, 25 Jul 2026 01:15:59 +0300 Subject: [PATCH 2/6] docs(claude): correct the two engine-blocked skill banners MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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). --- .claude/commands/post-merge-systemize.md | 22 ++++++++++++++++------ .claude/commands/triage-friction-log.md | 21 +++++++++++++++------ 2 files changed, 31 insertions(+), 12 deletions(-) diff --git a/.claude/commands/post-merge-systemize.md b/.claude/commands/post-merge-systemize.md index b88df16..ca51841 100644 --- a/.claude/commands/post-merge-systemize.md +++ b/.claude/commands/post-merge-systemize.md @@ -1,9 +1,19 @@ -> **v0 kit status — engine not yet vendored.** This skill documents the *doctrine* and the -> pattern-finding pipeline, but its deterministic engine — the merged-PR fetch/digest scripts -> and pipeline config it references (`scripts/…`, `config/…`) — is **not shipped in kit v0** -> (they depend on a forge/PR client and a tracker client that are project-specific). Until a -> later kit revision vendors them, either wire your own engine against this prose or run the -> skill in a lighter LLM-only mode. The four fully-wired skills (`session-start`, `wrap-up`, +--- +description: Weekly pattern-finding across the last 7 days of merged PRs — cluster review findings by root cause, and route patterns spanning ≥2 PRs to a CLAUDE.md/skill-prompt PR, single high-severity incidents to the tracker, and everything else to the friction log. Use for scheduled post-merge retros, not ad hoc single-PR review. +argument-hint: "[backfill|test]" +--- + +> **Engine tracked, not yet vendored — this is a blocked dependency, not unwritten +> work.** This skill documents the *doctrine* and the pattern-finding pipeline. A +> working deterministic engine — the merged-PR fetch/digest scripts and pipeline +> config it references (`scripts/…`, `config/…`) — already exists (in the kit +> maintainer's own toolkit repo) but isn't vendored into kit v0 yet: it's blocked on +> a tracker-adapter abstraction so the engine can target any configured +> `tracker.backend` rather than one hardcoded tracker client (it already talks to a +> forge/PR client generically). Tracked in +> [agentic-dev-kit#7](https://github.com/topij/agentic-dev-kit/issues/7). Until that +> lands, either wire your own engine against this prose or run the skill in a lighter +> LLM-only mode. The four fully-wired skills (`session-start`, `wrap-up`, > `parallel`, `pr-watch`) ship their engines and work out of the box. Weekly pattern-finding across the last 7 days of merged PRs: read each PR's review diff --git a/.claude/commands/triage-friction-log.md b/.claude/commands/triage-friction-log.md index 6375983..f3315cd 100644 --- a/.claude/commands/triage-friction-log.md +++ b/.claude/commands/triage-friction-log.md @@ -1,9 +1,18 @@ -> **v0 kit status — engine not yet vendored.** This skill documents the *doctrine* and the -> intended two-session (draft → operator approval → finalize) mechanism, but its -> deterministic engine — the parse/draft/finalize scripts and pipeline config it references -> (`scripts/…`, `config/…`) — is **not shipped in kit v0** (they depend on a tracker client, -> a notify channel, and a state-machine that are project-specific). Until a later kit revision -> vendors them, either wire your own engine against this prose or run the skill in a lighter +--- +description: Triage the friction-log inbox into tracker tickets — draft tracker-issue payloads from un-graduated entries, get operator approval via DM, then file approved tickets and open a draft PR sweeping them into the archive. Use to graduate accumulated friction-log entries into the tracker. +argument-hint: "[resume|new|test]" +--- + +> **Engine tracked, not yet vendored — this is a blocked dependency, not unwritten +> work.** This skill documents the *doctrine* and the intended two-session (draft → +> operator approval → finalize) mechanism. A working deterministic engine — the +> parse/draft/finalize scripts and pipeline config it references (`scripts/…`, +> `config/…`) — already exists (in the kit maintainer's own toolkit repo) but isn't +> vendored into kit v0 yet: it's blocked on a tracker-adapter abstraction so the +> engine can target any configured `tracker.backend` rather than one hardcoded +> tracker client. Tracked in +> [agentic-dev-kit#6](https://github.com/topij/agentic-dev-kit/issues/6). Until that +> lands, either wire your own engine against this prose or run the skill in a lighter > LLM-only mode. The four fully-wired skills (`session-start`, `wrap-up`, `parallel`, > `pr-watch`) ship their engines and work out of the box. From 82cbed8b2693753e930b27613eb1ca2bd955a4cf Mon Sep 17 00:00:00 2001 From: Topi Jarvinen Date: Sat, 25 Jul 2026 01:16:07 +0300 Subject: [PATCH 3/6] 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. --- scripts/hooks/pr_followup_hook.py | 129 ++++++++++++++++++++++++ scripts/tests/test_pr_followup_hook.py | 133 +++++++++++++++++++++++++ 2 files changed, 262 insertions(+) create mode 100755 scripts/hooks/pr_followup_hook.py create mode 100644 scripts/tests/test_pr_followup_hook.py diff --git a/scripts/hooks/pr_followup_hook.py b/scripts/hooks/pr_followup_hook.py new file mode 100755 index 0000000..28876ee --- /dev/null +++ b/scripts/hooks/pr_followup_hook.py @@ -0,0 +1,129 @@ +#!/usr/bin/env python3 +"""PostToolUse hook: after a PR is opened / marked ready, mandate the watch-fix loop. + +Wired in `.claude/settings.json` under `hooks.PostToolUse` with matcher `Bash` and +`if: "Bash(gh pr *)"`. The `if` pre-filters to `gh pr …` commands; this script +further narrows to `gh pr create` / `gh pr ready` (the moments a PR goes live for +review) and injects an `additionalContext` instruction so the session runs the +kit's PR follow-through loop (Principle #5 in `PRINCIPLES.md`) without being asked. +This closes the gap where the kit only had prose asking the agent to run `/pr-watch` +unasked (Principle #8: "a rule that lives only in a doc is a wish"). + +Generalized from a project-specific version: the reminder names whichever review +bot(s) and fallback command this repo actually configures, read from +`config/dev-model.yaml` via `scripts/lib/kitconfig.py` — never a hardcoded bot name +(Principle #10, "No hardcoding"). `paths.engines` resolves where `pr_watch.py` lives +so the reminder's poll command is correct even when the kit is vendored under a +nested engines dir. If config can't be read (missing file, kitconfig import +failure, or this script's own repo-root walk fails), the reminder still fires with +generic, runtime-agnostic wording rather than skipping the nag entirely — a +degraded reminder is better than a silently missing one. + +Cron-safe: a no-op when `JOB_NAME` is set (the kit's own cron/CI signal — see +`scripts/lib/state_paths/resolver.py`), so a scheduled workflow's own PR-opens never +trigger it. Always exits 0 — a hook must never fail a session. + +Reads the PostToolUse JSON on stdin; the bash command is at `.tool_input.command`. +To inject context from a PostToolUse hook you print +`{"hookSpecificOutput": {"hookEventName": "PostToolUse", "additionalContext": …}}` +(plain stdout is not surfaced for PostToolUse). +""" + +from __future__ import annotations + +import json +import os +import re +import sys +from pathlib import Path + +_TRIGGER = re.compile(r"\bgh\s+pr\s+(create|ready)\b") + +_DEFAULT_FALLBACK_COMMAND = "/code-review" +_DEFAULT_ENGINES_DIR = "scripts" + + +def _load_review_config() -> tuple[list[str], str, str]: + """Read ``(review.bots, review.fallback_commands.claude, paths.engines)``. + + Best-effort: any failure (missing config, kitconfig unimportable, malformed + values) falls back to generic defaults rather than raising — this hook must + never fail a session over a config read. + """ + try: + here = Path(__file__).resolve() + lib_dir = here.parent.parent / "lib" + sys.path.insert(0, str(lib_dir)) + import kitconfig # noqa: PLC0415 + + config = kitconfig.load_config() + bots = kitconfig.get_str_list(config, "review.bots", []) + fallback = kitconfig.get(config, "review.fallback_commands.claude", _DEFAULT_FALLBACK_COMMAND) + engines = kitconfig.get(config, "paths.engines", _DEFAULT_ENGINES_DIR) + if not isinstance(fallback, str) or not fallback: + fallback = _DEFAULT_FALLBACK_COMMAND + if not isinstance(engines, str) or not engines: + engines = _DEFAULT_ENGINES_DIR + return bots, fallback, engines + except Exception: + return [], _DEFAULT_FALLBACK_COMMAND, _DEFAULT_ENGINES_DIR + + +def _bot_description(bots: list[str]) -> str: + if not bots: + return "the configured review bot(s)" + names = [b.strip() for b in bots if isinstance(b, str) and b.strip()] + if not names: + return "the configured review bot(s)" + return " / ".join(names) + + +def build_reminder() -> str: + bots, fallback_command, engines_dir = _load_review_config() + bot_desc = _bot_description(bots) + return ( + "A pull request was just opened or marked ready for review. Per the kit's " + '"PR follow-through" policy (PRINCIPLES.md #5/#8) this step is MANDATORY and ' + "you should start it now without being asked: run the watch-and-fix loop for " + "this PR — invoke `/pr-watch` (or poll " + f"`uv run {engines_dir}/pr_watch.py --json`) and do NOT yield this turn " + f"until CI is fully green AND every {bot_desc} finding is fixed or " + "replied-to with a reason. Fix real findings, reply-with-reason to nitpicks " + "you disagree with, `--mark-seen` each handled round, and keep polling (CI " + "can take 20-30 min). If a review bot is unavailable, run the configured " + f"fallback (`{fallback_command}`) instead of treating the outage as a review " + "waiver. Only stop early if you hit something that genuinely needs an " + "operator decision." + ) + + +def main() -> int: + try: + data = json.load(sys.stdin) + except (json.JSONDecodeError, ValueError, UnicodeDecodeError): + return 0 # malformed payload — never block the session + + if os.environ.get("JOB_NAME"): + return 0 # cron/CI context — don't nag + + if not isinstance(data, dict): + return 0 + + tool_input = data.get("tool_input") + command = tool_input.get("command") if isinstance(tool_input, dict) else None + if isinstance(command, str) and _TRIGGER.search(command): + print( + json.dumps( + { + "hookSpecificOutput": { + "hookEventName": "PostToolUse", + "additionalContext": build_reminder(), + } + } + ) + ) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/tests/test_pr_followup_hook.py b/scripts/tests/test_pr_followup_hook.py new file mode 100644 index 0000000..d7d465f --- /dev/null +++ b/scripts/tests/test_pr_followup_hook.py @@ -0,0 +1,133 @@ +"""Tests for scripts/hooks/pr_followup_hook.py — the PostToolUse PR follow-through nag. + +Covers the contract the hook must hold regardless of config state: it only fires on +`gh pr create` / `gh pr ready`, never in a cron/CI context (`JOB_NAME` set), and it +always exits 0 — a malformed stdin payload must never fail the hosting session. +""" + +from __future__ import annotations + +import importlib.util +import io +import json +from pathlib import Path +from types import ModuleType + +import pytest + + +ENGINE_DIR = Path(__file__).resolve().parent.parent +HOOK_PATH = ENGINE_DIR / "hooks" / "pr_followup_hook.py" + + +def _load_hook() -> ModuleType: + spec = importlib.util.spec_from_file_location("pr_followup_hook_under_test", HOOK_PATH) + assert spec is not None and spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def _run(module: ModuleType, monkeypatch, capsys, stdin_text: str) -> tuple[int, str]: + monkeypatch.setattr("sys.stdin", io.StringIO(stdin_text)) + exit_code = module.main() + captured = capsys.readouterr() + return exit_code, captured.out + + +def _payload(command: str) -> str: + return json.dumps({"tool_input": {"command": command}}) + + +@pytest.fixture(autouse=True) +def _no_job_name(monkeypatch): + """Every test runs outside cron/CI unless it explicitly sets JOB_NAME.""" + monkeypatch.delenv("JOB_NAME", raising=False) + + +def test_triggers_on_gh_pr_create(monkeypatch, capsys): + hook = _load_hook() + exit_code, out = _run(hook, monkeypatch, capsys, _payload("gh pr create --draft --title x")) + assert exit_code == 0 + body = json.loads(out) + assert body["hookSpecificOutput"]["hookEventName"] == "PostToolUse" + assert "additionalContext" in body["hookSpecificOutput"] + assert "pr-watch" in body["hookSpecificOutput"]["additionalContext"] + + +def test_triggers_on_gh_pr_ready(monkeypatch, capsys): + hook = _load_hook() + exit_code, out = _run(hook, monkeypatch, capsys, _payload("gh pr ready 42")) + assert exit_code == 0 + body = json.loads(out) + assert body["hookSpecificOutput"]["hookEventName"] == "PostToolUse" + + +def test_does_not_trigger_on_gh_pr_view(monkeypatch, capsys): + hook = _load_hook() + exit_code, out = _run(hook, monkeypatch, capsys, _payload("gh pr view 42")) + assert exit_code == 0 + assert out == "" + + +def test_does_not_trigger_on_unrelated_bash_command(monkeypatch, capsys): + hook = _load_hook() + exit_code, out = _run(hook, monkeypatch, capsys, _payload("ls -la")) + assert exit_code == 0 + assert out == "" + + +def test_noops_when_job_name_is_set(monkeypatch, capsys): + hook = _load_hook() + monkeypatch.setenv("JOB_NAME", "nightly-triage") + exit_code, out = _run(hook, monkeypatch, capsys, _payload("gh pr create --draft")) + assert exit_code == 0 + assert out == "" + + +@pytest.mark.parametrize( + "stdin_text", + [ + "not json at all", + "", + "[]", + "null", + '"just a string"', + '{"tool_input": "not-a-dict"}', + '{"tool_input": {"command": 123}}', + ], +) +def test_exits_zero_on_malformed_or_unexpected_stdin(monkeypatch, capsys, stdin_text): + hook = _load_hook() + exit_code, out = _run(hook, monkeypatch, capsys, stdin_text) + assert exit_code == 0 + assert out == "" + + +def test_reminder_names_configured_bots_and_fallback_not_a_hardcoded_bot(monkeypatch): + """The reminder must be sourced from config (review.bots / fallback_commands), + never a hardcoded bot literal — this is the whole point of generalizing the + reference implementation (Principle #10, "No hardcoding").""" + hook = _load_hook() + reminder = hook.build_reminder() + assert "coderabbit" in reminder # from this repo's config/dev-model.yaml review.bots + assert "/code-review" in reminder # review.fallback_commands.claude + assert "codex" not in reminder.lower() + assert "bugbot" not in reminder.lower() + + +def test_load_review_config_degrades_gracefully_when_config_unreadable(monkeypatch): + """A missing/unreadable config must never raise — the hook falls back to + generic wording rather than failing the session.""" + hook = _load_hook() + hook._load_review_config() # prime the import so `kitconfig` lands in sys.modules + import kitconfig # noqa: PLC0415 + + def _boom(*_args, **_kwargs): + raise FileNotFoundError("no config here") + + monkeypatch.setattr(kitconfig, "load_config", _boom) + bots, fallback, engines = hook._load_review_config() + assert bots == [] + assert fallback == "/code-review" + assert engines == "scripts" From b0caf2303735b484ceae652f2b17d3d3579c36be Mon Sep 17 00:00:00 2001 From: Topi Jarvinen Date: Sat, 25 Jul 2026 01:16:17 +0300 Subject: [PATCH 4/6] feat(scripts): port check_memory_budget.py, the auto-memory tripwire MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- scripts/check_memory_budget.py | 222 ++++++++++++++++++++++ scripts/tests/test_check_memory_budget.py | 128 +++++++++++++ 2 files changed, 350 insertions(+) create mode 100755 scripts/check_memory_budget.py create mode 100644 scripts/tests/test_check_memory_budget.py diff --git a/scripts/check_memory_budget.py b/scripts/check_memory_budget.py new file mode 100755 index 0000000..1967f83 --- /dev/null +++ b/scripts/check_memory_budget.py @@ -0,0 +1,222 @@ +#!/usr/bin/env python3 +# /// script +# requires-python = ">=3.12" +# dependencies = [] +# /// +"""Warn-only tripwire: nudge a memory-index review when Claude Code's agent +memory grows too large. + +Claude Code's own per-project auto-memory index (``MEMORY.md`` — one pointer +line per saved memory) is loaded into context at every session start. Past a +size limit the loader **truncates** it — the oldest/least-relevant index +lines silently fall out of context, so a real memory becomes unrecallable. +``docs/handoff.md`` and ``docs/friction-log.md`` get a line-count budget +tripwire from ``check_doc_budget.py`` (Principle #1 in ``PRINCIPLES.md``); +this is the analogous guard for the memory index — the Claude-runtime +counterpart, since this artifact is Claude Code-specific and has no Codex +equivalent. + +Two signals, mirroring ``check_doc_budget.py``: + +* **Total size** vs a byte budget set just under the observed SessionStart + load limit, so the warning fires while there's still room to act rather + than after lines have already been dropped. +* **Per-line length** — any index pointer over ~200 chars. The index is meant + to be one terse "[Title](file.md) — hook" line each; an over-long line is a + hook that should be tightened (or the memory pruned). These also + disproportionately eat the byte budget. + +**Warn-only by design** — always exits 0 (a SessionStart hook should *nudge*, +never block). ``--strict`` makes it exit 1 when over budget (soft CI signal). + +Why this isn't driven by ``config/dev-model.yaml``'s ``doc_budgets`` list +(unlike ``check_doc_budget.py``): that list is iterated unconditionally by +``check_doc_budget.py`` — every entry's ``path`` is resolved relative to the +repo root and required to exist, with mandatory ``archive``/``remedy`` +fields. The memory index lives *outside* the repo at a path keyed by this +machine's Claude Code config dir and this repo's absolute path (not portable +across clones/machines, and not swept to an in-repo archive file), so adding +it as a ``doc_budgets`` entry would make every ``check_doc_budget.py`` run +fail loudly with "configured doc not found" wherever no memory has been +saved yet — including the very SessionStart hook this check itself rides +alongside. The byte/line-length budgets below are therefore kept as +documented platform constants (overridable via ``--max-bytes`` / +``--max-line-chars``), the same way ``check_doc_budget.py`` keeps its own +`MAX_BYTES`-equivalent tuning out of adopter config. ``scripts/lib/kitconfig`` +is still reused, for the one thing that *is* config-shaped here: portable +repo-root discovery (walking up for ``.git`` rather than assuming a fixed +``scripts/