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
4 changes: 4 additions & 0 deletions .claude/commands/adopt.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
5 changes: 5 additions & 0 deletions .claude/commands/parallel.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
22 changes: 16 additions & 6 deletions .claude/commands/post-merge-systemize.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
5 changes: 5 additions & 0 deletions .claude/commands/pr-watch.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
4 changes: 4 additions & 0 deletions .claude/commands/session-start.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
21 changes: 15 additions & 6 deletions .claude/commands/triage-friction-log.md
Original file line number Diff line number Diff line change
@@ -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.

Expand Down
4 changes: 4 additions & 0 deletions .claude/commands/wrap-up.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
43 changes: 43 additions & 0 deletions .claude/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
{
"permissions": {
"allow": [
"Bash(gh pr view:*)",
"Bash(gh pr list:*)",
"Bash(gh pr checks:*)",
"Bash(gh run view:*)",
"Bash(uv run scripts/pr_watch.py:*)"
]
},
"hooks": {
"SessionStart": [
{
"matcher": "startup",
"hooks": [
{
"type": "command",
"command": "[ -z \"$JOB_NAME\" ] && cd \"$CLAUDE_PROJECT_DIR\" && uv run --script scripts/check_doc_budget.py --quiet || true",
"timeout": 15
},
{
"type": "command",
"command": "[ -z \"$JOB_NAME\" ] && cd \"$CLAUDE_PROJECT_DIR\" && uv run --script scripts/check_memory_budget.py --quiet || true",
"timeout": 15
}
]
}
],
"PostToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "python3 \"$CLAUDE_PROJECT_DIR/scripts/hooks/pr_followup_hook.py\"",
"if": "Bash(gh pr *)",
"timeout": 10
}
]
}
]
}
}
32 changes: 32 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Makefile — thin entry points: `make install-hooks` and `make 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 — see init.sh's own
# comments on why) is 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 — well outside what a
# target named "install-hooks" should touch, and on a repo whose narrative
# docs still carry the `devkit-template: unrendered` marker (this kit's own
# repo, before an adopter has claimed them) it would rewrite docs/** as a
# side effect. So this target extracts and runs just the install_hooks()
# function body straight out of init.sh — it always reflects init.sh's
# current logic, never a stale/diverged copy — after resolving
# `paths.engines` the same way init.sh does (via config/dev-model.yaml,
# falling back to `scripts`), reusing scripts/lib/kitconfig.py to read it.
#
# test
# ----
# Runs the same suites the lane contract's local gate runs before every push.

.PHONY: install-hooks test

install-hooks:
@engines_dir="$$(python3 -c "import sys; sys.path.insert(0, 'scripts/lib'); import kitconfig; c = kitconfig.load_config(); print(kitconfig.get(c, 'paths.engines', 'scripts'))" 2>/dev/null || echo scripts)"; \
eval "$$(sed -n '/^install_hooks() {/,/^}/p' init.sh)"; \
install_hooks

test:
uv run --with pytest --with pyyaml python -m pytest scripts/lib/state_paths/tests scripts/tests -q
222 changes: 222 additions & 0 deletions scripts/check_memory_budget.py
Original file line number Diff line number Diff line change
@@ -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/<script>.py`` depth), so the repo-slug this script derives stays
correct even when the kit is vendored under a nested `paths.engines` dir.

The memory dir lives **outside the repo**, at
``${CLAUDE_CONFIG_DIR:-~/.claude}/projects/<repo-slug>/memory/MEMORY.md``
where ``<repo-slug>`` is the repo's absolute path with every non-alphanumeric
char replaced by ``-`` (Claude Code's own project-dir naming).
``--memory-file`` overrides this (tests, other machines).

Usage:

python3 scripts/check_memory_budget.py # report
python3 scripts/check_memory_budget.py --quiet # print only when over budget (the hook)
python3 scripts/check_memory_budget.py --strict # exit 1 when over budget
python3 scripts/check_memory_budget.py --json # machine-readable

Exit codes:
0 — always (warn-only), unless ``--strict`` and over budget.
2 — usage error (MEMORY.md not found at the resolved path).

Stdlib-only and ~milliseconds so it is safe to run from a SessionStart hook.
"""

from __future__ import annotations

import argparse
import json
import os
import re
import sys
from dataclasses import dataclass
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parent / "lib"))
from kitconfig import repo_root # noqa: E402

# Discover the repo root by walking up for a `.git` marker (via kitconfig)
# rather than assuming a fixed `scripts/<script>.py` depth — so the derived
# repo-slug (below) stays correct when the kit is vendored under a nested
# dir (e.g. scripts/devkit/).
REPO_ROOT = repo_root()

# Budget sits just under the observed SessionStart load limit so the warning
# fires with headroom to act (≈10+ memories) before truncation, not after. A
# tuning constant for a dev-tool feature — documented here, not in config
# (see module docstring for why this isn't a `doc_budgets` entry).
MAX_BYTES = 24_000
# The index is one terse pointer line per memory; over ~200 chars means an
# over-long hook to tighten (or a memory to prune).
MAX_LINE_CHARS = 200


def default_memory_file() -> Path:
"""Resolve MEMORY.md from the repo slug (Claude Code's project-dir naming).

``<repo-slug>`` is the repo's absolute path with every non-alphanumeric char
replaced by ``-`` — e.g. ``/home/u/agentic-dev-kit`` -> ``-home-u-agentic-dev-kit``.
"""
config_dir = Path(os.environ.get("CLAUDE_CONFIG_DIR") or (Path.home() / ".claude"))
slug = re.sub(r"[^a-zA-Z0-9]", "-", str(REPO_ROOT))
return config_dir / "projects" / slug / "memory" / "MEMORY.md"


@dataclass(frozen=True)
class MemoryStatus:
path: str
size_bytes: int
line_count: int
max_bytes: int
max_line_chars: int
long_lines: list[tuple[int, int]] # (1-based line number, length) for over-long lines

@property
def over_size(self) -> bool:
return self.size_bytes > self.max_bytes

@property
def over(self) -> bool:
return self.over_size or bool(self.long_lines)


def evaluate(memory_file: Path, *, max_bytes: int = MAX_BYTES, max_line_chars: int = MAX_LINE_CHARS) -> MemoryStatus:
"""Measure MEMORY.md against the byte budget + per-line length budget.

Raises ``FileNotFoundError`` if the index is missing at ``memory_file``.
"""
if not memory_file.is_file():
raise FileNotFoundError(f"memory index not found: {memory_file}")
text = memory_file.read_text(encoding="utf-8")
lines = text.splitlines()
long_lines = [(i, len(line)) for i, line in enumerate(lines, start=1) if len(line) > max_line_chars]
return MemoryStatus(
path=str(memory_file),
size_bytes=len(text.encode("utf-8")),
line_count=len(lines),
max_bytes=max_bytes,
max_line_chars=max_line_chars,
long_lines=long_lines,
)


def render(status: MemoryStatus, *, quiet: bool) -> str:
"""Human-readable report. When ``quiet`` only print if over budget."""
if not status.over:
return (
""
if quiet
else f"✓ MEMORY.md {status.size_bytes / 1000:.1f} KB / {status.line_count} lines (budget ~{status.max_bytes / 1000:.0f} KB)"
)
lines: list[str] = []
if status.over_size:
lines.append(
f"⚠ MEMORY.md is {status.size_bytes / 1000:.1f} KB (budget ~{status.max_bytes / 1000:.0f} KB) — "
f"approaching the SessionStart load limit; a review/prune pass over the oldest or "
f"least-relevant memories is overdue."
)
if status.long_lines:
shown = ", ".join(f"L{n} ({length}c)" for n, length in status.long_lines[:5])
more = "" if len(status.long_lines) <= 5 else f" (+{len(status.long_lines) - 5} more)"
lines.append(
f"⚠ {len(status.long_lines)} index line(s) over {status.max_line_chars} chars: {shown}{more} — "
f"tighten the hook (or prune the memory)."
)
return "\n".join(lines)


def main(argv: list[str] | None = None) -> int:
"""CLI entry point. Returns 0 (warn-only), 1 (``--strict`` and over), or 2 (missing file)."""
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--memory-file", type=Path, default=None, help="MEMORY.md path (default: repo-slug-derived).")
parser.add_argument("--quiet", action="store_true", help="print nothing when under budget (for the hook).")
parser.add_argument("--strict", action="store_true", help="exit 1 when over budget.")
parser.add_argument("--json", action="store_true", help="machine-readable output.")
parser.add_argument("--max-bytes", type=int, default=MAX_BYTES, help=f"byte budget (default {MAX_BYTES}).")
parser.add_argument(
"--max-line-chars", type=int, default=MAX_LINE_CHARS, help=f"per-line char budget (default {MAX_LINE_CHARS})."
)
args = parser.parse_args(argv)
if args.max_bytes <= 0 or args.max_line_chars <= 0:
parser.error("--max-bytes and --max-line-chars must be positive integers")

memory_file = args.memory_file.resolve() if args.memory_file else default_memory_file()
try:
status = evaluate(memory_file, max_bytes=args.max_bytes, max_line_chars=args.max_line_chars)
except (OSError, UnicodeDecodeError) as exc:
# FileNotFoundError (missing index) is the common case, but a permission
# or decode error must also exit 2 cleanly rather than traceback — this
# runs from a SessionStart hook.
print(f"error: {exc}", file=sys.stderr)
return 2

if args.json:
print(
json.dumps(
{
"path": status.path,
"size_bytes": status.size_bytes,
"line_count": status.line_count,
"max_bytes": status.max_bytes,
"over_size": status.over_size,
"long_lines": status.long_lines,
"over": status.over,
}
)
)
else:
report = render(status, quiet=args.quiet)
if report:
print(report)

return 1 if (args.strict and status.over) else 0


if __name__ == "__main__":
raise SystemExit(main())
Loading
Loading