Claude Code plugin that grooms and executes plans across user projects. Plans live in the per-project vault — ~/Claude/{project}/ by default, or a repo-local directory via the .booping marker's vault_path: key; skills, agents, templates, and config live in this repo.
Last updated: 2026-04-30.
Runtime template-rendering pipeline + build-time src/files/ pipeline are both live. Current state:
- Build-time skills/agents: every committed
skills/<name>/SKILL.mdandagents/<name>.mdis generated fromsrc/files/<rel>.j2viajust build(which callsbin/booping build). Frontmatter values that vary across files (today:effort) live insrc/config_files.yaml, a build-only config that is not project-overridable. - Runtime rendering: the body of each generated thin shell is a single
!booping render src/templates/.../.md.j2`` line; bodies are rendered at skill-load time bybin/booping(Python uv project at `booping-python/`). - Project config override:
~/Claude/{project}/config.yamldeep-merges oversrc/config.yamlat render time (runtime only — does not affectconfig_files.yaml). - Single CLI:
bin/boopingwith subcommandsrender,render-sprints,transition,frontmatter-update,vault-commit,build,debug-context,debug-template. - Static docs:
docs/is hand-authored from this point on (no build step). The single dynamic doc (plan_lifecycle_overview) is rendered at runtime viabin/booping render src/templates/docs/plan_lifecycle_overview.md.j2.
booping-python/— uv Python project containing theboopingCLI (subcommandsrender,render-sprints,transition,frontmatter-update,vault-commit,build,debug-context,debug-template). Source underbooping-python/src/booping/; tests underbooping-python/tests/.bin/booping— shell wrapper: resolves plugin root from its own location and exec'suv run --project booping-python booping "$@".bin/booping-create-project— standalone uv inline script; scaffolds the vault directories +.boopingmarker. Defaults to~/Claude/{project}/; with--local [dir](default./booping) scaffolds a repo-local vault and writes the.boopingvault_path:key + a vault.gitignore. Out of scope for the runtime pipeline.bin/booping-external-llm-call— standalone uv inline script; renders a Jinja2 prompt template frombin/llm-call-templates/and sends it to Gemini. Out of scope for the runtime pipeline.src/config.yaml— single source of truth for structured data rendered into skills at runtime (plan statuses + transitions, task types, per-skill agents / status / etc.). Loaded at render time byContext.assemble(). Project-overridable.src/config_files.yaml— build-only config consumed bybin/booping build. Carries frontmatter values that vary acrosssrc/files/**/*.j2templates (today: justeffortper skill / per agent). Not project-overridable.src/files/<rel>.j2— build-time templates for every committedskills/<name>/SKILL.mdandagents/<name>.md. Mirrors the plugin root one-to-one. Each template is a thin shell (frontmatter + a single!booping render src/templates/...`` line); rendered byjust buildusing `src/config_files.yaml`.src/templates/skills/<name>.md.j2— runtime skill templates. Rendered at skill-load time via!booping render ...`` in the generated thin shell.src/templates/agents/<name>.md.j2— runtime agent templates. Same shape. The developer agent's body lives in_partials/_developer_body.j2.src/templates/docs/plan_lifecycle_overview.md.j2— the single runtime-rendered doc (usesconfig.plan.statuses). Invoked from/chatviabin/booping render.src/templates/_partials/_*.j2— reusable fragments. Include via{% include %}(data-only) or import + macro (parameterized, e.g._plan_transitions.j2). Parameterized partials receive call-site arguments via thekwargsnamespace.skills/<name>/SKILL.md— build artefact rendered fromsrc/files/skills/<name>/SKILL.md.j2. Never hand-edit; edit thesrc/files/template and runjust build.agents/<name>.md— build artefact rendered fromsrc/files/agents/<name>.md.j2. Same rule.docs/— hand-authored static reference docs, lazy-loaded by skills via${CLAUDE_PLUGIN_ROOT}/docs/<name>.mdlinks. Includesdocs/plan_templates/*.md,docs/review_templates/*.md,docs/template_plan_frontmatter.md, anddocs/images/. No build step touches this directory.documentation/— end-user docs site source; per-page markdown consumed by MkDocs (mkdocs.ymlat repo root). Built and deployed to thegh-pagesbranch by.github/workflows/docs.ymlon push tomain(published athttps://A.github.io/claude-booping/). Also lazy-loaded by/helpvia${CLAUDE_PLUGIN_ROOT}/documentation/<name>.md. Distinct fromdocs/(which is plugin-internal lazy-load fragments owned by skills);documentation/is the public reference site.~/Claude/{project}/config.yaml— optional per-project config override; deep-merges oversrc/config.yamlat render time (runtime only).~/.claude/agents/<id>.md— a project may register self-contained global agents as external workers (project-level user config, native to Claude Code; not maintained by the plugin). Wiring details: documentation/integrating-external-agents.md.
Every piece of information lives in exactly one of these owners. When you can't decide where something belongs, walk this list top-down.
Loaded at skill-load time by Context.assemble() — no build step. Per-project overrides live at ~/Claude/{project}/config.yaml and deep-merge over src/config.yaml (project keys win; missing keys fall through; lists replace wholesale). The architecture leaves room for a future global config (e.g. ~/Claude/config.yaml) by accepting an ordered override-paths list in Config.load() — adding a global tier is a one-line change when needed.
- Where a plan can go (statuses, transitions, owner).
- Boundaries of each move (
gates— verifiable preconditions;when— human-readable trigger). - Artifacts produced in a state.
- Side effects of a move (
hooksper transition +plan.superstates.<name>.on_entry/on_exitboundary hooks +plan.hooks.post— frontmatter mutations, sprints.md re-render, vault commit), executed deterministically bybooping transition. - Structured surfaces that other skills / CLIs also consume: task types, sprint scale, agent capabilities, branch conventions.
- Delegation targets — which agents a skill may delegate to, declared as
skills.<name>.agents.<id>entries (built-in workers plus any self-contained global agents the project registers). See documentation/integrating-external-agents.md.
Instructions, goal, rules, and judgement specific to one skill — the verbs and heuristics for clearing the gates that config defines.
- Verbs and heuristics — how to produce the artifacts and clear the gates for the states this skill owns.
- User interaction — what to ask, when, how to present, how to handle approval vs change requests vs cancellation.
- Judgment calls the contract can't pre-specify.
- Invariants the skill enforces across all its states ("Hard rules").
Content shared across skills/agents, or extracted from a single skill body to keep it lean.
- Cross-cutting guides included into multiple skills (git guide, plan transitions, available agents, project context).
- Process fragments extracted from a single skill into its own partial (sprint planning, plan-frontmatter shape, task classification).
- Bodies extracted from a single agent into a partial (
_developer_body.j2for the developer agent). - Lazy-loaded craft docs (
docs/*.md) that a skill links into via[label](${CLAUDE_PLUGIN_ROOT}/docs/<name>.md)when the situation demands it.
Out of framework scope — authored per-project, loaded by skills at runtime.
- Per-skill / per-agent extensions (
_booping/skill_<name>.md,_booping/agent_<name>.md). - Accumulated lessons (
lessons/). - Project-local plan templates (
plan_templates/*.md). - Project-local review templates (
review_templates/*.md).
The attached repo's own CLAUDE.md is also loaded by skills (it sits in the repo, not the vault) — same role as the above: project conventions the framework reads but doesn't author.
- Minimum useful context: show only information the skill needs to perform its job. Never bake in stale, speculative, or unrelated data.
- Less prose, less drift: every extra sentence in a skill or partial adds tension between what's written and what the model does. Cut motivation, restated context, "you are the skill / you are responsible to" preambles, and any explanation the schema or a referenced doc already carries. The schema is the source of truth; prose decays.
- Schema over prose: structured data lives in
src/config.yaml. If a format or value is there, the skill body must not also describe it in prose — render from config.
When you write a skill, walk these questions top-down for every piece of information you're tempted to inline. Each step moves the cost out of the skill body and into a cheaper mechanism.
-
Does the skill need this for its main route? Load it eagerly: inline a CLI fact via
!command``, or include a static fragment as a partial.- Example: project name + path rendered into skill body via
{% include "_partials/_project_context.j2" %}. - Example: the git guide included via the
_git_guide.j2partial.
- Example: project name + path rendered into skill body via
-
Does the skill need this only on a specific route, or only when a condition holds? Lazy-load with a
[label](${CLAUDE_PLUGIN_ROOT}/docs/<name>.md)link; the model fetches it when the situation demands it.- Example:
/groomlinks to per-task-type guidance; only the active type's guide (one offeature,bug,refactoring) is loaded per plan.
- Example:
-
Does the skill need the whole, or only one slice? Split into a partial.
- Example:
/developonly needs the plan frontmatter shape; it does not pull in the full plan-template body.
- Example:
-
Is this information already available to the skill? Remove the duplicate.
- Example: plan transitions are rendered into each skill via
_plan_transitions.j2. Skills must not restate the flow in their own prose.
- Example: plan transitions are rendered into each skill via
-
Is the value dynamic? Reference the source, never the literal.
- Example: the status
/learnqueries to find plans to absorb is named insrc/config.yaml; the skill renders from there rather than spelling the status name in prose.
- Example: the status
- Wide-domain: skills must work across stacks (Django, Rust, Hugo, etc.). Project-specific concerns live in
~/Claude/{project}/_booping/skill_<name>.md,lessons/, and the project's ownCLAUDE.md— never in the skill here. - Phases over flat sections: Preflight → High-level workflow → Phase 0..N.
- Preflight becomes thinner in template-driven skills: items that were "Read partial X" now appear either as inlined partial content, as
!command`` blocks, or as lazy[detailed guidance](${CLAUDE_PLUGIN_ROOT}/docs/…)links. Preflight is for the things that still require the model to act (e.g. read vault lessons, read `booping/skill.md`). - Research delegation: delegate heavy reads / summarization work to
booping-researcherto protect the skill's context. The agent is for aggregating many sources into a summary, not for single-file spot checks — those stay in the skill. - Agent wiring: skills own all reads/writes against
~/Claude/{project}/. Agents touch only code in the attached repo and never scan the vault. Briefings carry the request, related files, and DoD — no lesson paths. Lesson context reaches agents via two baked-in channels: DoD + Verify pasted from the plan (folded in by/groom) and the shared extension at~/Claude/{project}/_booping/agent_booping-<name>.md, injected at agent load time viatools.render('src/templates/_partials/_extra_instructions.j2', extra_instruction_key='agent_booping-<name>')in the agent template (owned by/learn). A skill (e.g./develop) may also delegate to a self-contained global agent the project provides at~/.claude/agents/<id>.md— invoked by bare name through the Agent tool (subagent_type="<id>"), the same path the built-in workers use. Such an agent is wired in via a plainskills.<name>.agents.<id>config block plus a_booping/skill_<name>.mdextension that shapes its briefing. See documentation/integrating-external-agents.md. - Per-project quality checks:
/developruns the project's own lint / typecheck / test tooling once at Phase 4 (Final Verification), alongside plan-authoredVerifycommands. Command discovery order: repoCLAUDE.md→_booping/skill_develop.md→ inspection ofpackage.json,pyproject.toml,Justfile, etc. - Local-vault branch offer:
_project_context.j2exposesis_local_vault(true when the resolved vault lives inside the repo working tree). On a local vault,/groomoffers to create a branch for the plan before drafting it; the skill carries aBash(git:*)allowance for that step. This is the intended end-state of the local-vault sprint (/installprompts for vault location,booping-create-project --localscaffolds it).
Top-level keys currently in use:
skills.<name>.agents.<agent-name>.good_for/.bad_for— delegation guidance rendered by_available_agents.j2. Currently used by/groomand/develop.skills.<name>.agents.<agent-name>.internal(true on built-in entries) and per-skillskills.<name>.disable_internal_agents— whendisable_internal_agentsis set,_available_agents.j2hides booping's built-in (internal: true) agents from that skill's table, leaving only the agents the project explicitly registered (e.g. a self-contained global external agent). See documentation/integrating-external-agents.md.skills.<name>.status— the plan status this skill owns or reads.learnownsawaiting-learning;retroownsawaiting-retro.code-reviewis stateless (does not own a status) but readsskills.code-review.status(defaultawaiting-retro) for its argument-free plan picker. Rendered into skill bodies that gate on or query a single status.git.branches— list of{branch, when}entries.branchis the literal prefix string (slash and format up to user, e.g.feat/);whenis a list of short matches (plantypenames likefeature, or freeform descriptors). Rendered via_git_guide.j2macro; consumed by/developfor branch selection.plan.statuses.<key>—desc,owner(skill),terminal(bool), optionalartifacts(list of strings describing what the state produces),transitions(list of{to, skill, when, gates?, hooks?}). Filtered per-skill by_plan_transitions.j2(macro shows only statuses a skill owns or has transitions out of, and only rows whereskill == <current>).gatesis a list of verifiable preconditions rendered into theGatescolumn.hooksis a list of machine-readable hook strings (e.g.frontmatter-update planned=@now,frontmatter-update commit=@head) thetransitioncommand runs when the edge fires; they replace the old proseon_exitstrings. The partial summarises them into theOn exit (auto)column.plan.superstates.<name>— OR-superstate grouping over statuses, forming the lifecycle spinespecification→planned→executing→review→terminal(specification= {backlog, in-spec};planned= {awaiting-plan-review, ready-for-dev};executing= {in-progress};review= {awaiting-retro, awaiting-learning};terminal= {done, fail, cancelled}). Phase-wide edges live on the superstate, not repeated per status:specificationandplannedboth offer → cancelled;executingoffers → fail;reviewoffers → done (the skip-ahead path). Each carriesstates(the member statuses), optionaltransitions(edges inherited by every member), andon_entry/on_exit(boundary hook lists fired when a move crosses the superstate boundary). Edge resolution: a status's own edges ∪ its superstate's inherited edges, with the substate winning ontocollision (e.g. awaiting-learning's gated → done shadows the review skip edge). Inherited edges render tagged*(via <superstate>)*. Resolved perbooping.context.lifecycle.resolve_edges(edges) andresolve_hooks(boundaryon_exit+ edgehooks+ boundaryon_entry); thetransitioncommand and_plan_transitions.j2both consume the resolution.plan.hooks.post— list of hook names (render-sprints,vault-commit) thetransitioncommand runs after every edge's per-transition hooks, on every move. The hook vocabulary across both surfaces:frontmatter-update <key>=<val>...(set/interpolate frontmatter),render-sprints(re-render the vaultsprints.md),vault-commit(stage + commit plan + sprints.md).frontmatter-updatehooks appear per-edge and in superstate boundaries;render-sprints/vault-commitare the implicit automaticposthooks (suppressed from the renderedOn exit (auto)column).tasks— list of{type, description, doc_uri}. Rendered by_task_classification.j2as bullets with a lazy-load link todoc_uri(relative from repo root).sprint—scale(list of{sp, meaning}),default_threshold_sp(SP total past which /groom should propose splitting — not a velocity),redecompose_threshold(tasks ≥ this SP must be re-decomposed),group_threshold(tasks ≤ this SP should be grouped into one agent briefing). Rendered by_sprint_planning.j2;redecompose_threshold/group_thresholdare skipped when falsy. Threshold is rendered inline at skill-load time fromconfig.sprint.default_threshold_sp.
- Statuses + transitions live in
src/config.yaml(plan.statuses). The rendered skill body shows only that skill's slice. Terminal states:done,fail,cancelled. - Status transitions are executed via
booping transition <to> <plan>, a deterministic hook-runner — not manual frontmatter edits or hand-run git. The LLM decides the edge and clears the judgmentgates; the command applies every mechanical mutation (status set, date stamps, commit snapshot, sprints.md re-render, vault commit) atomically. The skill fires it when a trigger in its rendered transitions table matches an internal action. - Current flow:
backlog → in-spec → awaiting-plan-review → ready-for-dev → in-progress → awaiting-retro → awaiting-learning → done, withcancelled/failterminal branches and in-groom loopbacks (awaiting-plan-review → in-spec,in-spec → backlog).backlogis for parked plans only (split stubs, user-filed ideas not yet in grooming). Active groom runs write directly toin-spec.in-specis where/groomdoes its work.awaiting-plan-reviewis the explicit user-approval gate.ready-for-devis the queue/developclaims from. No backlog-shortcut into/develop.
- Each transition carries
when(trigger), optionalgates(verifiable preconditions), and optionalhooks(machine-readable side effects thetransitioncommand runs — e.g.frontmatter-update planned=@now). Superstates contribute boundaryon_entry/on_exithooks and inherited edges;plan.hooks.postadds the automaticrender-sprints+vault-committail on every move. Each status carries optionalartifacts(what the state produces). - Plans carry an optional
commit:frontmatter field (repo HEAD at the time of the snapshot). Set onin-spec → awaiting-plan-review(groom finalises draft) and re-snapshotted on entry to theexecutingsuperstate (/develop intake, after the user confirmed the plan is still valid) — both via afrontmatter-update commit=@headhook thetransitioncommand runs. Legacy plans withoutcommit:continue to load withcommit: None. - After a transition, verify by re-reading the plan frontmatter to confirm
status:matches the new state. sprints.mdis a snapshot rendered fromcontext.plansviabooping render-sprints(templatesrc/templates/sprints.md.j2). Everybooping transitionmove refreshes it (the automaticrender-sprintspost hook) and commits it alongside the plan (thevault-commitpost hook), so it stays current across transitions./chatalso refreshes it on orient. Never hand-edit it.
- The vault lives at
~/Claude/{project}/by default, or at a repo-local directory when the.boopingmarker carries an optionalvault_path:key (relative paths resolve against the repo root; absolute paths and~expansion are honoured; absent →~/Claude/{project}).booping-create-project --local [dir](default./booping) scaffolds the local variant and writes bothvault_path:and a vault.gitignore. - The vault is Obsidian-ready: markdown files + YAML frontmatter that Obsidian renders as Properties. No proprietary database. Open the vault directory in Obsidian for graph view + backlinks across plans, retros, and lessons.
plans/{YYYYMMDD}-{kebab-title}.md— plan files; frontmatter perdocs/template_plan_frontmatter.md. Sibling stubs setsplit_from: plans/...to point at the primary plan they were split from.plan_templates/*.md— project-local plan templates. Each has frontmatter (name,description) + two top-level sections (# Plan Body,# Quality Checklist). Discovered alongside core templates byPlanTemplate.load_all(); can override a core template by sharing itsname, or add entirely new ones.review_templates/*.md— project-local code-review templates. Loaded by/code-reviewalongside core templates underdocs/review_templates/; selected per-plan based on stack signals.lessons/— accumulated lessons; loaded by skills' Preflight.notes/— user notes (plan-review comments, code-review threads, ideas for next sprints). Not consumed by skills or agents — purely for the user's own reference._booping/skill_<name>.md— project-local extensions to wide-domain skills._booping/.booping.log— append-only invocation log.render/render-sprintswrite one line per call. Format:<iso8601-utc>: [<subcommand>] <detail>.
(Project conventions live in the attached repo's own CLAUDE.md, not in the vault.)
bin/booping render <template-path> [--output <path>]— render a Jinja2 template with full project context to stdout (or to a file with--output). Used at skill-load time via!booping render ...`` and to invoke the runtimeplan_lifecycle_overviewdoc.bin/booping render-sprints [--output <path>]— render<vault>/sprints.mdfromsrc/templates/sprints.md.j2. Default output is the resolved vault'ssprints.md;--output PATHoverrides;--output -writes to stdout.bin/booping transition <to> <plan> [--also <path>...]— deterministic hook-runner: move a plan to status<to>and apply every mechanical mutation the resolved edge declares (status set, date stamps, commit snapshot, sprints.md re-render, vault commit) in one shot. The LLM decides the edge and clears the judgment gates; this command owns all the mechanics.--also <path>...stages extra vault artifacts (e.g. a retrospective file, a sibling stub) into the same transition commit.bin/booping frontmatter-update <plan> [key=val ...] [--remove <key>...]— set or remove plan frontmatter keys. Values support@now(UTCyyyymmdd hh:mm),@today(yyyymmdd), and@head(repo HEAD sha) interpolation.--remove <key>(repeatable) drops keys; removals apply beforekey=valsets. The atomic primitive thefrontmatter-updatehook is built on.bin/booping vault-commit <to-status> <plan> [--also <path>...]— path-scoped staging hook: stage and commit the plan file +sprints.md(plus any--alsopaths) in the vault git repo, with<to-status>in the commit message. The git side-effect thevault-commitpost hook is built on.bin/booping build— render everysrc/files/**/*.j2to its plugin-root destination usingsrc/config_files.yaml. Run after editing anysrc/files/<rel>.j2orsrc/config_files.yaml.bin/booping debug-context— dump the assembledContextas YAML for troubleshooting.bin/booping debug-template <template-path>— render a template and append a debug context footer.bin/booping-create-project <project-name> [--local [dir]]— scaffold the vault directories +.boopingmarker. By default the vault lands at~/Claude/{project}/;--local [dir](default./booping) scaffolds a repo-local vault instead, writing avault_path:key into.boopingplus a vault.gitignore.bin/booping-external-llm-call --prompt=<name> --context.<key>=<path>... [-- <free-text>]— render a Jinja2 prompt template frombin/llm-call-templates/<name>.md.j2and send it to Gemini. Handles its own API-key check. Current templates:validate-plan.just build— one-shot build viabin/booping build.just devwatchessrc/files/+src/config_files.yamland rebuilds on change (requireswatchexec).just lint,just typecheck,just test— run ruff, basedpyright, pytest againstbooping-python/.
- Edits to
src/templates/skills/*.md.j2,src/templates/agents/*.md.j2, andsrc/templates/_partials/*.j2are live — rendered at skill-load time, no rebuild needed. - Edits to
src/files/**/*.j2orsrc/config_files.yamlrequirejust buildto materialize into the on-diskskills/<name>/SKILL.md/agents/<name>.md.git diff -- skills/ agents/afterjust buildis the drift signal. skills/<name>/SKILL.mdandagents/<name>.mdare build artefacts — never hand-edit. Editsrc/files/<rel>.j2(orsrc/config_files.yamlfor shared frontmatter values) and runjust build.docs/*.mdis hand-authored. No build step regenerates it.documentation/*.mdis hand-authored. Onlymkdocs buildconsumes it, and the build runs in GitHub Actions on push tomain(.github/workflows/docs.yml) — there is no local-build-on-commit step. Usejust docs/just docs-servefor local preview.- Prefer extracting to a partial over inlining when prose grows past a paragraph or two.
- Prefer moving structured data into
src/config.yamlover prose in partials. - No comments that restate code. Only WHY for non-obvious bits.
- Conventional commits with scope:
feat(booping): ...,fix(install): ..., etc. - The plugin code itself stays stack-agnostic — no Python/Django/JS specifics inside skills.
- README.md's Statuses section is hand-maintained narrative — revisit it whenever
src/config.yamlplan.statuseschanges (status name, description, terminal flag, or addition/removal).
Use src/templates/skills/chat.md.j2 as the reference.
- Decide what goes in config vs prose: structured values (per-skill agents, owned status, task-type / status / transition surfaces) belong in
src/config.yaml; verbs, heuristics, and judgment calls go in the template. - Author the template at
src/templates/skills/<name>.md.j2. No frontmatter — the thin shell carries it. Standard wiring:{% include "_partials/_project_context.j2" %}— loads project name/path at render time.{{ available_agents.render("<name>") }}(import macro withwith context) — renders agent delegation table from config.{{ plan_transitions.render("<name>") }}(import macro withwith context) — renders the transitions slice for this skill.{{ tools.render('src/templates/_partials/_lessons.j2') }}— inlines live lessons.{{ tools.render('src/templates/_partials/_extra_instructions.j2', extra_instruction_key='skill_<name>') }}— inlines project-local skill extension.
- Add the skill's
agentsblock (and anystatusit owns) tosrc/config.yamlunderskills.<name>. Skills with no structured surface still get an empty entry (<name>: {}) so_available_agents.j2can resolve them. - For any detail only some routes need, write it as
docs/<name>.mdand lazy-link from the skill body with[label](${CLAUDE_PLUGIN_ROOT}/docs/<name>.md)rather than inlining. - Author
src/files/skills/<name>/SKILL.md.j2as a thin shell: copy frontmatter shape from a sibling (e.g.src/files/skills/chat/SKILL.md.j2); setallowed-tools(includeBash(booping:*)plus any non-booping shell calls the skill needs); useeffort: {{ skills.<name>.effort }}and add the matching key tosrc/config_files.yamlunderskills:; set the body to!booping render src/templates/skills/.md.j2``. - Run
just buildto materializeskills/<name>/SKILL.md. Sanity-check by runningbin/booping render src/templates/skills/<name>.md.j2and inspecting the output.